feat(i18n): Enhance internationalization support for tool specifications and notifications
- Added TitleKey and DescriptionKey fields to ToolSpec for dynamic localization. - Updated tool specifications to use i18nx for titles, descriptions, and appendices. - Refactored notification messages to utilize i18nx for localization in conversation and ticket assignment events. - Improved localization in the debug dialog component for skill definitions. - Added new translation keys in English and Chinese for ticket and conversation assignment notifications. - Ensured that fallback messages are properly localized based on user locale.
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
componenttool "github.com/cloudwego/eino/components/tool"
|
||||
@@ -89,7 +90,7 @@ func (g *CreateTicketGraph) Run(ctx context.Context, argumentsInJSON string) (st
|
||||
Handled: true,
|
||||
Terminal: true,
|
||||
Action: "ticket_created",
|
||||
ReplyText: fmt.Sprintf("Ticket created. Ticket no: %s. Title: %s.", strings.TrimSpace(item.TicketNo), strings.TrimSpace(item.Title)),
|
||||
ReplyText: i18nx.Getf(i18nx.DefaultLocale, "graph.ticketCreated", strings.TrimSpace(item.TicketNo), strings.TrimSpace(item.Title)),
|
||||
ShouldRetry: false,
|
||||
}), nil
|
||||
case ConfirmationDecisionCancel:
|
||||
@@ -134,7 +135,7 @@ func (g *CreateTicketGraph) buildCreateRequest(argumentsInJSON string) (request.
|
||||
}
|
||||
|
||||
func (g *CreateTicketGraph) buildConfirmationPrompt(req request.CreateTicketFromConversationRequest) string {
|
||||
return fmt.Sprintf("I am ready to create a ticket for you.\nTitle: %s\nDescription: %s\nPlease reply with \"Confirm\" or \"Cancel\".",
|
||||
return i18nx.Getf(i18nx.DefaultLocale, "graph.createTicketConfirmPrompt",
|
||||
strings.TrimSpace(req.Title), strings.TrimSpace(req.Description))
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"agent-desk/internal/ai/runtime/tooling"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/tracex"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
@@ -122,7 +123,7 @@ func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string,
|
||||
}
|
||||
|
||||
func (g *HandoffGraph) buildReason(argumentsInJSON string) (string, error) {
|
||||
reason := "The user needs human support."
|
||||
reason := i18nx.Get("graph.defaultHandoffReason")
|
||||
var args handoffGraphArgs
|
||||
if strings.TrimSpace(argumentsInJSON) != "" {
|
||||
if err := json.Unmarshal([]byte(argumentsInJSON), &args); err != nil {
|
||||
@@ -136,7 +137,7 @@ func (g *HandoffGraph) buildReason(argumentsInJSON string) (string, error) {
|
||||
}
|
||||
|
||||
func (g *HandoffGraph) buildConfirmationPrompt(reason string) string {
|
||||
return fmt.Sprintf("I am ready to connect you to a human support agent.\nReason: %s\nPlease reply with \"Confirm\" or \"Cancel\".", strings.TrimSpace(reason))
|
||||
return i18nx.Getf(i18nx.DefaultLocale, "graph.handoffConfirmPrompt", strings.TrimSpace(reason))
|
||||
}
|
||||
|
||||
func parseHandoffDecision(value string) ConfirmationDecision {
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package graphs
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
)
|
||||
|
||||
const (
|
||||
InterruptTypeTicketCreationConfirmation = "ticket_creation_confirmation"
|
||||
InterruptTypeHandoffConfirmation = "handoff_confirmation"
|
||||
ConfirmOrCancelPrompt = `Please reply with "Confirm" or "Cancel".`
|
||||
NeedExplicitConfirmationPrompt = `I need your explicit confirmation. Please reply with "Confirm" or "Cancel".`
|
||||
ConfirmationExpiredReply = "This confirmation has expired. Please start again."
|
||||
CancelCreateTicketReply = "Ticket creation has been cancelled."
|
||||
CancelHandoffReply = "Human handoff has been cancelled."
|
||||
)
|
||||
|
||||
var (
|
||||
ConfirmOrCancelPrompt = i18nx.Get("graph.confirmOrCancel")
|
||||
NeedExplicitConfirmationPrompt = i18nx.Get("graph.needExplicitConfirmation")
|
||||
ConfirmationExpiredReply = i18nx.Get("graph.confirmationExpired")
|
||||
CancelCreateTicketReply = i18nx.Get("graph.cancelCreateTicket")
|
||||
CancelHandoffReply = i18nx.Get("graph.cancelHandoff")
|
||||
)
|
||||
|
||||
type ConfirmationDecision string
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
applicationruntime "agent-desk/internal/ai/application/runtime"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
svc "agent-desk/internal/services"
|
||||
)
|
||||
|
||||
@@ -39,7 +40,7 @@ func buildConversationInterrupt(conversation models.Conversation, message models
|
||||
|
||||
func resolveInterruptPrompt(summary *applicationruntime.Summary) string {
|
||||
if summary == nil || len(summary.Interrupts) == 0 {
|
||||
return "Please provide more information and try again."
|
||||
return i18nx.Get("conversation.interrupt.defaultPrompt")
|
||||
}
|
||||
if prompt := extractInterruptMessage(summary.Interrupts[0].InfoPreview); prompt != "" {
|
||||
return prompt
|
||||
@@ -47,7 +48,7 @@ func resolveInterruptPrompt(summary *applicationruntime.Summary) string {
|
||||
if prompt := strings.TrimSpace(summary.Interrupts[0].InfoPreview); prompt != "" {
|
||||
return prompt
|
||||
}
|
||||
return "Please provide more information and try again."
|
||||
return i18nx.Get("conversation.interrupt.defaultPrompt")
|
||||
}
|
||||
|
||||
func extractInterruptMessage(infoPreview string) string {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"agent-desk/internal/ai/runtime/graphs"
|
||||
"agent-desk/internal/ai/runtime/registry"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/toolx"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
@@ -48,7 +49,7 @@ func (t *AnalyzeConversationTool) Build(ctx registry.Context) (einotool.BaseTool
|
||||
func (t *AnalyzeConversationTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: toolx.GraphAnalyzeConversation.Name,
|
||||
Desc: "Graph Tool. Summarizes the current conversation, identifies complaint/payment/sentiment risk signals, and recommends whether to continue answering, create a ticket, or hand off to a human.",
|
||||
Desc: i18nx.Get("tool.graph.analyzeConversation.info"),
|
||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||
Version: einojsonschema.Version,
|
||||
Type: "object",
|
||||
@@ -57,42 +58,42 @@ func (t *AnalyzeConversationTool) Info(ctx context.Context) (*schema.ToolInfo, e
|
||||
Key: "goal",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Analysis goal, such as whether to hand off to a human, create a ticket, or perform risk review.",
|
||||
Description: i18nx.Get("tool.graph.analyzeConversation.param.goal"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "observedIssue",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Main issue or request observed in the conversation.",
|
||||
Description: i18nx.Get("tool.graph.analyzeConversation.param.observedIssue"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needTicket",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: "Whether to focus on evaluating ticket creation.",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.needTicket"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needHumanHandoff",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: "Whether to focus on evaluating human handoff.",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.needHumanHandoff"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needQualityCheck",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: "Whether to focus on risk or quality review.",
|
||||
Description: i18nx.Get("tool.graph.analyzeConversation.param.needQualityCheck"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "additionalContext",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Additional context, such as disputes, complaint points, or business constraints already identified.",
|
||||
Description: i18nx.Get("tool.graph.analyzeConversation.param.additionalContext"),
|
||||
},
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"agent-desk/internal/ai/runtime/graphs"
|
||||
"agent-desk/internal/ai/runtime/registry"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/toolx"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
@@ -52,7 +53,7 @@ func (t *CreateTicketGraphTool) Build(ctx registry.Context) (einotool.BaseTool,
|
||||
func (t *CreateTicketGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: toolx.GraphCreateTicketConfirm.Name,
|
||||
Desc: "Graph Tool. Handles ticket parameter preparation, user confirmation, actual ticket creation, and result return. Use only when the user explicitly asks to create a ticket and the title and description are clear.",
|
||||
Desc: i18nx.Get("tool.graph.createTicketConfirm.info"),
|
||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||
Version: einojsonschema.Version,
|
||||
Type: "object",
|
||||
@@ -65,14 +66,14 @@ func (t *CreateTicketGraphTool) Info(ctx context.Context) (*schema.ToolInfo, err
|
||||
Key: "title",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Ticket title. Concisely summarizes the issue.",
|
||||
Description: i18nx.Get("tool.graph.createTicketConfirm.param.title"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "description",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Ticket description. Clearly captures the user's issue, symptoms, and request.",
|
||||
Description: i18nx.Get("tool.graph.createTicketConfirm.param.description"),
|
||||
},
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"agent-desk/internal/ai/runtime/graphs"
|
||||
"agent-desk/internal/ai/runtime/registry"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/toolx"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
@@ -52,7 +53,7 @@ func (t *HandoffGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error
|
||||
func (t *HandoffGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: toolx.GraphHandoffConversation.Name,
|
||||
Desc: "Graph Tool. Handles handoff reason preparation, user confirmation, actual human handoff, and result return. Use only when the user explicitly asks for a human agent or you have confirmed that human handling is required. Do not repeat the call when the result has terminal=true and shouldRetry=false.",
|
||||
Desc: i18nx.Get("tool.graph.handoffConversation.info"),
|
||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||
Version: einojsonschema.Version,
|
||||
Type: "object",
|
||||
@@ -61,7 +62,7 @@ func (t *HandoffGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
Key: "reason",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Handoff reason. Briefly explain why a human is needed, such as explicit user request, manual verification, or after-sales handling.",
|
||||
Description: i18nx.Get("tool.graph.handoffConversation.param.reason"),
|
||||
},
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"agent-desk/internal/ai/runtime/graphs"
|
||||
"agent-desk/internal/ai/runtime/registry"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/toolx"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
@@ -48,7 +49,7 @@ func (t *PrepareTicketDraftTool) Build(ctx registry.Context) (einotool.BaseTool,
|
||||
func (t *PrepareTicketDraftTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: toolx.GraphPrepareTicketDraft.Name,
|
||||
Desc: "Graph Tool. Prepares a ticket draft from the current conversation and collected information. Use it before create_ticket_with_confirmation when the ticket content needs to be organized.",
|
||||
Desc: i18nx.Get("tool.graph.prepareTicketDraft.info"),
|
||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||
Version: einojsonschema.Version,
|
||||
Type: "object",
|
||||
@@ -57,42 +58,42 @@ func (t *PrepareTicketDraftTool) Info(ctx context.Context) (*schema.ToolInfo, er
|
||||
Key: "title",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Prepared ticket title. Optional.",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.title"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "description",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Prepared ticket description. Optional.",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.description"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "issue",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "The issue or error message the user is experiencing.",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.issue"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "impact",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Impact scope, such as unable to sign in, unable to place an order, or business interruption.",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.impact"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "expectedOutcome",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "The user's expected outcome or request.",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.expectedOutcome"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "currentAttempt",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "当前已尝试过的处理步骤,可选。",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.param.currentAttempt"),
|
||||
},
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"agent-desk/internal/ai/runtime/graphs"
|
||||
"agent-desk/internal/ai/runtime/registry"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/toolx"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
@@ -48,7 +49,7 @@ func (t *TriageServiceRequestTool) Build(ctx registry.Context) (einotool.BaseToo
|
||||
func (t *TriageServiceRequestTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: toolx.GraphTriageServiceRequest.Name,
|
||||
Desc: "Graph Tool. Analyzes the current conversation to decide whether to continue answering, prepare a ticket draft, or hand off to a human. When ticket creation is recommended, it returns a structured ticket draft suggestion.",
|
||||
Desc: i18nx.Get("tool.graph.triageServiceRequest.info"),
|
||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||
Version: einojsonschema.Version,
|
||||
Type: "object",
|
||||
@@ -57,35 +58,35 @@ func (t *TriageServiceRequestTool) Info(ctx context.Context) (*schema.ToolInfo,
|
||||
Key: "goal",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Analysis goal, such as whether to escalate, create a ticket, or hand off to a human.",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.goal"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "observedIssue",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Main issue or dispute observed in the conversation.",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.observedIssue"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needTicket",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: "Whether to focus on evaluating ticket creation.",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.needTicket"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "needHumanHandoff",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "boolean",
|
||||
Description: "Whether to focus on evaluating human handoff.",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.needHumanHandoff"),
|
||||
},
|
||||
},
|
||||
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||
Key: "additionalContext",
|
||||
Value: &einojsonschema.Schema{
|
||||
Type: "string",
|
||||
Description: "Additional context, such as risk signals or constraints already identified.",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.param.additionalContext"),
|
||||
},
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/utils"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
@@ -74,11 +73,11 @@ func localizeTicketAssignedNotification(title string, content string) (string, s
|
||||
return localizeNotificationTitle(title), content
|
||||
}
|
||||
if matches := ticketAssignedNotificationPattern.FindStringSubmatch(lines[0]); len(matches) == 2 {
|
||||
lines[0] = fmt.Sprintf("Ticket %s has been assigned to you.", matches[1])
|
||||
lines[0] = i18nx.Getf(i18nx.LocaleEnUS, "notification.ticketAssigned.line", matches[1])
|
||||
}
|
||||
for i, line := range lines[1:] {
|
||||
if reason, ok := strings.CutPrefix(line, "指派原因: "); ok {
|
||||
lines[i+1] = "Assignment reason: " + reason
|
||||
lines[i+1] = i18nx.Getf(i18nx.LocaleEnUS, "notification.ticketAssigned.reason", reason)
|
||||
}
|
||||
}
|
||||
return localizeNotificationTitle(title), strings.Join(lines, "\n")
|
||||
@@ -90,15 +89,15 @@ func localizeConversationAssignedNotification(title string, content string) (str
|
||||
return localizeNotificationTitle(title), content
|
||||
}
|
||||
if matches := conversationAssignedNotificationPattern.FindStringSubmatch(lines[0]); len(matches) == 2 {
|
||||
lines[0] = fmt.Sprintf("Conversation #%s has been assigned to you.", matches[1])
|
||||
lines[0] = i18nx.Getf(i18nx.LocaleEnUS, "notification.conversationAssigned.line", matches[1])
|
||||
}
|
||||
for i, line := range lines[1:] {
|
||||
if reason, ok := strings.CutPrefix(line, "分配原因: "); ok {
|
||||
lines[i+1] = "Assignment reason: " + reason
|
||||
lines[i+1] = i18nx.Getf(i18nx.LocaleEnUS, "notification.conversationAssigned.reason", reason)
|
||||
continue
|
||||
}
|
||||
if reason, ok := strings.CutPrefix(line, "转接原因: "); ok {
|
||||
lines[i+1] = "Transfer reason: " + reason
|
||||
lines[i+1] = i18nx.Getf(i18nx.LocaleEnUS, "notification.conversationTransferred.reason", reason)
|
||||
}
|
||||
}
|
||||
return localizeNotificationTitle(title), strings.Join(lines, "\n")
|
||||
@@ -107,13 +106,13 @@ func localizeConversationAssignedNotification(title string, content string) (str
|
||||
func localizeNotificationTitle(title string) string {
|
||||
switch strings.TrimSpace(title) {
|
||||
case "工单指派提醒":
|
||||
return "Ticket assigned"
|
||||
return i18nx.Getf(i18nx.LocaleEnUS, "notification.ticketAssigned.title")
|
||||
case "会话转接提醒":
|
||||
return "Conversation transferred"
|
||||
return i18nx.Getf(i18nx.LocaleEnUS, "notification.conversationTransferred.title")
|
||||
case "会话自动分配提醒":
|
||||
return "Conversation auto-assigned"
|
||||
return i18nx.Getf(i18nx.LocaleEnUS, "notification.conversationAutoAssigned.title")
|
||||
case "会话分配提醒":
|
||||
return "Conversation assigned"
|
||||
return i18nx.Getf(i18nx.LocaleEnUS, "notification.conversationAssigned.title")
|
||||
default:
|
||||
return title
|
||||
}
|
||||
|
||||
@@ -389,3 +389,117 @@ error.agentTeamSchedule.startTimeInvalid: "Invalid start time format."
|
||||
error.agentTeamSchedule.startTimeInvalidWithFormat: "Invalid start time format. Use HH:mm or HH:mm:ss."
|
||||
error.agentTeamSchedule.endTimeInvalid: "Invalid end time format."
|
||||
error.agentTeamSchedule.endTimeInvalidWithFormat: "Invalid end time format. Use HH:mm or HH:mm:ss."
|
||||
tool.builtin.toolSearch.title: "Search and Run Dynamic Tools"
|
||||
tool.builtin.toolSearch.description: "Searches the MCP tools currently available to the agent and runs the selected tool after its toolCode is confirmed. Best for long-tail tools; it should not replace fixed built-in workflow tools."
|
||||
tool.builtin.toolSearch.appendix: |-
|
||||
When you need long-tail MCP capabilities, prefer the tool_search tool and follow these rules:
|
||||
1. Call tool_search first to find the dynamic tool you need, then continue with the selected real tool.
|
||||
2. Do not assume all long-tail tools are visible at the start; only tools selected by tool_search are exposed to subsequent model calls.
|
||||
3. If a fixed built-in tool can already complete the task, prefer the fixed tool and do not overuse tool_search.
|
||||
tool.builtin.skill.title: "Load Skill Instructions"
|
||||
tool.builtin.skill.description: "Loads specialized skill instructions for the current agent when extra task-specific guidance is needed."
|
||||
tool.graph.triageServiceRequest.title: "Route Service Request"
|
||||
tool.graph.triageServiceRequest.description: "Analyzes the current conversation to decide whether to keep answering, prepare a ticket draft, or hand off to a human, including a ticket draft when ticket creation is appropriate."
|
||||
tool.graph.triageServiceRequest.appendix: |-
|
||||
When you need to decide between continuing the answer, creating a ticket, or handing off to a human, call triage_service_request first and follow these rules:
|
||||
1. The tool returns recommendedAction and includes ticketDraft when ticket creation is needed.
|
||||
2. If recommendedAction=continue_answering, continue clarifying or answering instead of escalating directly.
|
||||
3. If recommendedAction=prepare_ticket, use ticketDraft or collect missing fields before calling create_ticket_with_confirmation.
|
||||
4. If recommendedAction=handoff_to_human, confirm the reason is sufficient before calling handoff_to_human.
|
||||
5. When the escalation path is unclear, use this tool instead of making a complex routing decision from the main prompt alone.
|
||||
tool.graph.analyzeConversation.title: "Analyze Conversation Risk and Summary"
|
||||
tool.graph.analyzeConversation.description: "Summarizes the current conversation, identifies risk signals, and recommends whether to keep answering, create a ticket, or hand off to a human."
|
||||
tool.graph.analyzeConversation.appendix: |-
|
||||
When the conversation may involve escalation, refunds, compensation, clear negative sentiment, ticket creation, or human handoff, call analyze_conversation first and follow these rules:
|
||||
1. This tool returns a structured summary, risk signals, and next-step recommendation. It does not create tickets or hand off to a human.
|
||||
2. If the tool recommends handoff_to_human, confirm the handoff conditions before calling handoff_to_human.
|
||||
3. If the tool recommends prepare_ticket, call prepare_ticket_draft or collect more information before creating a ticket.
|
||||
4. If the tool recommends continue_answering, continue clarifying and answering instead of escalating too early.
|
||||
tool.graph.prepareTicketDraft.title: "Prepare Ticket Draft"
|
||||
tool.graph.prepareTicketDraft.description: "Turns the current conversation and collected details into a ticket draft with a suggested title, description, missing fields, and follow-up questions."
|
||||
tool.graph.prepareTicketDraft.appendix: |-
|
||||
When the user has asked to create a ticket, file a complaint, report an issue, or request after-sales handling, but the title or description is still unclear, call prepare_ticket_draft first and follow these rules:
|
||||
1. This tool prepares a ticket draft and returns a suggested title, suggested description, missing fields, and follow-up questions.
|
||||
2. If ready=false, ask follow-up questions based on missingFields and followUpQuestions instead of creating a ticket directly.
|
||||
3. If ready=true, use the result to consider calling create_ticket_with_confirmation.
|
||||
4. This tool only prepares a draft. It does not create a ticket.
|
||||
tool.graph.createTicketConfirm.title: "Create Ticket With Confirmation"
|
||||
tool.graph.createTicketConfirm.description: "Guides ticket creation with parameter preparation, customer confirmation, actual ticket creation, and final result delivery."
|
||||
tool.graph.createTicketConfirm.appendix: |-
|
||||
You can call create_ticket_with_confirmation after enough information has been collected, but follow these rules:
|
||||
1. Only consider this tool when the user explicitly wants to submit a ticket, complaint, issue report, or after-sales request.
|
||||
2. Before calling it, prepare a clear ticket title and issue description. If the information is still scattered, call prepare_ticket_draft or ask follow-up questions first.
|
||||
3. Once you are ready to create a ticket, you must call create_ticket_with_confirmation. Do not simply claim in text that the ticket has been created.
|
||||
4. This Graph Tool asks the user for confirmation first. The ticket is created only after the user confirms; if the user cancels, the flow ends.
|
||||
5. If the user is only asking questions, complaining generally, or expressing dissatisfaction without explicitly requesting a ticket, continue clarifying instead of proactively creating one.
|
||||
tool.graph.handoffConversation.title: "Handoff to Human With Confirmation"
|
||||
tool.graph.handoffConversation.description: "Guides human handoff with reason preparation, customer confirmation, actual transfer, and final result delivery."
|
||||
tool.graph.handoffConversation.appendix: |-
|
||||
You can call handoff_to_human after confirming that human help is needed, but follow these rules:
|
||||
1. Only call this tool when the user explicitly asks for a human agent or you have determined that the issue must be handled by a human.
|
||||
2. Before calling it, summarize the handoff reason clearly. If the reason is vague, ask a follow-up question first.
|
||||
3. Once you decide to hand off, you must call handoff_to_human. Do not simply say in text that you have connected the user to a human.
|
||||
4. This Graph Tool asks the user for confirmation first. The handoff happens only after the user confirms; if the user cancels, the flow ends.
|
||||
5. If the issue can still be solved in the current conversation, continue helping instead of escalating too early.
|
||||
6. If the tool returns terminal=true and shouldRetry=false, the handoff flow has ended. Do not call it repeatedly.
|
||||
conversation.handoff.waiting: "We are connecting you to a human support agent. Please wait."
|
||||
conversation.handoff.offHours: "Human support is currently outside service hours. You can keep describing the issue and I will do my best to help. You can also request a human agent again when service hours resume."
|
||||
notification.ticketAssigned.title: "Ticket assigned"
|
||||
notification.ticketAssigned.line: "Ticket %s has been assigned to you."
|
||||
notification.ticketAssigned.reason: "Assignment reason: %s"
|
||||
notification.conversationTransferred.title: "Conversation transferred"
|
||||
notification.conversationAutoAssigned.title: "Conversation auto-assigned"
|
||||
notification.conversationAssigned.title: "Conversation assigned"
|
||||
notification.conversationAssigned.line: "Conversation #%s has been assigned to you."
|
||||
notification.conversationAssigned.reason: "Assignment reason: %s"
|
||||
notification.conversationTransferred.reason: "Transfer reason: %s"
|
||||
notification.ticketAssigned.wxwork.no: "Ticket no: %s"
|
||||
notification.ticketAssigned.wxwork.title: "Title: %s"
|
||||
notification.ticketAssigned.wxwork.status: "Status: %s"
|
||||
notification.assignee: "Assignee: %s"
|
||||
notification.conversationAssigned.wxwork.id: "Conversation ID: #%d"
|
||||
notification.conversationAssigned.wxwork.summary: "Summary: %s"
|
||||
notification.conversationAssigned.wxwork.channel: "Channel: %s"
|
||||
notification.conversationAssigned.wxwork.status: "Status: %s"
|
||||
notification.time: "Time: %s"
|
||||
graph.confirmOrCancel: "Please reply with \"Confirm\" or \"Cancel\"."
|
||||
graph.needExplicitConfirmation: "I need your explicit confirmation. Please reply with \"Confirm\" or \"Cancel\"."
|
||||
graph.confirmationExpired: "This confirmation has expired. Please start again."
|
||||
graph.cancelCreateTicket: "Ticket creation has been cancelled."
|
||||
graph.cancelHandoff: "Human handoff has been cancelled."
|
||||
graph.ticketCreated: "Ticket created. Ticket no: %s. Title: %s."
|
||||
graph.createTicketConfirmPrompt: |-
|
||||
I am ready to create a ticket for you.
|
||||
Title: %s
|
||||
Description: %s
|
||||
Please reply with "Confirm" or "Cancel".
|
||||
graph.defaultHandoffReason: "The user needs human support."
|
||||
graph.handoffConfirmPrompt: |-
|
||||
I am ready to connect you to a human support agent.
|
||||
Reason: %s
|
||||
Please reply with "Confirm" or "Cancel".
|
||||
conversation.interrupt.defaultPrompt: "Please provide more information and try again."
|
||||
ticket.defaultConversationTitle: "Conversation ticket"
|
||||
tool.graph.createTicketConfirm.info: "Graph Tool. Handles ticket parameter preparation, user confirmation, actual ticket creation, and result return. Use only when the user explicitly asks to create a ticket and the title and description are clear."
|
||||
tool.graph.createTicketConfirm.param.title: "Ticket title. Concisely summarizes the issue."
|
||||
tool.graph.createTicketConfirm.param.description: "Ticket description. Clearly captures the user's issue, symptoms, and request."
|
||||
tool.graph.handoffConversation.info: "Graph Tool. Handles handoff reason preparation, user confirmation, actual human handoff, and result return. Use only when the user explicitly asks for a human agent or you have confirmed that human handling is required. Do not repeat the call when the result has terminal=true and shouldRetry=false."
|
||||
tool.graph.handoffConversation.param.reason: "Handoff reason. Briefly explain why a human is needed, such as explicit user request, manual verification, or after-sales handling."
|
||||
tool.graph.triageServiceRequest.info: "Graph Tool. Analyzes the current conversation to decide whether to continue answering, prepare a ticket draft, or hand off to a human. When ticket creation is recommended, it returns a structured ticket draft suggestion."
|
||||
tool.graph.triageServiceRequest.param.goal: "Analysis goal, such as whether to escalate, create a ticket, or hand off to a human."
|
||||
tool.graph.triageServiceRequest.param.observedIssue: "Main issue or dispute observed in the conversation."
|
||||
tool.graph.triageServiceRequest.param.needTicket: "Whether to focus on evaluating ticket creation."
|
||||
tool.graph.triageServiceRequest.param.needHumanHandoff: "Whether to focus on evaluating human handoff."
|
||||
tool.graph.triageServiceRequest.param.additionalContext: "Additional context, such as risk signals or constraints already identified."
|
||||
tool.graph.analyzeConversation.info: "Graph Tool. Summarizes the current conversation, identifies complaint/payment/sentiment risk signals, and recommends whether to continue answering, create a ticket, or hand off to a human."
|
||||
tool.graph.analyzeConversation.param.goal: "Analysis goal, such as whether to hand off to a human, create a ticket, or perform risk review."
|
||||
tool.graph.analyzeConversation.param.observedIssue: "Main issue or request observed in the conversation."
|
||||
tool.graph.analyzeConversation.param.needQualityCheck: "Whether to focus on risk or quality review."
|
||||
tool.graph.analyzeConversation.param.additionalContext: "Additional context, such as disputes, complaint points, or business constraints already identified."
|
||||
tool.graph.prepareTicketDraft.info: "Graph Tool. Prepares a ticket draft from the current conversation and collected information. Use it before create_ticket_with_confirmation when the ticket content needs to be organized."
|
||||
tool.graph.prepareTicketDraft.param.title: "Prepared ticket title. Optional."
|
||||
tool.graph.prepareTicketDraft.param.description: "Prepared ticket description. Optional."
|
||||
tool.graph.prepareTicketDraft.param.issue: "The issue or error message the user is experiencing."
|
||||
tool.graph.prepareTicketDraft.param.impact: "Impact scope, such as unable to sign in, unable to place an order, or business interruption."
|
||||
tool.graph.prepareTicketDraft.param.expectedOutcome: "The user's expected outcome or request."
|
||||
tool.graph.prepareTicketDraft.param.currentAttempt: "Current attempted troubleshooting steps. Optional."
|
||||
|
||||
@@ -389,3 +389,117 @@ error.agentTeamSchedule.startTimeInvalid: "开始时间格式错误"
|
||||
error.agentTeamSchedule.startTimeInvalidWithFormat: "开始时间格式错误,请使用 HH:mm 或 HH:mm:ss"
|
||||
error.agentTeamSchedule.endTimeInvalid: "结束时间格式错误"
|
||||
error.agentTeamSchedule.endTimeInvalidWithFormat: "结束时间格式错误,请使用 HH:mm 或 HH:mm:ss"
|
||||
tool.builtin.toolSearch.title: "搜索并调用动态工具"
|
||||
tool.builtin.toolSearch.description: "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。"
|
||||
tool.builtin.toolSearch.appendix: |-
|
||||
当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则:
|
||||
1. 先调用 tool_search 搜索需要的动态工具,再继续使用已选中的真实工具。
|
||||
2. 不要假设所有长尾工具一开始就可见;只有被 tool_search 选中的工具,后续模型调用才会暴露出来。
|
||||
3. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。
|
||||
tool.builtin.skill.title: "加载专项技能说明"
|
||||
tool.builtin.skill.description: "用于按需加载当前 Agent 可用的专项技能说明文档,适合在需要专项处理规则时再注入上下文。"
|
||||
tool.graph.triageServiceRequest.title: "服务请求分流"
|
||||
tool.graph.triageServiceRequest.description: "Graph Tool。分析当前会话,判断应继续回答、准备工单草稿还是转人工;当适合创建工单时,也会准备工单草稿。"
|
||||
tool.graph.triageServiceRequest.appendix: |-
|
||||
当你需要在继续回答、创建工单、转人工之间做判断时,优先调用 triage_service_request,并遵守以下规则:
|
||||
1. 工具会返回 recommendedAction,并在需要创建工单时包含 ticketDraft。
|
||||
2. 如果 recommendedAction=continue_answering,继续澄清或回答,不要直接升级。
|
||||
3. 如果 recommendedAction=prepare_ticket,使用 ticketDraft 或补充缺失字段后,再考虑调用 create_ticket_with_confirmation。
|
||||
4. 如果 recommendedAction=handoff_to_human,先确认转人工原因足够明确,再调用 handoff_to_human。
|
||||
5. 当升级路径不明确时,使用此工具,而不是只依赖主提示词做复杂路由判断。
|
||||
tool.graph.analyzeConversation.title: "分析会话风险和摘要"
|
||||
tool.graph.analyzeConversation.description: "Graph Tool。总结当前会话,识别风险信号,并建议继续回答、创建工单或转人工。"
|
||||
tool.graph.analyzeConversation.appendix: |-
|
||||
当会话可能涉及升级、退款、赔偿、明显负面情绪、创建工单或转人工时,优先调用 analyze_conversation,并遵守以下规则:
|
||||
1. 此工具返回结构化摘要、风险信号和下一步建议;它不会创建工单或执行转人工。
|
||||
2. 如果工具建议 handoff_to_human,先确认转人工条件,再调用 handoff_to_human。
|
||||
3. 如果工具建议 prepare_ticket,调用 prepare_ticket_draft 或继续收集信息后再创建工单。
|
||||
4. 如果工具建议 continue_answering,继续澄清和回答,不要过早升级。
|
||||
tool.graph.prepareTicketDraft.title: "准备工单草稿"
|
||||
tool.graph.prepareTicketDraft.description: "Graph Tool。基于当前会话和已收集信息准备工单草稿,包括建议标题、描述、缺失字段和追问问题。"
|
||||
tool.graph.prepareTicketDraft.appendix: |-
|
||||
当用户要求创建工单、投诉、报障或售后处理,但标题或描述仍不清晰时,优先调用 prepare_ticket_draft,并遵守以下规则:
|
||||
1. 此工具会准备工单草稿,并返回建议标题、建议描述、缺失字段和追问问题。
|
||||
2. 如果 ready=false,根据 missingFields 和 followUpQuestions 继续追问,不要直接创建工单。
|
||||
3. 如果 ready=true,使用结果考虑调用 create_ticket_with_confirmation。
|
||||
4. 此工具只准备草稿,不会创建工单。
|
||||
tool.graph.createTicketConfirm.title: "创建工单确认流程"
|
||||
tool.graph.createTicketConfirm.description: "Graph Tool。处理工单参数准备、用户确认、实际创建工单和结果返回。"
|
||||
tool.graph.createTicketConfirm.appendix: |-
|
||||
收集到足够信息后,可以调用 create_ticket_with_confirmation,但必须遵守以下规则:
|
||||
1. 只有当用户明确想提交工单、投诉、问题报告或售后请求时,才考虑使用此工具。
|
||||
2. 调用前准备清晰的工单标题和问题描述;如果信息仍然分散,先调用 prepare_ticket_draft 或继续追问。
|
||||
3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation,不要只用文本声称工单已创建。
|
||||
4. 此 Graph Tool 会先请求用户确认;只有用户确认后才会创建工单,用户取消则流程结束。
|
||||
5. 如果用户只是提问、泛泛抱怨或表达不满但没有明确要求创建工单,应继续澄清,而不是主动创建。
|
||||
tool.graph.handoffConversation.title: "转人工确认流程"
|
||||
tool.graph.handoffConversation.description: "Graph Tool。处理转人工原因准备、用户确认、实际转人工和结果返回。"
|
||||
tool.graph.handoffConversation.appendix: |-
|
||||
确认需要人工帮助后,可以调用 handoff_to_human,但必须遵守以下规则:
|
||||
1. 只有当用户明确要求人工客服,或你判断该问题必须由人工处理时才调用此工具。
|
||||
2. 调用前清晰总结转人工原因;如果原因模糊,先追问。
|
||||
3. 一旦决定转人工,必须调用 handoff_to_human,不要只用文本声称已接入人工。
|
||||
4. 此 Graph Tool 会先请求用户确认;只有用户确认后才会转人工,用户取消则流程结束。
|
||||
5. 如果问题仍可在当前会话解决,继续帮助用户,不要过早升级。
|
||||
6. 如果工具返回 terminal=true 且 shouldRetry=false,说明转人工流程已结束,不要重复调用。
|
||||
conversation.handoff.waiting: "正在为你接入人工客服,请稍候。"
|
||||
conversation.handoff.offHours: "当前不在人工客服服务时间内。你可以继续描述问题,我会尽力协助;也可以在服务时间恢复后再次申请人工客服。"
|
||||
notification.ticketAssigned.title: "工单指派提醒"
|
||||
notification.ticketAssigned.line: "工单 %s 已指派给你"
|
||||
notification.ticketAssigned.reason: "指派原因: %s"
|
||||
notification.conversationTransferred.title: "会话转接提醒"
|
||||
notification.conversationAutoAssigned.title: "会话自动分配提醒"
|
||||
notification.conversationAssigned.title: "会话分配提醒"
|
||||
notification.conversationAssigned.line: "会话 #%s 已分配给你"
|
||||
notification.conversationAssigned.reason: "分配原因: %s"
|
||||
notification.conversationTransferred.reason: "转接原因: %s"
|
||||
notification.ticketAssigned.wxwork.no: "工单号: %s"
|
||||
notification.ticketAssigned.wxwork.title: "工单标题: %s"
|
||||
notification.ticketAssigned.wxwork.status: "当前状态: %s"
|
||||
notification.assignee: "处理人: %s"
|
||||
notification.conversationAssigned.wxwork.id: "会话ID: #%d"
|
||||
notification.conversationAssigned.wxwork.summary: "摘要: %s"
|
||||
notification.conversationAssigned.wxwork.channel: "渠道: %s"
|
||||
notification.conversationAssigned.wxwork.status: "状态: %s"
|
||||
notification.time: "时间: %s"
|
||||
graph.confirmOrCancel: "请回复“确认”或“取消”。"
|
||||
graph.needExplicitConfirmation: "需要你明确确认。请回复“确认”或“取消”。"
|
||||
graph.confirmationExpired: "本次确认已过期,请重新发起。"
|
||||
graph.cancelCreateTicket: "已取消本次工单创建。"
|
||||
graph.cancelHandoff: "已取消本次转人工。"
|
||||
graph.ticketCreated: "工单已创建。工单号:%s。标题:%s。"
|
||||
graph.createTicketConfirmPrompt: |-
|
||||
我已准备好为你创建工单。
|
||||
标题:%s
|
||||
描述:%s
|
||||
请回复“确认”或“取消”。
|
||||
graph.defaultHandoffReason: "用户需要人工客服支持。"
|
||||
graph.handoffConfirmPrompt: |-
|
||||
我已准备好为你接入人工客服。
|
||||
原因:%s
|
||||
请回复“确认”或“取消”。
|
||||
conversation.interrupt.defaultPrompt: "请补充更多信息后再试。"
|
||||
ticket.defaultConversationTitle: "会话工单"
|
||||
tool.graph.createTicketConfirm.info: "Graph Tool。处理工单参数准备、用户确认、实际创建工单和结果返回。仅当用户明确要求创建工单且标题、描述清晰时使用。"
|
||||
tool.graph.createTicketConfirm.param.title: "工单标题。简洁概括问题。"
|
||||
tool.graph.createTicketConfirm.param.description: "工单描述。清晰记录用户的问题、现象和诉求。"
|
||||
tool.graph.handoffConversation.info: "Graph Tool。处理转人工原因准备、用户确认、实际转人工和结果返回。仅当用户明确要求人工客服,或你确认需要人工处理时使用。当结果 terminal=true 且 shouldRetry=false 时不要重复调用。"
|
||||
tool.graph.handoffConversation.param.reason: "转人工原因。简要说明为什么需要人工,例如用户明确要求、需要人工核验或售后处理。"
|
||||
tool.graph.triageServiceRequest.info: "Graph Tool。分析当前会话,判断应继续回答、准备工单草稿还是转人工。建议创建工单时,会返回结构化工单草稿建议。"
|
||||
tool.graph.triageServiceRequest.param.goal: "分析目标,例如是否升级、创建工单或转人工。"
|
||||
tool.graph.triageServiceRequest.param.observedIssue: "当前会话中观察到的主要问题或争议。"
|
||||
tool.graph.triageServiceRequest.param.needTicket: "是否重点评估创建工单。"
|
||||
tool.graph.triageServiceRequest.param.needHumanHandoff: "是否重点评估转人工。"
|
||||
tool.graph.triageServiceRequest.param.additionalContext: "补充上下文,例如已经识别的风险信号或约束。"
|
||||
tool.graph.analyzeConversation.info: "Graph Tool。总结当前会话,识别投诉、支付、情绪等风险信号,并建议继续回答、创建工单或转人工。"
|
||||
tool.graph.analyzeConversation.param.goal: "分析目标,例如是否转人工、创建工单或执行风险复核。"
|
||||
tool.graph.analyzeConversation.param.observedIssue: "当前会话中观察到的主要问题或请求。"
|
||||
tool.graph.analyzeConversation.param.needQualityCheck: "是否重点进行风险或质量复核。"
|
||||
tool.graph.analyzeConversation.param.additionalContext: "补充上下文,例如争议点、投诉点或已识别的业务约束。"
|
||||
tool.graph.prepareTicketDraft.info: "Graph Tool。根据当前会话和已收集信息准备工单草稿。当工单内容需要整理时,应在 create_ticket_with_confirmation 之前使用。"
|
||||
tool.graph.prepareTicketDraft.param.title: "准备好的工单标题,可选。"
|
||||
tool.graph.prepareTicketDraft.param.description: "准备好的工单描述,可选。"
|
||||
tool.graph.prepareTicketDraft.param.issue: "用户正在遇到的问题或报错信息。"
|
||||
tool.graph.prepareTicketDraft.param.impact: "影响范围,例如无法登录、无法下单或业务中断。"
|
||||
tool.graph.prepareTicketDraft.param.expectedOutcome: "用户期望的处理结果或诉求。"
|
||||
tool.graph.prepareTicketDraft.param.currentAttempt: "当前已尝试过的处理步骤,可选。"
|
||||
|
||||
@@ -12,13 +12,16 @@ type ToolSpec struct {
|
||||
ServerCode string
|
||||
Name string
|
||||
Title string
|
||||
TitleKey string
|
||||
Description string
|
||||
DescriptionKey string
|
||||
SourceType enums.ToolSourceType
|
||||
AutoInjected bool
|
||||
DirectAccess bool
|
||||
RuntimeStatic bool
|
||||
Aliases []string
|
||||
Appendix string
|
||||
AppendixKey string
|
||||
}
|
||||
|
||||
type ToolMetadata struct {
|
||||
@@ -33,24 +36,24 @@ var (
|
||||
Code: "builtin/tool_search",
|
||||
ServerCode: "builtin",
|
||||
Name: "tool_search",
|
||||
Title: "搜索并调用动态工具",
|
||||
Description: "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。",
|
||||
Title: i18nx.Get("tool.builtin.toolSearch.title"),
|
||||
TitleKey: "tool.builtin.toolSearch.title",
|
||||
Description: i18nx.Get("tool.builtin.toolSearch.description"),
|
||||
DescriptionKey: "tool.builtin.toolSearch.description",
|
||||
SourceType: enums.ToolSourceTypeBuiltin,
|
||||
AutoInjected: true,
|
||||
DirectAccess: true,
|
||||
Appendix: strings.TrimSpace(`
|
||||
当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则:
|
||||
1. 先调用 tool_search 搜索需要的动态工具,再继续使用已选中的真实工具。
|
||||
2. 不要假设所有长尾工具一开始就可见;只有被 tool_search 选中的工具,后续模型调用才会暴露出来。
|
||||
3. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。
|
||||
`),
|
||||
Appendix: i18nx.Get("tool.builtin.toolSearch.appendix"),
|
||||
AppendixKey: "tool.builtin.toolSearch.appendix",
|
||||
}
|
||||
BuiltinSkill = ToolSpec{
|
||||
Code: "builtin/skill",
|
||||
ServerCode: "builtin",
|
||||
Name: "skill",
|
||||
Title: "加载专项技能说明",
|
||||
Description: "用于按需加载当前 Agent 可用的专项技能说明文档,适合在需要专项处理规则时再注入上下文。",
|
||||
Title: i18nx.Get("tool.builtin.skill.title"),
|
||||
TitleKey: "tool.builtin.skill.title",
|
||||
Description: i18nx.Get("tool.builtin.skill.description"),
|
||||
DescriptionKey: "tool.builtin.skill.description",
|
||||
SourceType: enums.ToolSourceTypeBuiltin,
|
||||
AutoInjected: true,
|
||||
}
|
||||
@@ -58,88 +61,69 @@ var (
|
||||
Code: "graph/triage_service_request",
|
||||
ServerCode: "graph",
|
||||
Name: "triage_service_request",
|
||||
Title: "Triage service request",
|
||||
Description: "Graph Tool. Analyzes the current conversation to decide whether to continue answering, prepare a ticket draft, or hand off to a human. It also prepares a ticket draft when ticket creation is recommended.",
|
||||
Title: i18nx.Get("tool.graph.triageServiceRequest.title"),
|
||||
TitleKey: "tool.graph.triageServiceRequest.title",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.description"),
|
||||
DescriptionKey: "tool.graph.triageServiceRequest.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
RuntimeStatic: true,
|
||||
Appendix: strings.TrimSpace(`
|
||||
When you need to decide between continuing the answer, creating a ticket, or handing off to a human, call triage_service_request first and follow these rules:
|
||||
1. The tool returns recommendedAction and includes ticketDraft when ticket creation is needed.
|
||||
2. If recommendedAction=continue_answering, continue clarifying or answering instead of escalating directly.
|
||||
3. If recommendedAction=prepare_ticket, use ticketDraft or collect missing fields before calling create_ticket_with_confirmation.
|
||||
4. If recommendedAction=handoff_to_human, confirm the reason is sufficient before calling handoff_to_human.
|
||||
5. When the escalation path is unclear, use this tool instead of making a complex routing decision from the main prompt alone.
|
||||
`),
|
||||
Appendix: i18nx.Get("tool.graph.triageServiceRequest.appendix"),
|
||||
AppendixKey: "tool.graph.triageServiceRequest.appendix",
|
||||
}
|
||||
GraphAnalyzeConversation = ToolSpec{
|
||||
Code: "graph/analyze_conversation",
|
||||
ServerCode: "graph",
|
||||
Name: "analyze_conversation",
|
||||
Title: "Analyze conversation risk and summary",
|
||||
Description: "Graph Tool. Summarizes the current conversation, identifies risk signals, and recommends whether to continue answering, create a ticket, or hand off to a human.",
|
||||
Title: i18nx.Get("tool.graph.analyzeConversation.title"),
|
||||
TitleKey: "tool.graph.analyzeConversation.title",
|
||||
Description: i18nx.Get("tool.graph.analyzeConversation.description"),
|
||||
DescriptionKey: "tool.graph.analyzeConversation.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
RuntimeStatic: true,
|
||||
Appendix: strings.TrimSpace(`
|
||||
When the conversation may involve escalation, refunds, compensation, clear negative sentiment, ticket creation, or human handoff, call analyze_conversation first and follow these rules:
|
||||
1. This tool returns a structured summary, risk signals, and next-step recommendation. It does not create tickets or hand off to a human.
|
||||
2. If the tool recommends handoff_to_human, confirm the handoff conditions before calling handoff_to_human.
|
||||
3. If the tool recommends prepare_ticket, call prepare_ticket_draft or collect more information before creating a ticket.
|
||||
4. If the tool recommends continue_answering, continue clarifying and answering instead of escalating too early.
|
||||
`),
|
||||
Appendix: i18nx.Get("tool.graph.analyzeConversation.appendix"),
|
||||
AppendixKey: "tool.graph.analyzeConversation.appendix",
|
||||
}
|
||||
GraphPrepareTicketDraft = ToolSpec{
|
||||
Code: "graph/prepare_ticket_draft",
|
||||
ServerCode: "graph",
|
||||
Name: "prepare_ticket_draft",
|
||||
Title: "Prepare ticket draft",
|
||||
Description: "Graph Tool. Prepares a ticket draft from the current conversation and collected information, including a suggested title, description, missing fields, and follow-up questions.",
|
||||
Title: i18nx.Get("tool.graph.prepareTicketDraft.title"),
|
||||
TitleKey: "tool.graph.prepareTicketDraft.title",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.description"),
|
||||
DescriptionKey: "tool.graph.prepareTicketDraft.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
RuntimeStatic: true,
|
||||
Appendix: strings.TrimSpace(`
|
||||
When the user has asked to create a ticket, file a complaint, report an issue, or request after-sales handling, but the title or description is still unclear, call prepare_ticket_draft first and follow these rules:
|
||||
1. This tool prepares a ticket draft and returns a suggested title, suggested description, missing fields, and follow-up questions.
|
||||
2. If ready=false, ask follow-up questions based on missingFields and followUpQuestions instead of creating a ticket directly.
|
||||
3. If ready=true, use the result to consider calling create_ticket_with_confirmation.
|
||||
4. This tool only prepares a draft. It does not create a ticket.
|
||||
`),
|
||||
Appendix: i18nx.Get("tool.graph.prepareTicketDraft.appendix"),
|
||||
AppendixKey: "tool.graph.prepareTicketDraft.appendix",
|
||||
}
|
||||
GraphCreateTicketConfirm = ToolSpec{
|
||||
Code: "graph/create_ticket_with_confirmation",
|
||||
ServerCode: "graph",
|
||||
Name: "create_ticket_with_confirmation",
|
||||
Title: "Create ticket confirmation flow",
|
||||
Description: "Graph Tool. Handles ticket parameter preparation, user confirmation, actual ticket creation, and result return.",
|
||||
Title: i18nx.Get("tool.graph.createTicketConfirm.title"),
|
||||
TitleKey: "tool.graph.createTicketConfirm.title",
|
||||
Description: i18nx.Get("tool.graph.createTicketConfirm.description"),
|
||||
DescriptionKey: "tool.graph.createTicketConfirm.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
DirectAccess: true,
|
||||
RuntimeStatic: true,
|
||||
Aliases: []string{"builtin/create_ticket_with_confirmation"},
|
||||
Appendix: strings.TrimSpace(`
|
||||
You can call create_ticket_with_confirmation after enough information has been collected, but follow these rules:
|
||||
1. Only consider this tool when the user explicitly wants to submit a ticket, complaint, issue report, or after-sales request.
|
||||
2. Before calling it, prepare a clear ticket title and issue description. If the information is still scattered, call prepare_ticket_draft or ask follow-up questions first.
|
||||
3. Once you are ready to create a ticket, you must call create_ticket_with_confirmation. Do not simply claim in text that the ticket has been created.
|
||||
4. This Graph Tool asks the user for confirmation first. The ticket is created only after the user confirms; if the user cancels, the flow ends.
|
||||
5. If the user is only asking questions, complaining generally, or expressing dissatisfaction without explicitly requesting a ticket, continue clarifying instead of proactively creating one.
|
||||
`),
|
||||
Appendix: i18nx.Get("tool.graph.createTicketConfirm.appendix"),
|
||||
AppendixKey: "tool.graph.createTicketConfirm.appendix",
|
||||
}
|
||||
GraphHandoffConversation = ToolSpec{
|
||||
Code: "graph/handoff_to_human",
|
||||
ServerCode: "graph",
|
||||
Name: "handoff_to_human",
|
||||
Title: "Human handoff confirmation flow",
|
||||
Description: "Graph Tool. Handles handoff reason preparation, user confirmation, actual human handoff, and result return.",
|
||||
Title: i18nx.Get("tool.graph.handoffConversation.title"),
|
||||
TitleKey: "tool.graph.handoffConversation.title",
|
||||
Description: i18nx.Get("tool.graph.handoffConversation.description"),
|
||||
DescriptionKey: "tool.graph.handoffConversation.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
DirectAccess: true,
|
||||
RuntimeStatic: true,
|
||||
Appendix: strings.TrimSpace(`
|
||||
You can call handoff_to_human after confirming that human help is needed, but follow these rules:
|
||||
1. Only call this tool when the user explicitly asks for a human agent or you have determined that the issue must be handled by a human.
|
||||
2. Before calling it, summarize the handoff reason clearly. If the reason is vague, ask a follow-up question first.
|
||||
3. Once you decide to hand off, you must call handoff_to_human. Do not simply say in text that you have connected the user to a human.
|
||||
4. This Graph Tool asks the user for confirmation first. The handoff happens only after the user confirms; if the user cancels, the flow ends.
|
||||
5. If the issue can still be solved in the current conversation, continue helping instead of escalating too early.
|
||||
6. If the tool returns terminal=true and shouldRetry=false, the handoff flow has ended. Do not call it repeatedly.
|
||||
`),
|
||||
Appendix: i18nx.Get("tool.graph.handoffConversation.appendix"),
|
||||
AppendixKey: "tool.graph.handoffConversation.appendix",
|
||||
}
|
||||
RegisteredToolSpecs = []ToolSpec{
|
||||
BuiltinToolSearch,
|
||||
@@ -224,11 +208,8 @@ func GetRegisteredToolTitleLocale(toolCode string, locale string) string {
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if i18nx.NormalizeLocale(locale) != i18nx.LocaleEnUS {
|
||||
return spec.Title
|
||||
}
|
||||
if text := registeredToolEnglishTitle(spec.Code); text != "" {
|
||||
return text
|
||||
if strings.TrimSpace(spec.TitleKey) != "" {
|
||||
return i18nx.Getf(locale, spec.TitleKey)
|
||||
}
|
||||
return spec.Title
|
||||
}
|
||||
@@ -246,55 +227,21 @@ func GetRegisteredToolDescriptionLocale(toolCode string, locale string) string {
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if i18nx.NormalizeLocale(locale) != i18nx.LocaleEnUS {
|
||||
return spec.Description
|
||||
}
|
||||
if text := registeredToolEnglishDescription(spec.Code); text != "" {
|
||||
return text
|
||||
if strings.TrimSpace(spec.DescriptionKey) != "" {
|
||||
return i18nx.Getf(locale, spec.DescriptionKey)
|
||||
}
|
||||
return spec.Description
|
||||
}
|
||||
|
||||
func registeredToolEnglishTitle(toolCode string) string {
|
||||
switch toolCode {
|
||||
case BuiltinToolSearch.Code:
|
||||
return "Search and Run Dynamic Tools"
|
||||
case BuiltinSkill.Code:
|
||||
return "Load Skill Instructions"
|
||||
case GraphTriageServiceRequest.Code:
|
||||
return "Route Service Request"
|
||||
case GraphAnalyzeConversation.Code:
|
||||
return "Analyze Conversation Risk and Summary"
|
||||
case GraphPrepareTicketDraft.Code:
|
||||
return "Prepare Ticket Draft"
|
||||
case GraphCreateTicketConfirm.Code:
|
||||
return "Create Ticket With Confirmation"
|
||||
case GraphHandoffConversation.Code:
|
||||
return "Handoff to Human With Confirmation"
|
||||
default:
|
||||
func GetRegisteredToolAppendixLocale(toolCode string, locale string) string {
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if strings.TrimSpace(spec.AppendixKey) != "" {
|
||||
return i18nx.Getf(locale, spec.AppendixKey)
|
||||
}
|
||||
|
||||
func registeredToolEnglishDescription(toolCode string) string {
|
||||
switch toolCode {
|
||||
case BuiltinToolSearch.Code:
|
||||
return "Searches the MCP tools currently available to the agent and runs the selected tool after its toolCode is confirmed. Best for long-tail tools; it should not replace fixed built-in workflow tools."
|
||||
case BuiltinSkill.Code:
|
||||
return "Loads specialized skill instructions for the current agent when extra task-specific guidance is needed."
|
||||
case GraphTriageServiceRequest.Code:
|
||||
return "Analyzes the current conversation to decide whether to keep answering, prepare a ticket draft, or hand off to a human, including a ticket draft when ticket creation is appropriate."
|
||||
case GraphAnalyzeConversation.Code:
|
||||
return "Summarizes the current conversation, identifies risk signals, and recommends whether to keep answering, create a ticket, or hand off to a human."
|
||||
case GraphPrepareTicketDraft.Code:
|
||||
return "Turns the current conversation and collected details into a ticket draft with a suggested title, description, missing fields, and follow-up questions."
|
||||
case GraphCreateTicketConfirm.Code:
|
||||
return "Guides ticket creation with parameter preparation, customer confirmation, actual ticket creation, and final result delivery."
|
||||
case GraphHandoffConversation.Code:
|
||||
return "Guides human handoff with reason preparation, customer confirmation, actual transfer, and final result delivery."
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return spec.Appendix
|
||||
}
|
||||
|
||||
func GetRegisteredToolIdentity(toolCode string) (serverCode, toolName string, ok bool) {
|
||||
@@ -372,20 +319,25 @@ func BuildToolAppendices(hasDynamicMCPTools bool, toolCodes map[string]string) [
|
||||
}
|
||||
|
||||
func BuildToolAppendicesForCodes(hasDynamicMCPTools bool, toolCodes []string) []string {
|
||||
return BuildToolAppendicesForCodesLocale(hasDynamicMCPTools, toolCodes, i18nx.DefaultLocale)
|
||||
}
|
||||
|
||||
func BuildToolAppendicesForCodesLocale(hasDynamicMCPTools bool, toolCodes []string, locale string) []string {
|
||||
ret := make([]string, 0, len(toolCodes)+1)
|
||||
normalizedToolCodes := NormalizeToolCodes(toolCodes)
|
||||
if hasDynamicMCPTools && strings.TrimSpace(BuiltinToolSearch.Appendix) != "" {
|
||||
ret = append(ret, BuiltinToolSearch.Appendix)
|
||||
if hasDynamicMCPTools {
|
||||
if appendix := strings.TrimSpace(GetRegisteredToolAppendixLocale(BuiltinToolSearch.Code, locale)); appendix != "" {
|
||||
ret = append(ret, appendix)
|
||||
}
|
||||
}
|
||||
for _, spec := range RegisteredToolSpecs {
|
||||
if strings.TrimSpace(spec.Appendix) == "" {
|
||||
continue
|
||||
}
|
||||
if spec.Code == BuiltinToolSearch.Code {
|
||||
continue
|
||||
}
|
||||
if containsNormalizedToolCode(normalizedToolCodes, spec.Code) {
|
||||
ret = append(ret, spec.Appendix)
|
||||
if appendix := strings.TrimSpace(GetRegisteredToolAppendixLocale(spec.Code, locale)); appendix != "" {
|
||||
ret = append(ret, appendix)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
|
||||
@@ -53,12 +53,12 @@ func TestRegisteredToolTextUsesEnglishLocale(t *testing.T) {
|
||||
|
||||
func TestRegisteredToolTextKeepsChineseLocale(t *testing.T) {
|
||||
title := GetRegisteredToolTitleLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleZhCN)
|
||||
if title != GraphCreateTicketConfirm.Title {
|
||||
if title != "创建工单确认流程" {
|
||||
t.Fatalf("unexpected chinese title: %q", title)
|
||||
}
|
||||
|
||||
description := GetRegisteredToolDescriptionLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleZhCN)
|
||||
if description != GraphCreateTicketConfirm.Description {
|
||||
if description != "Graph Tool。处理工单参数准备、用户确认、实际创建工单和结果返回。" {
|
||||
t.Fatalf("unexpected chinese description: %q", description)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/eventbus"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/repositories"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
@@ -18,11 +19,19 @@ import (
|
||||
|
||||
var ConversationHumanDispatchService = newConversationHumanDispatchService()
|
||||
|
||||
const (
|
||||
HandoffWaitingMessage = "We are connecting you to a human support agent. Please wait."
|
||||
HandoffOffHoursMessage = "Human support is currently outside service hours. You can keep describing the issue and I will do my best to help. You can also request a human agent again when service hours resume."
|
||||
var (
|
||||
HandoffWaitingMessage = HandoffWaitingMessageForLocale(i18nx.DefaultLocale)
|
||||
HandoffOffHoursMessage = HandoffOffHoursMessageForLocale(i18nx.DefaultLocale)
|
||||
)
|
||||
|
||||
func HandoffWaitingMessageForLocale(locale string) string {
|
||||
return i18nx.Getf(locale, "conversation.handoff.waiting")
|
||||
}
|
||||
|
||||
func HandoffOffHoursMessageForLocale(locale string) string {
|
||||
return i18nx.Getf(locale, "conversation.handoff.offHours")
|
||||
}
|
||||
|
||||
type HandoffDecisionType string
|
||||
|
||||
const (
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/eventbus"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/mlogclub/simple/common/strs"
|
||||
@@ -37,11 +38,11 @@ func handleConversationAssignedNotify(ctx context.Context, event events.Conversa
|
||||
func conversationAssignedNotifyTitle(assignType string) string {
|
||||
switch strings.TrimSpace(assignType) {
|
||||
case events.ConversationAssignTypeTransfer:
|
||||
return "Conversation transferred"
|
||||
return i18nx.Get("notification.conversationTransferred.title")
|
||||
case events.ConversationAssignTypeAutoAssign:
|
||||
return "Conversation auto-assigned"
|
||||
return i18nx.Get("notification.conversationAutoAssigned.title")
|
||||
default:
|
||||
return "Conversation assigned"
|
||||
return i18nx.Get("notification.conversationAssigned.title")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,21 +50,21 @@ func buildConversationAssignedNotifyBody(conversation *models.Conversation, assi
|
||||
if conversation == nil {
|
||||
return ""
|
||||
}
|
||||
reasonLabel := "Assignment reason"
|
||||
reasonKey := "notification.conversationAssigned.reason"
|
||||
if strings.TrimSpace(assignType) == events.ConversationAssignTypeTransfer {
|
||||
reasonLabel = "Transfer reason"
|
||||
reasonKey = "notification.conversationTransferred.reason"
|
||||
}
|
||||
lines := []string{
|
||||
fmt.Sprintf("Conversation ID: #%d", conversation.ID),
|
||||
fmt.Sprintf("Summary: %s", strs.DefaultIfBlank(services.ConversationService.BuildConversationSummary(conversation), "-")),
|
||||
fmt.Sprintf("Channel: %s", resolveConversationChannelLabel(conversation)),
|
||||
fmt.Sprintf("Status: %s", enums.GetIMConversationStatusLabel(conversation.Status)),
|
||||
fmt.Sprintf("Assignee: %s", resolveNotifyUserLabel(assigneeID)),
|
||||
i18nx.Getf(i18nx.DefaultLocale, "notification.conversationAssigned.wxwork.id", conversation.ID),
|
||||
i18nx.Getf(i18nx.DefaultLocale, "notification.conversationAssigned.wxwork.summary", strs.DefaultIfBlank(services.ConversationService.BuildConversationSummary(conversation), "-")),
|
||||
i18nx.Getf(i18nx.DefaultLocale, "notification.conversationAssigned.wxwork.channel", resolveConversationChannelLabel(conversation)),
|
||||
i18nx.Getf(i18nx.DefaultLocale, "notification.conversationAssigned.wxwork.status", enums.GetIMConversationStatusLabel(conversation.Status)),
|
||||
i18nx.Getf(i18nx.DefaultLocale, "notification.assignee", resolveNotifyUserLabel(assigneeID)),
|
||||
}
|
||||
if strings.TrimSpace(reason) != "" {
|
||||
lines = append(lines, fmt.Sprintf("%s: %s", reasonLabel, strings.TrimSpace(reason)))
|
||||
lines = append(lines, i18nx.Getf(i18nx.DefaultLocale, reasonKey, strings.TrimSpace(reason)))
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("Time: %s", time.Now().Format("2006-01-02 15:04:05")))
|
||||
lines = append(lines, i18nx.Getf(i18nx.DefaultLocale, "notification.time", time.Now().Format("2006-01-02 15:04:05")))
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"agent-desk/internal/events"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/eventbus"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/mlogclub/simple/common/strs"
|
||||
@@ -31,16 +32,16 @@ func handleTicketAssignedInAppNotification(ctx context.Context, event events.Tic
|
||||
if ticket == nil {
|
||||
return nil
|
||||
}
|
||||
content := fmt.Sprintf("Ticket %s has been assigned to you.", strs.DefaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID)))
|
||||
content := i18nx.Getf(i18nx.DefaultLocale, "notification.ticketAssigned.line", strs.DefaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID)))
|
||||
if title := strings.TrimSpace(ticket.Title); title != "" {
|
||||
content = content + "\n" + title
|
||||
}
|
||||
if reason := strings.TrimSpace(event.Reason); reason != "" {
|
||||
content = content + "\nAssignment reason: " + reason
|
||||
content = content + "\n" + i18nx.Getf(i18nx.DefaultLocale, "notification.ticketAssigned.reason", reason)
|
||||
}
|
||||
_, err := services.NotificationService.CreateAndPush(request.CreateNotificationRequest{
|
||||
RecipientUserID: event.ToUserID,
|
||||
Title: "Ticket assigned",
|
||||
Title: i18nx.Get("notification.ticketAssigned.title"),
|
||||
Content: content,
|
||||
NotificationType: "ticket_assigned",
|
||||
BizType: "ticket",
|
||||
@@ -61,12 +62,16 @@ func handleConversationAssignedInAppNotification(ctx context.Context, event even
|
||||
if conversation == nil {
|
||||
return nil
|
||||
}
|
||||
content := fmt.Sprintf("Conversation #%d has been assigned to you.", conversation.ID)
|
||||
content := i18nx.Getf(i18nx.DefaultLocale, "notification.conversationAssigned.line", conversation.ID)
|
||||
if summary := strings.TrimSpace(services.ConversationService.BuildConversationSummary(conversation)); summary != "" {
|
||||
content = content + "\n" + summary
|
||||
}
|
||||
if reason := strings.TrimSpace(event.Reason); reason != "" {
|
||||
content = content + "\nAssignment reason: " + reason
|
||||
reasonKey := "notification.conversationAssigned.reason"
|
||||
if strings.TrimSpace(event.AssignType) == events.ConversationAssignTypeTransfer {
|
||||
reasonKey = "notification.conversationTransferred.reason"
|
||||
}
|
||||
content = content + "\n" + i18nx.Getf(i18nx.DefaultLocale, reasonKey, reason)
|
||||
}
|
||||
_, err := services.NotificationService.CreateAndPush(request.CreateNotificationRequest{
|
||||
RecipientUserID: event.ToUserID,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/eventbus"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
"context"
|
||||
"fmt"
|
||||
@@ -29,7 +30,7 @@ func handleTicketAssignedNotify(ctx context.Context, event events.TicketAssigned
|
||||
return nil
|
||||
}
|
||||
content := buildTicketAssignedNotifyBody(ticket, event.ToUserID, event.Reason)
|
||||
return services.WxWorkNotifyService.SendTextToAssigneeOrDefault(event.ToUserID, "Ticket assigned", content)
|
||||
return services.WxWorkNotifyService.SendTextToAssigneeOrDefault(event.ToUserID, i18nx.Get("notification.ticketAssigned.title"), content)
|
||||
}
|
||||
|
||||
func buildTicketAssignedNotifyBody(ticket *models.Ticket, assigneeID int64, reason string) string {
|
||||
@@ -37,14 +38,14 @@ func buildTicketAssignedNotifyBody(ticket *models.Ticket, assigneeID int64, reas
|
||||
return ""
|
||||
}
|
||||
lines := []string{
|
||||
fmt.Sprintf("Ticket no: %s", strs.DefaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID))),
|
||||
fmt.Sprintf("Title: %s", strs.DefaultIfBlank(ticket.Title, "-")),
|
||||
fmt.Sprintf("Status: %s", enums.GetTicketStatusLabel(ticket.Status)),
|
||||
fmt.Sprintf("Assignee: %s", resolveNotifyUserLabel(assigneeID)),
|
||||
i18nx.Getf(i18nx.DefaultLocale, "notification.ticketAssigned.wxwork.no", strs.DefaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID))),
|
||||
i18nx.Getf(i18nx.DefaultLocale, "notification.ticketAssigned.wxwork.title", strs.DefaultIfBlank(ticket.Title, "-")),
|
||||
i18nx.Getf(i18nx.DefaultLocale, "notification.ticketAssigned.wxwork.status", enums.GetTicketStatusLabel(ticket.Status)),
|
||||
i18nx.Getf(i18nx.DefaultLocale, "notification.assignee", resolveNotifyUserLabel(assigneeID)),
|
||||
}
|
||||
if strings.TrimSpace(reason) != "" {
|
||||
lines = append(lines, fmt.Sprintf("Assignment reason: %s", strings.TrimSpace(reason)))
|
||||
lines = append(lines, i18nx.Getf(i18nx.DefaultLocale, "notification.ticketAssigned.reason", strings.TrimSpace(reason)))
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("Time: %s", time.Now().Format("2006-01-02 15:04:05")))
|
||||
lines = append(lines, i18nx.Getf(i18nx.DefaultLocale, "notification.time", time.Now().Format("2006-01-02 15:04:05")))
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/eventbus"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/utils"
|
||||
"agent-desk/internal/repositories"
|
||||
|
||||
@@ -265,7 +266,7 @@ func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConve
|
||||
title = strings.TrimSpace(ConversationService.BuildConversationSummary(conversation))
|
||||
}
|
||||
if title == "" {
|
||||
title = "Conversation ticket"
|
||||
title = i18nx.Get("ticket.defaultConversationTitle")
|
||||
}
|
||||
description := strings.TrimSpace(req.Description)
|
||||
if description == "" {
|
||||
|
||||
@@ -457,10 +457,10 @@ function DebugDialogBody({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<ResultBlock title="Skill Route Trace" value={result?.skillRouteTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title="Tool Search Trace" value={result?.toolSearchTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title="Graph Tool Trace" value={result?.graphToolTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title="Trace Data" value={result?.traceData} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title={t("skillDefinition.skillRouteTrace")} value={result?.skillRouteTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title={t("skillDefinition.toolSearchTrace")} value={result?.toolSearchTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title={t("skillDefinition.graphToolTrace")} value={result?.graphToolTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title={t("skillDefinition.traceData")} value={result?.traceData} emptyText={t("skillDefinition.emptyData")} />
|
||||
</div>
|
||||
|
||||
{result?.interrupted && result.checkPointId ? (
|
||||
@@ -555,9 +555,9 @@ function DebugDialogBody({
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ResultBlock title="Resume Tool Search Trace" value={resumeResult.toolSearchTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title="Resume Graph Tool Trace" value={resumeResult.graphToolTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title="Resume Trace Data" value={resumeResult.traceData} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title={t("skillDefinition.resumeToolSearchTrace")} value={resumeResult.toolSearchTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title={t("skillDefinition.resumeGraphToolTrace")} value={resumeResult.graphToolTrace} emptyText={t("skillDefinition.emptyData")} />
|
||||
<ResultBlock title={t("skillDefinition.resumeTraceData")} value={resumeResult.traceData} emptyText={t("skillDefinition.emptyData")} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { normalizeLocale } from "@/i18n/config"
|
||||
import { translateMessage } from "@/i18n/messages"
|
||||
|
||||
type LocalizableNotification = {
|
||||
title: string
|
||||
content: string
|
||||
@@ -19,76 +22,87 @@ export function localizeNotificationItem<T extends LocalizableNotification>(
|
||||
notification: T,
|
||||
locale: string
|
||||
): T {
|
||||
if (locale !== "en-US") {
|
||||
const normalizedLocale = normalizeLocale(locale)
|
||||
if (normalizedLocale !== "en-US") {
|
||||
return notification
|
||||
}
|
||||
if (notification.notificationType === "ticket_assigned") {
|
||||
return {
|
||||
...notification,
|
||||
title: localizeNotificationTitle(notification.title),
|
||||
content: localizeTicketAssignedContent(notification.content),
|
||||
title: localizeNotificationTitle(notification.title, normalizedLocale),
|
||||
content: localizeTicketAssignedContent(notification.content, normalizedLocale),
|
||||
}
|
||||
}
|
||||
if (notification.notificationType === "conversation_assigned") {
|
||||
return {
|
||||
...notification,
|
||||
title: localizeNotificationTitle(notification.title),
|
||||
content: localizeConversationAssignedContent(notification.content),
|
||||
title: localizeNotificationTitle(notification.title, normalizedLocale),
|
||||
content: localizeConversationAssignedContent(notification.content, normalizedLocale),
|
||||
}
|
||||
}
|
||||
return notification
|
||||
}
|
||||
|
||||
function localizeTicketAssignedContent(content: string) {
|
||||
function localizeTicketAssignedContent(content: string, locale: ReturnType<typeof normalizeLocale>) {
|
||||
const lines = splitNotificationLines(content)
|
||||
if (lines.length === 0) {
|
||||
return content
|
||||
}
|
||||
const match = lines[0].match(TICKET_ASSIGNED_PATTERN)
|
||||
if (match?.[1]) {
|
||||
lines[0] = `Ticket ${match[1]} has been assigned to you.`
|
||||
lines[0] = translateMessage(locale, "notification.ticketAssignedLine", {
|
||||
ticketNo: match[1],
|
||||
})
|
||||
}
|
||||
return lines
|
||||
.map((line) =>
|
||||
line.startsWith(ASSIGNMENT_REASON_PREFIX)
|
||||
? `Assignment reason: ${line.slice(ASSIGNMENT_REASON_PREFIX.length)}`
|
||||
? translateMessage(locale, "notification.assignmentReason", {
|
||||
reason: line.slice(ASSIGNMENT_REASON_PREFIX.length),
|
||||
})
|
||||
: line
|
||||
)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function localizeConversationAssignedContent(content: string) {
|
||||
function localizeConversationAssignedContent(content: string, locale: ReturnType<typeof normalizeLocale>) {
|
||||
const lines = splitNotificationLines(content)
|
||||
if (lines.length === 0) {
|
||||
return content
|
||||
}
|
||||
const match = lines[0].match(CONVERSATION_ASSIGNED_PATTERN)
|
||||
if (match?.[1]) {
|
||||
lines[0] = `Conversation #${match[1]} has been assigned to you.`
|
||||
lines[0] = translateMessage(locale, "notification.conversationAssignedLine", {
|
||||
conversationId: match[1],
|
||||
})
|
||||
}
|
||||
return lines
|
||||
.map((line) => {
|
||||
if (line.startsWith(CONVERSATION_ASSIGNMENT_REASON_PREFIX)) {
|
||||
return `Assignment reason: ${line.slice(CONVERSATION_ASSIGNMENT_REASON_PREFIX.length)}`
|
||||
return translateMessage(locale, "notification.assignmentReason", {
|
||||
reason: line.slice(CONVERSATION_ASSIGNMENT_REASON_PREFIX.length),
|
||||
})
|
||||
}
|
||||
if (line.startsWith(TRANSFER_REASON_PREFIX)) {
|
||||
return `Transfer reason: ${line.slice(TRANSFER_REASON_PREFIX.length)}`
|
||||
return translateMessage(locale, "notification.transferReason", {
|
||||
reason: line.slice(TRANSFER_REASON_PREFIX.length),
|
||||
})
|
||||
}
|
||||
return line
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function localizeNotificationTitle(title: string) {
|
||||
function localizeNotificationTitle(title: string, locale: ReturnType<typeof normalizeLocale>) {
|
||||
switch (title.trim()) {
|
||||
case TICKET_ASSIGNED_TITLE:
|
||||
return "Ticket assigned"
|
||||
return translateMessage(locale, "notification.ticketAssignedTitle")
|
||||
case CONVERSATION_TRANSFERRED_TITLE:
|
||||
return "Conversation transferred"
|
||||
return translateMessage(locale, "notification.conversationTransferredTitle")
|
||||
case CONVERSATION_AUTO_ASSIGNED_TITLE:
|
||||
return "Conversation auto-assigned"
|
||||
return translateMessage(locale, "notification.conversationAutoAssignedTitle")
|
||||
case CONVERSATION_ASSIGNED_TITLE:
|
||||
return "Conversation assigned"
|
||||
return translateMessage(locale, "notification.conversationAssignedTitle")
|
||||
default:
|
||||
return title
|
||||
}
|
||||
|
||||
+16
-1
@@ -53,7 +53,15 @@
|
||||
"markAllReadFailed": "Could not mark all notifications as read.",
|
||||
"fallbackTitle": "Notification",
|
||||
"loading": "Loading notifications",
|
||||
"empty": "No notifications"
|
||||
"empty": "No notifications",
|
||||
"ticketAssignedTitle": "Ticket assigned",
|
||||
"ticketAssignedLine": "Ticket {ticketNo} has been assigned to you.",
|
||||
"assignmentReason": "Assignment reason: {reason}",
|
||||
"conversationTransferredTitle": "Conversation transferred",
|
||||
"conversationAutoAssignedTitle": "Conversation auto-assigned",
|
||||
"conversationAssignedTitle": "Conversation assigned",
|
||||
"conversationAssignedLine": "Conversation #{conversationId} has been assigned to you.",
|
||||
"transferReason": "Transfer reason: {reason}"
|
||||
},
|
||||
"json": {
|
||||
"invalid": "Invalid JSON.",
|
||||
@@ -1643,6 +1651,13 @@
|
||||
"agentRequired": "Select an AI agent.",
|
||||
"messageRequired": "Enter a user message.",
|
||||
"emptyData": "No data",
|
||||
"skillRouteTrace": "Skill Route Trace",
|
||||
"toolSearchTrace": "Tool Search Trace",
|
||||
"graphToolTrace": "Graph Tool Trace",
|
||||
"traceData": "Trace Data",
|
||||
"resumeToolSearchTrace": "Resume Tool Search Trace",
|
||||
"resumeGraphToolTrace": "Resume Graph Tool Trace",
|
||||
"resumeTraceData": "Resume Trace Data",
|
||||
"confirm": "Confirm",
|
||||
"reject": "Cancel",
|
||||
"debugFailed": "Skill debug failed.",
|
||||
|
||||
+16
-1
@@ -53,7 +53,15 @@
|
||||
"markAllReadFailed": "全部已读失败",
|
||||
"fallbackTitle": "通知",
|
||||
"loading": "正在加载通知",
|
||||
"empty": "暂无通知"
|
||||
"empty": "暂无通知",
|
||||
"ticketAssignedTitle": "工单指派提醒",
|
||||
"ticketAssignedLine": "工单 {ticketNo} 已指派给你",
|
||||
"assignmentReason": "指派原因: {reason}",
|
||||
"conversationTransferredTitle": "会话转接提醒",
|
||||
"conversationAutoAssignedTitle": "会话自动分配提醒",
|
||||
"conversationAssignedTitle": "会话分配提醒",
|
||||
"conversationAssignedLine": "会话 #{conversationId} 已分配给你",
|
||||
"transferReason": "转接原因: {reason}"
|
||||
},
|
||||
"json": {
|
||||
"invalid": "JSON 格式不合法",
|
||||
@@ -1644,6 +1652,13 @@
|
||||
"agentRequired": "请选择 AI Agent",
|
||||
"messageRequired": "请输入用户消息",
|
||||
"emptyData": "暂无数据",
|
||||
"skillRouteTrace": "Skill 路由追踪",
|
||||
"toolSearchTrace": "工具搜索追踪",
|
||||
"graphToolTrace": "Graph 工具追踪",
|
||||
"traceData": "追踪数据",
|
||||
"resumeToolSearchTrace": "恢复工具搜索追踪",
|
||||
"resumeGraphToolTrace": "恢复 Graph 工具追踪",
|
||||
"resumeTraceData": "恢复追踪数据",
|
||||
"confirm": "确认",
|
||||
"reject": "取消",
|
||||
"debugFailed": "Skill 调试失败",
|
||||
|
||||
Reference in New Issue
Block a user