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:
mlogclub
2026-06-03 09:36:33 +08:00
parent 5b6c54de34
commit 3439d20537
23 changed files with 520 additions and 265 deletions
@@ -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))
}
+3 -2
View File
@@ -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 {
+13 -6
View File
@@ -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"),
},
},
)),
+9 -10
View File
@@ -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
}
+114
View File
@@ -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."
+114
View File
@@ -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: "当前已尝试过的处理步骤,可选。"
+64 -112
View File
@@ -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 ""
}
}
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
+2 -2
View File
@@ -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")
}
+2 -1
View File
@@ -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>
+31 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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 调试失败",