feat: refactor handoff logic and update UI descriptions for Graph Tool integration

This commit is contained in:
mlogclub
2026-04-10 16:03:06 +08:00
parent 15afa10c7d
commit ea327e2d97
3 changed files with 22 additions and 42 deletions
+1 -36
View File
@@ -103,14 +103,6 @@ func (s *aiReplyService) TriggerReply(ctx context.Context, conversation models.C
if pendingInterrupt := svc.ConversationInterruptService.FindLatestPendingByConversationID(conversation.ID); pendingInterrupt != nil {
return s.resumePendingInterrupt(ctx, conversation, message, aiAgent, pendingInterrupt, trace, &summary)
}
if s.shouldHandoffByQuestion(message.Content, aiAgent) {
return s.handoffConversation(conversation, aiAgent, "用户主动要求人工")
}
if aiAgent.ServiceMode != enums.IMConversationServiceModeAIOnly &&
aiAgent.MaxAIReplyRounds > 0 &&
conversation.AIReplyRounds >= aiAgent.MaxAIReplyRounds {
return s.handoffConversation(conversation, aiAgent, "达到AI最大回复轮次")
}
aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID)
if aiConfig == nil {
return fmt.Errorf("ai config is nil")
@@ -295,23 +287,6 @@ func (s *aiReplyService) sendAIReply(conversation models.Conversation, message m
return replyMessage, err
}
func (s *aiReplyService) shouldHandoffByQuestion(question string, aiAgent models.AIAgent) bool {
if aiAgent.ServiceMode == enums.IMConversationServiceModeAIOnly {
return false
}
normalized := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(question)), " ", "")
if normalized == "" {
return false
}
keywords := []string{"转人工", "人工客服"}
for _, keyword := range keywords {
if strings.Contains(normalized, keyword) {
return true
}
}
return false
}
func (s *aiReplyService) writeRunLog(startedAt time.Time, message models.Message, conversation models.Conversation, aiAgent models.AIAgent,
question string, runErr error, trace *aiReplyTraceData, summary *Summary) {
errorMessage := ""
@@ -661,7 +636,7 @@ func parseRuntimeTraceData(raw string) runtimeTraceProjection {
func isCancellationReply(replyText string) bool {
replyText = strings.TrimSpace(replyText)
return strings.Contains(replyText, "已取消本次工单创建")
return strings.Contains(replyText, "已取消本次工单创建") || strings.Contains(replyText, "已取消本次转人工")
}
func isCheckpointMissingError(err error) bool {
@@ -672,16 +647,6 @@ func isCheckpointMissingError(err error) bool {
return strings.Contains(message, "failed to load from checkpoint") && strings.Contains(message, "not exist")
}
func (s *aiReplyService) handoffConversation(conversation models.Conversation, aiAgent models.AIAgent, reason string) error {
if err := svc.ConversationService.HandoffByAI(conversation.ID, &aiAgent, reason); err != nil {
return err
}
if _, err := svc.MessageService.SendAIMessage(conversation.ID, aiAgent.ID, fmt.Sprintf("ai_handoff_%d", conversation.LastMessageID), enums.IMMessageTypeText, "已为你转接人工客服,请稍候。", "", s.buildAIPrincipal(aiAgent)); err != nil {
return err
}
return nil
}
func (s *aiReplyService) buildAIPrincipal(aiAgent models.AIAgent) *dto.AuthPrincipal {
username := "AI"
if strings.TrimSpace(aiAgent.Name) != "" {
+17 -2
View File
@@ -95,7 +95,7 @@ func (s *service) prepareToolsForRun(req *Request) error {
AIAgent: req.AIAgent,
AIConfig: req.AIConfig,
UserMessage: req.UserMessage,
AllowedToolCodes: resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill),
AllowedToolCodes: ensureCoreGraphToolCodes(resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill)),
})
if err != nil {
return err
@@ -113,7 +113,7 @@ func (s *service) prepareToolsForResume(req *ResumeRequest) error {
Conversation: req.Conversation,
AIAgent: req.AIAgent,
AIConfig: req.AIConfig,
AllowedToolCodes: parseAgentAllowedToolCodes(req.AIAgent),
AllowedToolCodes: ensureCoreGraphToolCodes(parseAgentAllowedToolCodes(req.AIAgent)),
})
if err != nil {
return err
@@ -281,3 +281,18 @@ func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinit
return ret
}
}
func ensureCoreGraphToolCodes(toolCodes []string) []string {
if len(toolCodes) == 0 {
return nil
}
ret := make([]string, 0, len(toolCodes)+1)
ret = append(ret, toolCodes...)
for _, item := range ret {
if strings.TrimSpace(item) == toolx.GraphHandoffConversationToolCode {
return ret
}
}
ret = append(ret, toolx.GraphHandoffConversationToolCode)
return ret
}
@@ -124,7 +124,7 @@ const fallbackModeOptions = getEnumOptions(AIAgentFallbackModeLabels).map(
value: String(option.value),
label: option.label,
}),
);
).filter((option) => option.value !== String(AIAgentFallbackMode.Handoff));
function buildForm(item: AIAgent | null): EditForm {
if (!item) {
@@ -973,7 +973,7 @@ function EditDialogBody({
<SectionCard
title="服务策略"
description="控制转人工规则、兜底策略和自动回复行为边界。"
description="控制 Graph Tool 转人工配置、兜底策略和自动回复边界。"
>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-4">
<Field data-invalid={!!errors.handoffMode}>
@@ -1050,7 +1050,7 @@ function EditDialogBody({
/>
<PopoverContent side="top" align="start" className="max-w-xs">
<PopoverDescription>
AI 0
AI handoff_to_human Graph Tool
</PopoverDescription>
</PopoverContent>
</Popover>
@@ -1163,7 +1163,7 @@ function EditDialogBody({
</FieldLabel>
<FieldContent>
<div className="text-xs text-muted-foreground mb-1">
使
使 AI Graph Tool
</div>
<Textarea
id="ai-agent-fallback-message"