From 47a99a4372f992f70ed55360cbf3140cf7a4c8d7 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Fri, 10 Apr 2026 16:24:49 +0800 Subject: [PATCH] feat: refactor confirmation handling and add new constants for ticket creation and handoff processes --- .../ai/runtime/graphs/create_ticket_graph.go | 45 ++++--------------- internal/ai/runtime/graphs/handoff_graph.go | 45 +++++-------------- internal/ai/runtime/graphs/hitl.go | 45 +++++++++++++++++++ internal/ai/runtime/reply_service.go | 10 ++--- 4 files changed, 68 insertions(+), 77 deletions(-) create mode 100644 internal/ai/runtime/graphs/hitl.go diff --git a/internal/ai/runtime/graphs/create_ticket_graph.go b/internal/ai/runtime/graphs/create_ticket_graph.go index 809303d..36c69f6 100644 --- a/internal/ai/runtime/graphs/create_ticket_graph.go +++ b/internal/ai/runtime/graphs/create_ticket_graph.go @@ -34,13 +34,6 @@ type CreateTicketGraph struct { aiAgent *models.AIAgent } -type Decision string - -const ( - DecisionConfirm Decision = "confirm" - DecisionCancel Decision = "cancel" -) - func NewCreateTicketGraph(conversation *models.Conversation, aiAgent *models.AIAgent) *CreateTicketGraph { return &CreateTicketGraph{ conversation: conversation, @@ -59,7 +52,7 @@ func (g *CreateTicketGraph) Run(ctx context.Context, argumentsInJSON string) (st return "", err } info := CreateTicketGraphInterruptInfo{ - Type: "ticket_creation_confirmation", + Type: InterruptTypeTicketCreationConfirmation, Message: g.buildConfirmationPrompt(req), } return "", componenttool.StatefulInterrupt(ctx, info, CreateTicketGraphState{Request: req}) @@ -70,32 +63,32 @@ func (g *CreateTicketGraph) Run(ctx context.Context, argumentsInJSON string) (st isResumeTarget, hasData, resumeText := componenttool.GetResumeContext[string](ctx) if !isResumeTarget { info := CreateTicketGraphInterruptInfo{ - Type: "ticket_creation_confirmation", + Type: InterruptTypeTicketCreationConfirmation, Message: g.buildConfirmationPrompt(state.Request), } return "", componenttool.StatefulInterrupt(ctx, info, state) } if !hasData { info := CreateTicketGraphInterruptInfo{ - Type: "ticket_creation_confirmation", - Message: "请回复“确认”或“取消”。", + Type: InterruptTypeTicketCreationConfirmation, + Message: ConfirmOrCancelPrompt, } return "", componenttool.StatefulInterrupt(ctx, info, state) } decision := ParseConfirmationDecision(resumeText) switch decision { - case DecisionConfirm: + case ConfirmationDecisionConfirm: item, err := services.TicketService.CreateFromConversation(state.Request, g.buildAIPrincipal()) if err != nil { return "", err } return fmt.Sprintf("工单已创建,工单号:%s,标题:%s。", strings.TrimSpace(item.TicketNo), strings.TrimSpace(item.Title)), nil - case DecisionCancel: - return "已取消本次工单创建。", nil + case ConfirmationDecisionCancel: + return CancelCreateTicketReply, nil default: info := CreateTicketGraphInterruptInfo{ - Type: "ticket_creation_confirmation", - Message: "我需要你的明确确认,请直接回复“确认”或“取消”。", + Type: InterruptTypeTicketCreationConfirmation, + Message: NeedExplicitConfirmationPrompt, } return "", componenttool.StatefulInterrupt(ctx, info, state) } @@ -176,23 +169,3 @@ func getInt64Value(data map[string]any, key string) int64 { return 0 } } - -func ParseConfirmationDecision(value string) Decision { - value = strings.ToLower(strings.TrimSpace(value)) - if value == "" { - return "" - } - confirmWords := []string{"确认", "是", "好的", "可以", "ok", "yes", "继续", "同意"} - for _, item := range confirmWords { - if strings.Contains(value, item) { - return DecisionConfirm - } - } - cancelWords := []string{"取消", "不用", "不需要", "算了", "no"} - for _, item := range cancelWords { - if strings.Contains(value, item) { - return DecisionCancel - } - } - return "" -} diff --git a/internal/ai/runtime/graphs/handoff_graph.go b/internal/ai/runtime/graphs/handoff_graph.go index 68ee490..0a0170b 100644 --- a/internal/ai/runtime/graphs/handoff_graph.go +++ b/internal/ai/runtime/graphs/handoff_graph.go @@ -50,7 +50,7 @@ func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string, return "", err } info := HandoffGraphInterruptInfo{ - Type: "handoff_confirmation", + Type: InterruptTypeHandoffConfirmation, Message: g.buildConfirmationPrompt(reason), } return "", componenttool.StatefulInterrupt(ctx, info, HandoffGraphState{Reason: reason}) @@ -61,30 +61,30 @@ func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string, isResumeTarget, hasData, resumeText := componenttool.GetResumeContext[string](ctx) if !isResumeTarget { info := HandoffGraphInterruptInfo{ - Type: "handoff_confirmation", + Type: InterruptTypeHandoffConfirmation, Message: g.buildConfirmationPrompt(state.Reason), } return "", componenttool.StatefulInterrupt(ctx, info, state) } if !hasData { info := HandoffGraphInterruptInfo{ - Type: "handoff_confirmation", - Message: "请回复“确认”或“取消”。", + Type: InterruptTypeHandoffConfirmation, + Message: ConfirmOrCancelPrompt, } return "", componenttool.StatefulInterrupt(ctx, info, state) } switch parseHandoffDecision(resumeText) { - case graphDecisionConfirm: + case ConfirmationDecisionConfirm: if err := services.ConversationService.HandoffByAI(g.conversation.ID, g.aiAgent, state.Reason); err != nil { return "", err } return "已为你转接人工客服,请稍候。", nil - case graphDecisionCancel: - return "已取消本次转人工。", nil + case ConfirmationDecisionCancel: + return CancelHandoffReply, nil default: info := HandoffGraphInterruptInfo{ - Type: "handoff_confirmation", - Message: "我需要你的明确确认,请直接回复“确认”或“取消”。", + Type: InterruptTypeHandoffConfirmation, + Message: NeedExplicitConfirmationPrompt, } return "", componenttool.StatefulInterrupt(ctx, info, state) } @@ -108,31 +108,8 @@ func (g *HandoffGraph) buildConfirmationPrompt(reason string) string { return fmt.Sprintf("我准备为你转接人工客服。\n原因:%s\n请直接回复“确认”或“取消”。", strings.TrimSpace(reason)) } -type graphDecision string - -const ( - graphDecisionConfirm graphDecision = "confirm" - graphDecisionCancel graphDecision = "cancel" -) - -func parseHandoffDecision(value string) graphDecision { - value = strings.ToLower(strings.TrimSpace(value)) - if value == "" { - return "" - } - confirmWords := []string{"确认", "是", "好的", "可以", "ok", "yes", "继续", "同意"} - for _, item := range confirmWords { - if strings.Contains(value, item) { - return graphDecisionConfirm - } - } - cancelWords := []string{"取消", "不用", "不需要", "算了", "no"} - for _, item := range cancelWords { - if strings.Contains(value, item) { - return graphDecisionCancel - } - } - return "" +func parseHandoffDecision(value string) ConfirmationDecision { + return ParseConfirmationDecision(value) } func graphGetStringValue(data map[string]any, key string) string { diff --git a/internal/ai/runtime/graphs/hitl.go b/internal/ai/runtime/graphs/hitl.go new file mode 100644 index 0000000..ec2d086 --- /dev/null +++ b/internal/ai/runtime/graphs/hitl.go @@ -0,0 +1,45 @@ +package graphs + +import "strings" + +const ( + InterruptTypeTicketCreationConfirmation = "ticket_creation_confirmation" + InterruptTypeHandoffConfirmation = "handoff_confirmation" + ConfirmOrCancelPrompt = "请回复“确认”或“取消”。" + NeedExplicitConfirmationPrompt = "我需要你的明确确认,请直接回复“确认”或“取消”。" + ConfirmationExpiredReply = "本次确认已失效,请重新发起。" + CancelCreateTicketReply = "已取消本次工单创建。" + CancelHandoffReply = "已取消本次转人工。" +) + +type ConfirmationDecision string + +const ( + ConfirmationDecisionConfirm ConfirmationDecision = "confirm" + ConfirmationDecisionCancel ConfirmationDecision = "cancel" +) + +func ParseConfirmationDecision(value string) ConfirmationDecision { + value = strings.ToLower(strings.TrimSpace(value)) + if value == "" { + return "" + } + confirmWords := []string{"确认", "是", "好的", "可以", "ok", "yes", "继续", "同意"} + for _, item := range confirmWords { + if strings.Contains(value, item) { + return ConfirmationDecisionConfirm + } + } + cancelWords := []string{"取消", "不用", "不需要", "算了", "no"} + for _, item := range cancelWords { + if strings.Contains(value, item) { + return ConfirmationDecisionCancel + } + } + return "" +} + +func IsCancellationReply(replyText string) bool { + replyText = strings.TrimSpace(replyText) + return strings.Contains(replyText, CancelCreateTicketReply) || strings.Contains(replyText, CancelHandoffReply) +} diff --git a/internal/ai/runtime/reply_service.go b/internal/ai/runtime/reply_service.go index 8501433..bb79e94 100644 --- a/internal/ai/runtime/reply_service.go +++ b/internal/ai/runtime/reply_service.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "cs-agent/internal/ai/runtime/graphs" "cs-agent/internal/models" "cs-agent/internal/pkg/dto" "cs-agent/internal/pkg/enums" @@ -172,7 +173,7 @@ func (s *aiReplyService) resumePendingInterrupt(ctx context.Context, conversatio if isCheckpointMissingError(err) { summary = &Summary{ Status: "expired", - ReplyText: "本次确认已失效,请重新发起。", + ReplyText: graphs.ConfirmationExpiredReply, } *summaryRef = summary trace.Status = "interrupt_expired" @@ -220,7 +221,7 @@ func (s *aiReplyService) resumePendingInterrupt(ctx context.Context, conversatio if replyMessage != nil { replyMessageID = replyMessage.ID } - if isCancellationReply(summary.ReplyText) { + if graphs.IsCancellationReply(summary.ReplyText) { return svc.ConversationInterruptService.MarkCancelled(pendingInterrupt.ID, replyMessageID) } return svc.ConversationInterruptService.MarkResolved(pendingInterrupt.ID, replyMessageID) @@ -652,11 +653,6 @@ func parseRuntimeTraceData(raw string) runtimeTraceProjection { return trace } -func isCancellationReply(replyText string) bool { - replyText = strings.TrimSpace(replyText) - return strings.Contains(replyText, "已取消本次工单创建") || strings.Contains(replyText, "已取消本次转人工") -} - func isCheckpointMissingError(err error) bool { if err == nil { return false