diff --git a/internal/ai/runtime/graphs/create_ticket_graph.go b/internal/ai/runtime/graphs/create_ticket_graph.go index dd9d8ab..26017ac 100644 --- a/internal/ai/runtime/graphs/create_ticket_graph.go +++ b/internal/ai/runtime/graphs/create_ticket_graph.go @@ -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)) } diff --git a/internal/ai/runtime/graphs/handoff_graph.go b/internal/ai/runtime/graphs/handoff_graph.go index 8ffc3f0..e537d0c 100644 --- a/internal/ai/runtime/graphs/handoff_graph.go +++ b/internal/ai/runtime/graphs/handoff_graph.go @@ -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 { diff --git a/internal/ai/runtime/graphs/hitl.go b/internal/ai/runtime/graphs/hitl.go index 143c7a9..077f4d2 100644 --- a/internal/ai/runtime/graphs/hitl.go +++ b/internal/ai/runtime/graphs/hitl.go @@ -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 diff --git a/internal/ai/runtime/reply_interrupt_helpers.go b/internal/ai/runtime/reply_interrupt_helpers.go index 1bc9a69..c77df1d 100644 --- a/internal/ai/runtime/reply_interrupt_helpers.go +++ b/internal/ai/runtime/reply_interrupt_helpers.go @@ -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 { diff --git a/internal/ai/runtime/tools/analyze_conversation_tool.go b/internal/ai/runtime/tools/analyze_conversation_tool.go index dac13a8..dd0315a 100644 --- a/internal/ai/runtime/tools/analyze_conversation_tool.go +++ b/internal/ai/runtime/tools/analyze_conversation_tool.go @@ -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"), }, }, )), diff --git a/internal/ai/runtime/tools/create_ticket_confirm_tool.go b/internal/ai/runtime/tools/create_ticket_confirm_tool.go index 8b08b8b..7d0145f 100644 --- a/internal/ai/runtime/tools/create_ticket_confirm_tool.go +++ b/internal/ai/runtime/tools/create_ticket_confirm_tool.go @@ -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"), }, }, )), diff --git a/internal/ai/runtime/tools/handoff_graph_tool.go b/internal/ai/runtime/tools/handoff_graph_tool.go index a13ca79..c21e2e6 100644 --- a/internal/ai/runtime/tools/handoff_graph_tool.go +++ b/internal/ai/runtime/tools/handoff_graph_tool.go @@ -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"), }, }, )), diff --git a/internal/ai/runtime/tools/prepare_ticket_draft_tool.go b/internal/ai/runtime/tools/prepare_ticket_draft_tool.go index 20b8d0d..8c551c7 100644 --- a/internal/ai/runtime/tools/prepare_ticket_draft_tool.go +++ b/internal/ai/runtime/tools/prepare_ticket_draft_tool.go @@ -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"), }, }, )), diff --git a/internal/ai/runtime/tools/triage_service_request_tool.go b/internal/ai/runtime/tools/triage_service_request_tool.go index e352ed2..24eab35 100644 --- a/internal/ai/runtime/tools/triage_service_request_tool.go +++ b/internal/ai/runtime/tools/triage_service_request_tool.go @@ -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"), }, }, )), diff --git a/internal/builders/notification_builder.go b/internal/builders/notification_builder.go index 449a3f1..16f84a6 100644 --- a/internal/builders/notification_builder.go +++ b/internal/builders/notification_builder.go @@ -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 } diff --git a/internal/pkg/i18nx/locales/en-US.yml b/internal/pkg/i18nx/locales/en-US.yml index 64a1d9f..e3abd7b 100644 --- a/internal/pkg/i18nx/locales/en-US.yml +++ b/internal/pkg/i18nx/locales/en-US.yml @@ -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." diff --git a/internal/pkg/i18nx/locales/zh-CN.yml b/internal/pkg/i18nx/locales/zh-CN.yml index 9d21a38..f9013ab 100644 --- a/internal/pkg/i18nx/locales/zh-CN.yml +++ b/internal/pkg/i18nx/locales/zh-CN.yml @@ -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: "当前已尝试过的处理步骤,可选。" diff --git a/internal/pkg/toolx/builtin_tools.go b/internal/pkg/toolx/builtin_tools.go index 8d2292e..4d83bf2 100644 --- a/internal/pkg/toolx/builtin_tools.go +++ b/internal/pkg/toolx/builtin_tools.go @@ -8,17 +8,20 @@ import ( ) type ToolSpec struct { - Code string - ServerCode string - Name string - Title string - Description string - SourceType enums.ToolSourceType - AutoInjected bool - DirectAccess bool - RuntimeStatic bool - Aliases []string - Appendix string + Code string + 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 { @@ -30,116 +33,97 @@ type ToolMetadata struct { var ( BuiltinToolSearch = ToolSpec{ - Code: "builtin/tool_search", - ServerCode: "builtin", - Name: "tool_search", - Title: "搜索并调用动态工具", - Description: "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。", - SourceType: enums.ToolSourceTypeBuiltin, - AutoInjected: true, - DirectAccess: true, - Appendix: strings.TrimSpace(` -当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则: -1. 先调用 tool_search 搜索需要的动态工具,再继续使用已选中的真实工具。 -2. 不要假设所有长尾工具一开始就可见;只有被 tool_search 选中的工具,后续模型调用才会暴露出来。 -3. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。 -`), + Code: "builtin/tool_search", + ServerCode: "builtin", + Name: "tool_search", + 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: i18nx.Get("tool.builtin.toolSearch.appendix"), + AppendixKey: "tool.builtin.toolSearch.appendix", } BuiltinSkill = ToolSpec{ - Code: "builtin/skill", - ServerCode: "builtin", - Name: "skill", - Title: "加载专项技能说明", - Description: "用于按需加载当前 Agent 可用的专项技能说明文档,适合在需要专项处理规则时再注入上下文。", - SourceType: enums.ToolSourceTypeBuiltin, - AutoInjected: true, + Code: "builtin/skill", + ServerCode: "builtin", + Name: "skill", + 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, } GraphTriageServiceRequest = ToolSpec{ - 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.", - 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. -`), + Code: "graph/triage_service_request", + ServerCode: "graph", + Name: "triage_service_request", + 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: 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.", - 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. -`), + Code: "graph/analyze_conversation", + ServerCode: "graph", + Name: "analyze_conversation", + 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: 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.", - 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. -`), + Code: "graph/prepare_ticket_draft", + ServerCode: "graph", + Name: "prepare_ticket_draft", + 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: 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.", - 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. -`), + Code: "graph/create_ticket_with_confirmation", + ServerCode: "graph", + Name: "create_ticket_with_confirmation", + 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: 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.", - 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. -`), + Code: "graph/handoff_to_human", + ServerCode: "graph", + Name: "handoff_to_human", + 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: 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 "" } -} - -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 "" + if strings.TrimSpace(spec.AppendixKey) != "" { + return i18nx.Getf(locale, spec.AppendixKey) } + 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 diff --git a/internal/pkg/toolx/builtin_tools_test.go b/internal/pkg/toolx/builtin_tools_test.go index e47d5f1..fd7d41e 100644 --- a/internal/pkg/toolx/builtin_tools_test.go +++ b/internal/pkg/toolx/builtin_tools_test.go @@ -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) } } diff --git a/internal/services/conversation_human_dispatch_service.go b/internal/services/conversation_human_dispatch_service.go index 69430ba..a9ed0f3 100644 --- a/internal/services/conversation_human_dispatch_service.go +++ b/internal/services/conversation_human_dispatch_service.go @@ -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 ( diff --git a/internal/services/event_handlers/conversation_assigned_event_handler.go b/internal/services/event_handlers/conversation_assigned_event_handler.go index 22522b5..0ebabe1 100644 --- a/internal/services/event_handlers/conversation_assigned_event_handler.go +++ b/internal/services/event_handlers/conversation_assigned_event_handler.go @@ -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") } diff --git a/internal/services/event_handlers/notification_event_handler.go b/internal/services/event_handlers/notification_event_handler.go index d56e611..a35b210 100644 --- a/internal/services/event_handlers/notification_event_handler.go +++ b/internal/services/event_handlers/notification_event_handler.go @@ -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, diff --git a/internal/services/event_handlers/ticket_assigned_event_handler.go b/internal/services/event_handlers/ticket_assigned_event_handler.go index cce17be..6af4087 100644 --- a/internal/services/event_handlers/ticket_assigned_event_handler.go +++ b/internal/services/event_handlers/ticket_assigned_event_handler.go @@ -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") } diff --git a/internal/services/ticket_service.go b/internal/services/ticket_service.go index 6ea5969..7b6a397 100644 --- a/internal/services/ticket_service.go +++ b/internal/services/ticket_service.go @@ -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 == "" { diff --git a/web/app/dashboard/skill-definition/_components/debug-dialog.tsx b/web/app/dashboard/skill-definition/_components/debug-dialog.tsx index 5dea435..29931f0 100644 --- a/web/app/dashboard/skill-definition/_components/debug-dialog.tsx +++ b/web/app/dashboard/skill-definition/_components/debug-dialog.tsx @@ -457,10 +457,10 @@ function DebugDialogBody({