feat: implement Handoff Graph Tool for managing AI to human transitions
This commit is contained in:
@@ -0,0 +1,148 @@
|
|||||||
|
package graphs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"cs-agent/internal/models"
|
||||||
|
"cs-agent/internal/services"
|
||||||
|
|
||||||
|
componenttool "github.com/cloudwego/eino/components/tool"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandoffGraphState struct {
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandoffGraphInterruptInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
schema.RegisterName[HandoffGraphState]("cs_agent_handoff_graph_state")
|
||||||
|
schema.RegisterName[HandoffGraphInterruptInfo]("cs_agent_handoff_graph_interrupt_info")
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandoffGraph struct {
|
||||||
|
conversation *models.Conversation
|
||||||
|
aiAgent *models.AIAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandoffGraph(conversation *models.Conversation, aiAgent *models.AIAgent) *HandoffGraph {
|
||||||
|
return &HandoffGraph{
|
||||||
|
conversation: conversation,
|
||||||
|
aiAgent: aiAgent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string, error) {
|
||||||
|
if g == nil || g.conversation == nil || g.aiAgent == nil {
|
||||||
|
return "", fmt.Errorf("handoff graph not initialized")
|
||||||
|
}
|
||||||
|
wasInterrupted, hasState, state := componenttool.GetInterruptState[HandoffGraphState](ctx)
|
||||||
|
if !wasInterrupted {
|
||||||
|
reason, err := g.buildReason(argumentsInJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
info := HandoffGraphInterruptInfo{
|
||||||
|
Type: "handoff_confirmation",
|
||||||
|
Message: g.buildConfirmationPrompt(reason),
|
||||||
|
}
|
||||||
|
return "", componenttool.StatefulInterrupt(ctx, info, HandoffGraphState{Reason: reason})
|
||||||
|
}
|
||||||
|
if !hasState {
|
||||||
|
return "", fmt.Errorf("handoff graph state missing")
|
||||||
|
}
|
||||||
|
isResumeTarget, hasData, resumeText := componenttool.GetResumeContext[string](ctx)
|
||||||
|
if !isResumeTarget {
|
||||||
|
info := HandoffGraphInterruptInfo{
|
||||||
|
Type: "handoff_confirmation",
|
||||||
|
Message: g.buildConfirmationPrompt(state.Reason),
|
||||||
|
}
|
||||||
|
return "", componenttool.StatefulInterrupt(ctx, info, state)
|
||||||
|
}
|
||||||
|
if !hasData {
|
||||||
|
info := HandoffGraphInterruptInfo{
|
||||||
|
Type: "handoff_confirmation",
|
||||||
|
Message: "请回复“确认”或“取消”。",
|
||||||
|
}
|
||||||
|
return "", componenttool.StatefulInterrupt(ctx, info, state)
|
||||||
|
}
|
||||||
|
switch parseHandoffDecision(resumeText) {
|
||||||
|
case graphDecisionConfirm:
|
||||||
|
if err := services.ConversationService.HandoffByAI(g.conversation.ID, g.aiAgent, state.Reason); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "已为你转接人工客服,请稍候。", nil
|
||||||
|
case graphDecisionCancel:
|
||||||
|
return "已取消本次转人工。", nil
|
||||||
|
default:
|
||||||
|
info := HandoffGraphInterruptInfo{
|
||||||
|
Type: "handoff_confirmation",
|
||||||
|
Message: "我需要你的明确确认,请直接回复“确认”或“取消”。",
|
||||||
|
}
|
||||||
|
return "", componenttool.StatefulInterrupt(ctx, info, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HandoffGraph) buildReason(argumentsInJSON string) (string, error) {
|
||||||
|
reason := "用户需要转人工支持"
|
||||||
|
raw := make(map[string]any)
|
||||||
|
if strings.TrimSpace(argumentsInJSON) != "" {
|
||||||
|
if err := json.Unmarshal([]byte(argumentsInJSON), &raw); err != nil {
|
||||||
|
return "", fmt.Errorf("invalid handoff arguments: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parsed := strings.TrimSpace(graphGetStringValue(raw, "reason")); parsed != "" {
|
||||||
|
reason = parsed
|
||||||
|
}
|
||||||
|
return reason, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 graphGetStringValue(data map[string]any, key string) string {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
value, ok := data[key]
|
||||||
|
if !ok || value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
text, _ := value.(string)
|
||||||
|
return text
|
||||||
|
}
|
||||||
@@ -83,6 +83,9 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m
|
|||||||
} else if toolCode == toolx.GraphCreateTicketConfirmToolCode {
|
} else if toolCode == toolx.GraphCreateTicketConfirmToolCode {
|
||||||
serverCode = toolx.GraphToolCatalogServerCode
|
serverCode = toolx.GraphToolCatalogServerCode
|
||||||
toolName = toolx.GraphCreateTicketConfirmToolName
|
toolName = toolx.GraphCreateTicketConfirmToolName
|
||||||
|
} else if toolCode == toolx.GraphHandoffConversationToolCode {
|
||||||
|
serverCode = toolx.GraphToolCatalogServerCode
|
||||||
|
toolName = toolx.GraphHandoffConversationToolName
|
||||||
}
|
}
|
||||||
toolMetadataBy[modelName] = einocallbacks.ToolMetadata{
|
toolMetadataBy[modelName] = einocallbacks.ToolMetadata{
|
||||||
ToolCode: toolCode,
|
ToolCode: toolCode,
|
||||||
@@ -150,6 +153,16 @@ func buildAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.SkillD
|
|||||||
3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation 工具,禁止直接口头宣称“已经创建工单”。
|
3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation 工具,禁止直接口头宣称“已经创建工单”。
|
||||||
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正创建工单;用户取消则结束本次建单流程。
|
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正创建工单;用户取消则结束本次建单流程。
|
||||||
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
|
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
if hasToolCode(extraToolCodes, toolx.GraphHandoffConversationToolCode) {
|
||||||
|
appendixParts = append(appendixParts, strings.TrimSpace(`
|
||||||
|
你可以在确认需要人工介入后调用 handoff_to_human 这个 Graph Tool 来转人工,但必须遵守以下规则:
|
||||||
|
1. 只有在用户明确要求人工客服,或你已经判断该问题必须由人工继续处理时,才调用该工具。
|
||||||
|
2. 调用前先尽量整理清楚转人工原因;如果理由含糊,先追问或澄清,不要直接转人工。
|
||||||
|
3. 一旦决定转人工,必须调用 handoff_to_human 工具,禁止只在回复里口头说“我帮你转人工了”。
|
||||||
|
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正转人工;用户取消则结束本次转人工流程。
|
||||||
|
5. 如果问题仍可由当前对话继续解决,优先继续解答,不要过早转人工。
|
||||||
`))
|
`))
|
||||||
}
|
}
|
||||||
projectRoot, _ := os.Getwd()
|
projectRoot, _ := os.Getwd()
|
||||||
|
|||||||
@@ -673,33 +673,12 @@ func isCheckpointMissingError(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *aiReplyService) handoffConversation(conversation models.Conversation, aiAgent models.AIAgent, reason string) error {
|
func (s *aiReplyService) handoffConversation(conversation models.Conversation, aiAgent models.AIAgent, reason string) error {
|
||||||
now := time.Now()
|
if err := svc.ConversationService.HandoffByAI(conversation.ID, &aiAgent, reason); err != nil {
|
||||||
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
|
||||||
if err := repositories.ConversationRepository.Updates(ctx.Tx, conversation.ID, map[string]any{
|
|
||||||
"handoff_at": now,
|
|
||||||
"handoff_reason": strings.TrimSpace(reason),
|
|
||||||
"status": enums.IMConversationStatusPending,
|
|
||||||
"current_team_id": 0,
|
|
||||||
"current_assignee_id": 0,
|
|
||||||
"update_user_id": 0,
|
|
||||||
"update_user_name": aiAgent.Name,
|
|
||||||
"updated_at": now,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return svc.ConversationEventLogService.CreateEvent(ctx, conversation.ID, enums.IMEventTypeTransfer, enums.IMSenderTypeAI, aiAgent.ID, "AI转人工", strings.TrimSpace(reason))
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
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 {
|
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 err
|
||||||
}
|
}
|
||||||
if _, err := svc.ConversationDispatchService.DispatchConversation(conversation.ID); err != nil {
|
|
||||||
slog.Warn("auto dispatch conversation after ai handoff failed",
|
|
||||||
"conversation_id", conversation.ID,
|
|
||||||
"ai_agent_id", aiAgent.ID,
|
|
||||||
"error", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func newService() *service {
|
|||||||
runtime: engine.NewService(),
|
runtime: engine.NewService(),
|
||||||
registry: registry.NewRegistry(
|
registry: registry.NewRegistry(
|
||||||
tools.NewCreateTicketGraphTool(),
|
tools.NewCreateTicketGraphTool(),
|
||||||
|
tools.NewHandoffGraphTool(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"cs-agent/internal/ai/runtime/graphs"
|
||||||
|
"cs-agent/internal/ai/runtime/registry"
|
||||||
|
"cs-agent/internal/models"
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
|
|
||||||
|
einotool "github.com/cloudwego/eino/components/tool"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
einojsonschema "github.com/eino-contrib/jsonschema"
|
||||||
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HandoffConversationToolCode = toolx.GraphHandoffConversationToolCode
|
||||||
|
HandoffConversationToolName = toolx.GraphHandoffConversationToolName
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandoffGraphTool struct {
|
||||||
|
conversation *models.Conversation
|
||||||
|
aiAgent *models.AIAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandoffGraphTool() *HandoffGraphTool {
|
||||||
|
return &HandoffGraphTool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HandoffGraphTool) Name() string {
|
||||||
|
return HandoffConversationToolName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HandoffGraphTool) Code() string {
|
||||||
|
return HandoffConversationToolCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HandoffGraphTool) Enabled(ctx registry.Context) bool {
|
||||||
|
return ctx.Conversation != nil && ctx.AIAgent != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HandoffGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error) {
|
||||||
|
if !t.Enabled(ctx) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &HandoffGraphTool{
|
||||||
|
conversation: ctx.Conversation,
|
||||||
|
aiAgent: ctx.AIAgent,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HandoffGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||||
|
return &schema.ToolInfo{
|
||||||
|
Name: HandoffConversationToolName,
|
||||||
|
Desc: "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。仅在用户明确要求人工客服,或你已确认必须转人工处理时调用。",
|
||||||
|
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||||
|
Version: einojsonschema.Version,
|
||||||
|
Type: "object",
|
||||||
|
Properties: orderedmap.New[string, *einojsonschema.Schema](orderedmap.WithInitialData(
|
||||||
|
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||||
|
Key: "reason",
|
||||||
|
Value: &einojsonschema.Schema{
|
||||||
|
Type: "string",
|
||||||
|
Description: "转人工原因,简洁说明为何需要人工介入,例如用户明确要求人工、问题需要人工核验、需要人工售后处理等。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
Extra: map[string]any{
|
||||||
|
"toolCode": HandoffConversationToolCode,
|
||||||
|
"sourceType": "graph",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HandoffGraphTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) {
|
||||||
|
if t == nil || t.conversation == nil || t.aiAgent == nil {
|
||||||
|
return "", fmt.Errorf("handoff graph tool not initialized")
|
||||||
|
}
|
||||||
|
return graphs.NewHandoffGraph(t.conversation, t.aiAgent).Run(ctx, argumentsInJSON)
|
||||||
|
}
|
||||||
@@ -186,6 +186,9 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
} else if toolCode == toolx.GraphCreateTicketConfirmToolCode {
|
} else if toolCode == toolx.GraphCreateTicketConfirmToolCode {
|
||||||
serverCode = toolx.GraphToolCatalogServerCode
|
serverCode = toolx.GraphToolCatalogServerCode
|
||||||
toolName = toolx.GraphCreateTicketConfirmToolName
|
toolName = toolx.GraphCreateTicketConfirmToolName
|
||||||
|
} else if toolCode == toolx.GraphHandoffConversationToolCode {
|
||||||
|
serverCode = toolx.GraphToolCatalogServerCode
|
||||||
|
toolName = toolx.GraphHandoffConversationToolName
|
||||||
} else if parsedServerCode, parsedToolName := toolx.SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
|
} else if parsedServerCode, parsedToolName := toolx.SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
|
||||||
serverCode = parsedServerCode
|
serverCode = parsedServerCode
|
||||||
toolName = parsedToolName
|
toolName = parsedToolName
|
||||||
@@ -197,6 +200,8 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
title = toolx.BuiltinToolSearchToolTitle
|
title = toolx.BuiltinToolSearchToolTitle
|
||||||
case toolx.GraphCreateTicketConfirmToolCode:
|
case toolx.GraphCreateTicketConfirmToolCode:
|
||||||
title = toolx.GraphCreateTicketConfirmToolTitle
|
title = toolx.GraphCreateTicketConfirmToolTitle
|
||||||
|
case toolx.GraphHandoffConversationToolCode:
|
||||||
|
title = toolx.GraphHandoffConversationToolTitle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
description := strings.TrimSpace(tool.Description)
|
description := strings.TrimSpace(tool.Description)
|
||||||
@@ -206,6 +211,8 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
description = toolx.BuiltinToolSearchToolDescription
|
description = toolx.BuiltinToolSearchToolDescription
|
||||||
case toolx.GraphCreateTicketConfirmToolCode:
|
case toolx.GraphCreateTicketConfirmToolCode:
|
||||||
description = toolx.GraphCreateTicketConfirmToolDescription
|
description = toolx.GraphCreateTicketConfirmToolDescription
|
||||||
|
case toolx.GraphHandoffConversationToolCode:
|
||||||
|
description = toolx.GraphHandoffConversationToolDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{
|
ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ const (
|
|||||||
GraphCreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
GraphCreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
||||||
GraphCreateTicketConfirmToolTitle = "创建工单确认流程"
|
GraphCreateTicketConfirmToolTitle = "创建工单确认流程"
|
||||||
GraphCreateTicketConfirmToolDescription = "Graph Tool。用于封装建单参数整理、用户确认、真正建单和结果返回的确定性流程。"
|
GraphCreateTicketConfirmToolDescription = "Graph Tool。用于封装建单参数整理、用户确认、真正建单和结果返回的确定性流程。"
|
||||||
|
GraphHandoffConversationToolCode = "graph/handoff_to_human"
|
||||||
|
GraphHandoffConversationToolName = "handoff_to_human"
|
||||||
|
GraphHandoffConversationToolTitle = "转人工确认流程"
|
||||||
|
GraphHandoffConversationToolDescription = "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。"
|
||||||
BuiltinCreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation"
|
BuiltinCreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation"
|
||||||
BuiltinCreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
BuiltinCreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
||||||
BuiltinCreateTicketConfirmToolTitle = "创建工单并发起确认"
|
BuiltinCreateTicketConfirmToolTitle = "创建工单并发起确认"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
"cs-agent/internal/pkg/constants"
|
"cs-agent/internal/pkg/constants"
|
||||||
@@ -284,6 +285,44 @@ func (s *conversationService) TransferConversation(conversationID, toUserID int6
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *conversationService) HandoffByAI(conversationID int64, aiAgent *models.AIAgent, reason string) error {
|
||||||
|
if conversationID <= 0 {
|
||||||
|
return errorsx.InvalidParam("会话不存在")
|
||||||
|
}
|
||||||
|
if aiAgent == nil {
|
||||||
|
return errorsx.InvalidParam("AI Agent 不存在")
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||||
|
conversation := repositories.ConversationRepository.Get(ctx.Tx, conversationID)
|
||||||
|
if conversation == nil {
|
||||||
|
return errorsx.InvalidParam("会话不存在")
|
||||||
|
}
|
||||||
|
if err := repositories.ConversationRepository.Updates(ctx.Tx, conversationID, map[string]any{
|
||||||
|
"handoff_at": now,
|
||||||
|
"handoff_reason": strings.TrimSpace(reason),
|
||||||
|
"status": enums.IMConversationStatusPending,
|
||||||
|
"current_team_id": 0,
|
||||||
|
"current_assignee_id": 0,
|
||||||
|
"update_user_id": 0,
|
||||||
|
"update_user_name": aiAgent.Name,
|
||||||
|
"updated_at": now,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ConversationEventLogService.CreateEvent(ctx, conversationID, enums.IMEventTypeTransfer, enums.IMSenderTypeAI, aiAgent.ID, "AI转人工", strings.TrimSpace(reason))
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := ConversationDispatchService.DispatchConversation(conversationID); err != nil {
|
||||||
|
slog.Warn("auto dispatch conversation after ai handoff failed",
|
||||||
|
"conversation_id", conversationID,
|
||||||
|
"ai_agent_id", aiAgent.ID,
|
||||||
|
"error", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *conversationService) CloseConversation(conversationID int64, closeReason string, operator *dto.AuthPrincipal) error {
|
func (s *conversationService) CloseConversation(conversationID int64, closeReason string, operator *dto.AuthPrincipal) error {
|
||||||
if operator == nil {
|
if operator == nil {
|
||||||
return errorsx.Unauthorized("未登录或登录已过期")
|
return errorsx.Unauthorized("未登录或登录已过期")
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ type MCPToolCatalogItem struct {
|
|||||||
|
|
||||||
func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalogItem, error) {
|
func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalogItem, error) {
|
||||||
cfg := config.Current()
|
cfg := config.Current()
|
||||||
ret := make([]MCPToolCatalogItem, 0, 2)
|
ret := make([]MCPToolCatalogItem, 0, 3)
|
||||||
ret = append(ret, MCPToolCatalogItem{
|
ret = append(ret, MCPToolCatalogItem{
|
||||||
ToolCode: toolx.GraphCreateTicketConfirmToolCode,
|
ToolCode: toolx.GraphCreateTicketConfirmToolCode,
|
||||||
ServerCode: toolx.GraphToolCatalogServerCode,
|
ServerCode: toolx.GraphToolCatalogServerCode,
|
||||||
@@ -43,6 +43,15 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog
|
|||||||
Title: toolx.GraphCreateTicketConfirmToolTitle,
|
Title: toolx.GraphCreateTicketConfirmToolTitle,
|
||||||
Description: toolx.GraphCreateTicketConfirmToolDescription,
|
Description: toolx.GraphCreateTicketConfirmToolDescription,
|
||||||
})
|
})
|
||||||
|
ret = append(ret, MCPToolCatalogItem{
|
||||||
|
ToolCode: toolx.GraphHandoffConversationToolCode,
|
||||||
|
ServerCode: toolx.GraphToolCatalogServerCode,
|
||||||
|
ToolName: toolx.GraphHandoffConversationToolName,
|
||||||
|
SourceType: toolx.GraphToolCatalogServerCode,
|
||||||
|
AutoInjected: false,
|
||||||
|
Title: toolx.GraphHandoffConversationToolTitle,
|
||||||
|
Description: toolx.GraphHandoffConversationToolDescription,
|
||||||
|
})
|
||||||
if !cfg.MCP.Enabled {
|
if !cfg.MCP.Enabled {
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
@@ -96,7 +105,7 @@ func (s *toolCatalogService) ValidateToolCode(toolCode string) error {
|
|||||||
return errorsx.InvalidParam("toolCode不能为空")
|
return errorsx.InvalidParam("toolCode不能为空")
|
||||||
}
|
}
|
||||||
switch toolCode {
|
switch toolCode {
|
||||||
case toolx.BuiltinToolSearchToolCode, toolx.BuiltinCreateTicketConfirmToolCode, toolx.GraphCreateTicketConfirmToolCode:
|
case toolx.BuiltinToolSearchToolCode, toolx.BuiltinCreateTicketConfirmToolCode, toolx.GraphCreateTicketConfirmToolCode, toolx.GraphHandoffConversationToolCode:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||||
|
|||||||
Reference in New Issue
Block a user