From 3db7bec66247ea0927ff26847de2afb223e1eb0d Mon Sep 17 00:00:00 2001 From: mlogclub Date: Fri, 10 Apr 2026 15:45:51 +0800 Subject: [PATCH] feat: refactor ticket creation process to use Graph Tool for confirmation and state management --- .../ai/runtime/graphs/create_ticket_graph.go | 198 ++++++++++++++++++ .../impl/callbacks/agent_trace_handler.go | 13 ++ .../internal/impl/factory/agent_factory.go | 12 +- .../internal/impl/factory/tool_factory.go | 1 + internal/ai/runtime/service.go | 6 +- .../tools/create_ticket_confirm_tool.go | 144 ++----------- .../console/ai_agent_controller.go | 15 +- internal/pkg/toolx/builtin_tools.go | 14 ++ internal/pkg/toolx/mcp_tool.go | 1 + internal/services/skill_definition_service.go | 2 + internal/services/tool_catalog_service.go | 14 +- 11 files changed, 273 insertions(+), 147 deletions(-) create mode 100644 internal/ai/runtime/graphs/create_ticket_graph.go diff --git a/internal/ai/runtime/graphs/create_ticket_graph.go b/internal/ai/runtime/graphs/create_ticket_graph.go new file mode 100644 index 0000000..809303d --- /dev/null +++ b/internal/ai/runtime/graphs/create_ticket_graph.go @@ -0,0 +1,198 @@ +package graphs + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "cs-agent/internal/models" + "cs-agent/internal/pkg/dto" + "cs-agent/internal/pkg/dto/request" + "cs-agent/internal/services" + + componenttool "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +type CreateTicketGraphState struct { + Request request.CreateTicketFromConversationRequest +} + +type CreateTicketGraphInterruptInfo struct { + Type string `json:"type"` + Message string `json:"message"` +} + +func init() { + schema.RegisterName[CreateTicketGraphState]("cs_agent_create_ticket_graph_state") + schema.RegisterName[CreateTicketGraphInterruptInfo]("cs_agent_create_ticket_graph_interrupt_info") +} + +type CreateTicketGraph struct { + conversation *models.Conversation + 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, + aiAgent: aiAgent, + } +} + +func (g *CreateTicketGraph) Run(ctx context.Context, argumentsInJSON string) (string, error) { + if g == nil || g.conversation == nil || g.aiAgent == nil { + return "", fmt.Errorf("create ticket graph not initialized") + } + wasInterrupted, hasState, state := componenttool.GetInterruptState[CreateTicketGraphState](ctx) + if !wasInterrupted { + req, err := g.buildCreateRequest(argumentsInJSON) + if err != nil { + return "", err + } + info := CreateTicketGraphInterruptInfo{ + Type: "ticket_creation_confirmation", + Message: g.buildConfirmationPrompt(req), + } + return "", componenttool.StatefulInterrupt(ctx, info, CreateTicketGraphState{Request: req}) + } + if !hasState { + return "", fmt.Errorf("create ticket graph state missing") + } + isResumeTarget, hasData, resumeText := componenttool.GetResumeContext[string](ctx) + if !isResumeTarget { + info := CreateTicketGraphInterruptInfo{ + Type: "ticket_creation_confirmation", + Message: g.buildConfirmationPrompt(state.Request), + } + return "", componenttool.StatefulInterrupt(ctx, info, state) + } + if !hasData { + info := CreateTicketGraphInterruptInfo{ + Type: "ticket_creation_confirmation", + Message: "请回复“确认”或“取消”。", + } + return "", componenttool.StatefulInterrupt(ctx, info, state) + } + decision := ParseConfirmationDecision(resumeText) + switch decision { + case DecisionConfirm: + 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 + default: + info := CreateTicketGraphInterruptInfo{ + Type: "ticket_creation_confirmation", + Message: "我需要你的明确确认,请直接回复“确认”或“取消”。", + } + return "", componenttool.StatefulInterrupt(ctx, info, state) + } +} + +func (g *CreateTicketGraph) buildCreateRequest(argumentsInJSON string) (request.CreateTicketFromConversationRequest, error) { + req := request.CreateTicketFromConversationRequest{ + ConversationID: g.conversation.ID, + SyncToConversation: true, + } + raw := make(map[string]any) + if strings.TrimSpace(argumentsInJSON) != "" { + if err := json.Unmarshal([]byte(argumentsInJSON), &raw); err != nil { + return req, fmt.Errorf("invalid create ticket arguments: %w", err) + } + } + req.Title = strings.TrimSpace(getStringValue(raw, "title")) + req.Description = strings.TrimSpace(getStringValue(raw, "description")) + req.Priority = getInt64Value(raw, "priority") + req.Severity = int(getInt64Value(raw, "severity")) + if req.Title == "" { + req.Title = strings.TrimSpace(g.conversation.Subject) + } + if req.Description == "" { + req.Description = strings.TrimSpace(g.conversation.LastMessageSummary) + } + if strings.TrimSpace(req.Title) == "" { + return req, fmt.Errorf("ticket title is required") + } + return req, nil +} + +func (g *CreateTicketGraph) buildConfirmationPrompt(req request.CreateTicketFromConversationRequest) string { + return fmt.Sprintf("我准备为你创建工单。\n标题:%s\n描述:%s\n请直接回复“确认”或“取消”。", + strings.TrimSpace(req.Title), strings.TrimSpace(req.Description)) +} + +func (g *CreateTicketGraph) buildAIPrincipal() *dto.AuthPrincipal { + username := "AI" + if strings.TrimSpace(g.aiAgent.Name) != "" { + username = strings.TrimSpace(g.aiAgent.Name) + } + return &dto.AuthPrincipal{ + UserID: 0, + Username: username, + Nickname: username, + } +} + +func getStringValue(data map[string]any, key string) string { + if len(data) == 0 { + return "" + } + value, ok := data[key] + if !ok { + return "" + } + text, _ := value.(string) + return text +} + +func getInt64Value(data map[string]any, key string) int64 { + if len(data) == 0 { + return 0 + } + value, ok := data[key] + if !ok { + return 0 + } + switch v := value.(type) { + case float64: + return int64(v) + case int64: + return v + case int: + return int64(v) + default: + 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/internal/impl/callbacks/agent_trace_handler.go b/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go index dcf8fc5..5f397d3 100644 --- a/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go +++ b/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go @@ -17,6 +17,7 @@ type ToolMetadata struct { ToolCode string ServerCode string ToolName string + SourceType string } type RuntimeTraceHandler struct { @@ -58,6 +59,17 @@ func (h *RuntimeTraceHandler) WrapInvokableToolCall(_ context.Context, endpoint item.ErrorMessage = err.Error() } h.collector.AddToolItem(item) + if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.SourceType) == toolx.GraphToolCatalogServerCode { + h.collector.AddGraphToolItem(GraphToolTraceItem{ + ToolCode: item.ToolCode, + ToolName: item.ToolName, + Arguments: item.Arguments, + ResultPreview: item.ResultPreview, + LatencyMs: item.LatencyMs, + Status: item.Status, + ErrorMessage: item.ErrorMessage, + }) + } if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearchToolCode { h.collector.AddToolSearchItem(h.buildToolSearchTraceItem(argumentsInJSON, result, err)) } @@ -78,6 +90,7 @@ func (h *RuntimeTraceHandler) resolveToolMetadata(modelToolName string) (ToolMet ToolCode: toolx.BuiltinToolSearchToolCode, ServerCode: toolx.BuiltinToolCatalogServerCode, ToolName: toolx.BuiltinToolSearchToolName, + SourceType: toolx.BuiltinToolCatalogServerCode, }, true } metadata, ok := h.toolMetadataBy[modelToolName] diff --git a/internal/ai/runtime/internal/impl/factory/agent_factory.go b/internal/ai/runtime/internal/impl/factory/agent_factory.go index 2f16f6d..5308095 100644 --- a/internal/ai/runtime/internal/impl/factory/agent_factory.go +++ b/internal/ai/runtime/internal/impl/factory/agent_factory.go @@ -79,9 +79,9 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m if toolCode == toolx.BuiltinToolSearchToolCode { serverCode = toolx.BuiltinToolCatalogServerCode toolName = toolx.BuiltinToolSearchToolName - } else if toolCode == toolx.BuiltinCreateTicketConfirmToolCode { - serverCode = toolx.BuiltinToolCatalogServerCode - toolName = toolx.BuiltinCreateTicketConfirmToolName + } else if toolCode == toolx.GraphCreateTicketConfirmToolCode { + serverCode = toolx.GraphToolCatalogServerCode + toolName = toolx.GraphCreateTicketConfirmToolName } toolMetadataBy[modelName] = einocallbacks.ToolMetadata{ ToolCode: toolCode, @@ -126,13 +126,13 @@ func buildAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.SkillD 3. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。 `)) } - if hasToolCode(extraToolCodes, toolx.BuiltinCreateTicketConfirmToolCode) { + if hasToolCode(extraToolCodes, toolx.GraphCreateTicketConfirmToolCode) { appendixParts = append(appendixParts, strings.TrimSpace(` -你可以在确认信息充分后调用 create_ticket_with_confirmation 工具来创建工单,但必须遵守以下规则: +你可以在确认信息充分后调用 create_ticket_with_confirmation 这个 Graph Tool 来创建工单,但必须遵守以下规则: 1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。 2. 调用前你必须已经整理出清晰的工单标题和问题描述;如果信息不足,先继续追问,不要过早调用。 3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation 工具,禁止直接口头宣称“已经创建工单”。 -4. 该工具会先向用户发起确认。用户确认后才会真正创建工单;用户取消则结束本次建单流程。 +4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正创建工单;用户取消则结束本次建单流程。 5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。 `)) } diff --git a/internal/ai/runtime/internal/impl/factory/tool_factory.go b/internal/ai/runtime/internal/impl/factory/tool_factory.go index d97a138..0e18da1 100644 --- a/internal/ai/runtime/internal/impl/factory/tool_factory.go +++ b/internal/ai/runtime/internal/impl/factory/tool_factory.go @@ -29,6 +29,7 @@ func (f *ToolFactory) BuildMCPTools(aiAgent *models.AIAgent) ([]impladapter.MCPT ret := make([]impladapter.MCPToolDefinition, 0, len(raw)) for _, item := range raw { toolCode := strings.TrimSpace(item.ToolCode) + toolCode = toolx.NormalizeToolCodeAlias(toolCode) if toolCode == "" { toolCode = toolx.BuildMCPToolCode(item.ServerCode, item.ToolName) } diff --git a/internal/ai/runtime/service.go b/internal/ai/runtime/service.go index a170e4b..76bf9b4 100644 --- a/internal/ai/runtime/service.go +++ b/internal/ai/runtime/service.go @@ -19,7 +19,7 @@ func newService() *service { return &service{ runtime: engine.NewService(), registry: registry.NewRegistry( - tools.NewCreateTicketConfirmTool(), + tools.NewCreateTicketGraphTool(), ), } } @@ -219,6 +219,7 @@ func parseSkillAllowedToolCodes(skill *models.SkillDefinition) []string { ret := make([]string, 0, len(items)) for _, item := range items { item = strings.TrimSpace(item) + item = toolx.NormalizeToolCodeAlias(item) if item == "" { continue } @@ -238,6 +239,7 @@ func parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string { ret := make([]string, 0, len(items)) for _, item := range items { toolCode := strings.TrimSpace(item.ToolCode) + toolCode = toolx.NormalizeToolCodeAlias(toolCode) if toolCode == "" { continue } @@ -258,6 +260,7 @@ func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinit skillSet := make(map[string]struct{}, len(skillAllowed)) for _, item := range skillAllowed { item = strings.TrimSpace(item) + item = toolx.NormalizeToolCodeAlias(item) if item == "" { continue } @@ -266,6 +269,7 @@ func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinit ret := make([]string, 0, len(agentAllowed)) for _, item := range agentAllowed { item = strings.TrimSpace(item) + item = toolx.NormalizeToolCodeAlias(item) if item == "" { continue } diff --git a/internal/ai/runtime/tools/create_ticket_confirm_tool.go b/internal/ai/runtime/tools/create_ticket_confirm_tool.go index fbdae33..7bc8db0 100644 --- a/internal/ai/runtime/tools/create_ticket_confirm_tool.go +++ b/internal/ai/runtime/tools/create_ticket_confirm_tool.go @@ -2,18 +2,13 @@ package tools import ( "context" - "encoding/json" "fmt" - "strings" + "cs-agent/internal/ai/runtime/graphs" "cs-agent/internal/ai/runtime/registry" "cs-agent/internal/models" - "cs-agent/internal/pkg/dto" - "cs-agent/internal/pkg/dto/request" "cs-agent/internal/pkg/toolx" - "cs-agent/internal/services" - componenttool "github.com/cloudwego/eino/components/tool" einotool "github.com/cloudwego/eino/components/tool" "github.com/cloudwego/eino/schema" einojsonschema "github.com/eino-contrib/jsonschema" @@ -21,59 +16,45 @@ import ( ) const ( - CreateTicketConfirmToolCode = toolx.BuiltinCreateTicketConfirmToolCode - CreateTicketConfirmToolName = toolx.BuiltinCreateTicketConfirmToolName + CreateTicketConfirmToolCode = toolx.GraphCreateTicketConfirmToolCode + CreateTicketConfirmToolName = toolx.GraphCreateTicketConfirmToolName ) -type CreateTicketConfirmState struct { - Request request.CreateTicketFromConversationRequest -} - -type CreateTicketConfirmInterruptInfo struct { - Type string `json:"type"` - Message string `json:"message"` -} - -func init() { - schema.RegisterName[CreateTicketConfirmState]("cs_agent_create_ticket_confirm_state") - schema.RegisterName[CreateTicketConfirmInterruptInfo]("cs_agent_create_ticket_confirm_interrupt_info") -} - -type CreateTicketConfirmTool struct { +type CreateTicketGraphTool struct { conversation *models.Conversation aiAgent *models.AIAgent } -func NewCreateTicketConfirmTool() *CreateTicketConfirmTool { - return &CreateTicketConfirmTool{} +func NewCreateTicketGraphTool() *CreateTicketGraphTool { + return &CreateTicketGraphTool{} } -func (t *CreateTicketConfirmTool) Name() string { +func (t *CreateTicketGraphTool) Name() string { return CreateTicketConfirmToolName } -func (t *CreateTicketConfirmTool) Code() string { +func (t *CreateTicketGraphTool) Code() string { return CreateTicketConfirmToolCode } -func (t *CreateTicketConfirmTool) Enabled(ctx registry.Context) bool { +func (t *CreateTicketGraphTool) Enabled(ctx registry.Context) bool { return ctx.Conversation != nil && ctx.AIAgent != nil } -func (t *CreateTicketConfirmTool) Build(ctx registry.Context) (einotool.BaseTool, error) { +func (t *CreateTicketGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error) { if !t.Enabled(ctx) { return nil, nil } - return &CreateTicketConfirmTool{ + return &CreateTicketGraphTool{ conversation: ctx.Conversation, aiAgent: ctx.AIAgent, }, nil } -func (t *CreateTicketConfirmTool) Info(ctx context.Context) (*schema.ToolInfo, error) { +func (t *CreateTicketGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{ Name: CreateTicketConfirmToolName, - Desc: "当用户明确希望创建工单、投诉单、报障单,且你已经整理出工单标题和描述后,调用此工具。该工具不会立即创建工单,而是会先向用户发起确认;只有用户确认后才真正创建。不要在信息不足时调用。", + Desc: "Graph Tool。用于封装建单参数整理、用户确认、真正创建工单和结果返回的确定性流程。仅在用户明确要求建单且标题、描述已整理清楚后调用。", ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{ Version: einojsonschema.Version, Type: "object", @@ -113,104 +94,15 @@ func (t *CreateTicketConfirmTool) Info(ctx context.Context) (*schema.ToolInfo, e )), }), Extra: map[string]any{ - "toolCode": CreateTicketConfirmToolCode, + "toolCode": CreateTicketConfirmToolCode, + "sourceType": "graph", }, }, nil } -func (t *CreateTicketConfirmTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) { +func (t *CreateTicketGraphTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) { if t == nil || t.conversation == nil || t.aiAgent == nil { - return "", fmt.Errorf("ticket confirmation tool not initialized") - } - wasInterrupted, hasState, state := componenttool.GetInterruptState[CreateTicketConfirmState](ctx) - if !wasInterrupted { - req, err := t.buildCreateRequest(argumentsInJSON) - if err != nil { - return "", err - } - info := CreateTicketConfirmInterruptInfo{ - Type: "ticket_creation_confirmation", - Message: t.buildConfirmationPrompt(req), - } - return "", componenttool.StatefulInterrupt(ctx, info, CreateTicketConfirmState{Request: req}) - } - if !hasState { - return "", fmt.Errorf("ticket confirmation state missing") - } - isResumeTarget, hasData, resumeText := componenttool.GetResumeContext[string](ctx) - if !isResumeTarget { - info := CreateTicketConfirmInterruptInfo{ - Type: "ticket_creation_confirmation", - Message: t.buildConfirmationPrompt(state.Request), - } - return "", componenttool.StatefulInterrupt(ctx, info, state) - } - if !hasData { - info := CreateTicketConfirmInterruptInfo{ - Type: "ticket_creation_confirmation", - Message: "请回复“确认”或“取消”。", - } - return "", componenttool.StatefulInterrupt(ctx, info, state) - } - decision := ParseConfirmationDecision(resumeText) - switch decision { - case DecisionConfirm: - item, err := services.TicketService.CreateFromConversation(state.Request, t.buildAIPrincipal()) - if err != nil { - return "", err - } - return fmt.Sprintf("工单已创建,工单号:%s,标题:%s。", strings.TrimSpace(item.TicketNo), strings.TrimSpace(item.Title)), nil - case DecisionCancel: - return "已取消本次工单创建。", nil - default: - info := CreateTicketConfirmInterruptInfo{ - Type: "ticket_creation_confirmation", - Message: "我需要你的明确确认,请直接回复“确认”或“取消”。", - } - return "", componenttool.StatefulInterrupt(ctx, info, state) - } -} - -func (t *CreateTicketConfirmTool) buildCreateRequest(argumentsInJSON string) (request.CreateTicketFromConversationRequest, error) { - req := request.CreateTicketFromConversationRequest{ - ConversationID: t.conversation.ID, - SyncToConversation: true, - } - raw := make(map[string]any) - if strings.TrimSpace(argumentsInJSON) != "" { - if err := json.Unmarshal([]byte(argumentsInJSON), &raw); err != nil { - return req, fmt.Errorf("invalid create ticket arguments: %w", err) - } - } - req.Title = strings.TrimSpace(getStringValue(raw, "title")) - req.Description = strings.TrimSpace(getStringValue(raw, "description")) - req.Priority = getInt64Value(raw, "priority") - req.Severity = int(getInt64Value(raw, "severity")) - if req.Title == "" { - req.Title = strings.TrimSpace(t.conversation.Subject) - } - if req.Description == "" { - req.Description = strings.TrimSpace(t.conversation.LastMessageSummary) - } - if strings.TrimSpace(req.Title) == "" { - return req, fmt.Errorf("ticket title is required") - } - return req, nil -} - -func (t *CreateTicketConfirmTool) buildConfirmationPrompt(req request.CreateTicketFromConversationRequest) string { - return fmt.Sprintf("我准备为你创建工单。\n标题:%s\n描述:%s\n请直接回复“确认”或“取消”。", - strings.TrimSpace(req.Title), strings.TrimSpace(req.Description)) -} - -func (t *CreateTicketConfirmTool) buildAIPrincipal() *dto.AuthPrincipal { - username := "AI" - if strings.TrimSpace(t.aiAgent.Name) != "" { - username = strings.TrimSpace(t.aiAgent.Name) - } - return &dto.AuthPrincipal{ - UserID: 0, - Username: username, - Nickname: username, + return "", fmt.Errorf("create ticket graph tool not initialized") } + return graphs.NewCreateTicketGraph(t.conversation, t.aiAgent).Run(ctx, argumentsInJSON) } diff --git a/internal/controllers/console/ai_agent_controller.go b/internal/controllers/console/ai_agent_controller.go index 6224b75..bdaf5c0 100644 --- a/internal/controllers/console/ai_agent_controller.go +++ b/internal/controllers/console/ai_agent_controller.go @@ -174,6 +174,7 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse { if toolCode == "" { toolCode = toolx.BuildMCPToolCode(tool.ServerCode, tool.ToolName) } + toolCode = toolx.NormalizeToolCodeAlias(toolCode) if toolx.IsAutoInjectedToolCode(toolCode) { continue } @@ -182,9 +183,9 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse { if toolCode == toolx.BuiltinToolSearchToolCode { serverCode = toolx.BuiltinToolCatalogServerCode toolName = toolx.BuiltinToolSearchToolName - } else if toolCode == toolx.BuiltinCreateTicketConfirmToolCode { - serverCode = toolx.BuiltinToolCatalogServerCode - toolName = toolx.BuiltinCreateTicketConfirmToolName + } else if toolCode == toolx.GraphCreateTicketConfirmToolCode { + serverCode = toolx.GraphToolCatalogServerCode + toolName = toolx.GraphCreateTicketConfirmToolName } else if parsedServerCode, parsedToolName := toolx.SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" { serverCode = parsedServerCode toolName = parsedToolName @@ -194,8 +195,8 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse { switch toolCode { case toolx.BuiltinToolSearchToolCode: title = toolx.BuiltinToolSearchToolTitle - case toolx.BuiltinCreateTicketConfirmToolCode: - title = toolx.BuiltinCreateTicketConfirmToolTitle + case toolx.GraphCreateTicketConfirmToolCode: + title = toolx.GraphCreateTicketConfirmToolTitle } } description := strings.TrimSpace(tool.Description) @@ -203,8 +204,8 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse { switch toolCode { case toolx.BuiltinToolSearchToolCode: description = toolx.BuiltinToolSearchToolDescription - case toolx.BuiltinCreateTicketConfirmToolCode: - description = toolx.BuiltinCreateTicketConfirmToolDescription + case toolx.GraphCreateTicketConfirmToolCode: + description = toolx.GraphCreateTicketConfirmToolDescription } } ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{ diff --git a/internal/pkg/toolx/builtin_tools.go b/internal/pkg/toolx/builtin_tools.go index 652143f..2d1d4a5 100644 --- a/internal/pkg/toolx/builtin_tools.go +++ b/internal/pkg/toolx/builtin_tools.go @@ -6,6 +6,11 @@ const ( BuiltinToolSearchToolName = "tool_search" BuiltinToolSearchToolTitle = "搜索并调用动态工具" BuiltinToolSearchToolDescription = "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。" + GraphToolCatalogServerCode = "graph" + GraphCreateTicketConfirmToolCode = "graph/create_ticket_with_confirmation" + GraphCreateTicketConfirmToolName = "create_ticket_with_confirmation" + GraphCreateTicketConfirmToolTitle = "创建工单确认流程" + GraphCreateTicketConfirmToolDescription = "Graph Tool。用于封装建单参数整理、用户确认、真正建单和结果返回的确定性流程。" BuiltinCreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation" BuiltinCreateTicketConfirmToolName = "create_ticket_with_confirmation" BuiltinCreateTicketConfirmToolTitle = "创建工单并发起确认" @@ -15,3 +20,12 @@ const ( func IsAutoInjectedToolCode(toolCode string) bool { return toolCode == BuiltinToolSearchToolCode } + +func NormalizeToolCodeAlias(toolCode string) string { + switch toolCode { + case BuiltinCreateTicketConfirmToolCode: + return GraphCreateTicketConfirmToolCode + default: + return toolCode + } +} diff --git a/internal/pkg/toolx/mcp_tool.go b/internal/pkg/toolx/mcp_tool.go index d955e30..7af1eb2 100644 --- a/internal/pkg/toolx/mcp_tool.go +++ b/internal/pkg/toolx/mcp_tool.go @@ -31,6 +31,7 @@ func SplitMCPToolCode(toolCode string) (string, string) { func NormalizeMCPToolRequest(item request.AIAgentMCPToolRequest) (request.AIAgentMCPToolRequest, error) { toolCode := strings.TrimSpace(item.ToolCode) + toolCode = NormalizeToolCodeAlias(toolCode) serverCode := strings.TrimSpace(item.ServerCode) toolName := strings.TrimSpace(item.ToolName) if toolCode != "" { diff --git a/internal/services/skill_definition_service.go b/internal/services/skill_definition_service.go index 3d801b6..5e8aa3c 100644 --- a/internal/services/skill_definition_service.go +++ b/internal/services/skill_definition_service.go @@ -10,6 +10,7 @@ import ( "cs-agent/internal/pkg/dto/request" "cs-agent/internal/pkg/enums" "cs-agent/internal/pkg/errorsx" + "cs-agent/internal/pkg/toolx" "cs-agent/internal/pkg/utils" "cs-agent/internal/repositories" @@ -210,6 +211,7 @@ func normalizeSkillStringArray(input []string) ([]string, error) { seen := make(map[string]struct{}, len(ret)) for _, item := range ret { item = strings.TrimSpace(item) + item = toolx.NormalizeToolCodeAlias(item) if item == "" { continue } diff --git a/internal/services/tool_catalog_service.go b/internal/services/tool_catalog_service.go index 768bc09..7da344a 100644 --- a/internal/services/tool_catalog_service.go +++ b/internal/services/tool_catalog_service.go @@ -35,13 +35,13 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog cfg := config.Current() ret := make([]MCPToolCatalogItem, 0, 2) ret = append(ret, MCPToolCatalogItem{ - ToolCode: toolx.BuiltinCreateTicketConfirmToolCode, - ServerCode: toolx.BuiltinToolCatalogServerCode, - ToolName: toolx.BuiltinCreateTicketConfirmToolName, - SourceType: toolx.BuiltinToolCatalogServerCode, + ToolCode: toolx.GraphCreateTicketConfirmToolCode, + ServerCode: toolx.GraphToolCatalogServerCode, + ToolName: toolx.GraphCreateTicketConfirmToolName, + SourceType: toolx.GraphToolCatalogServerCode, AutoInjected: false, - Title: toolx.BuiltinCreateTicketConfirmToolTitle, - Description: toolx.BuiltinCreateTicketConfirmToolDescription, + Title: toolx.GraphCreateTicketConfirmToolTitle, + Description: toolx.GraphCreateTicketConfirmToolDescription, }) if !cfg.MCP.Enabled { return ret, nil @@ -96,7 +96,7 @@ func (s *toolCatalogService) ValidateToolCode(toolCode string) error { return errorsx.InvalidParam("toolCode不能为空") } switch toolCode { - case toolx.BuiltinToolSearchToolCode, toolx.BuiltinCreateTicketConfirmToolCode: + case toolx.BuiltinToolSearchToolCode, toolx.BuiltinCreateTicketConfirmToolCode, toolx.GraphCreateTicketConfirmToolCode: return nil } serverCode, toolName := toolx.SplitMCPToolCode(toolCode)