feat: refactor ticket creation process to use Graph Tool for confirmation and state management
This commit is contained in:
@@ -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 ""
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ type ToolMetadata struct {
|
|||||||
ToolCode string
|
ToolCode string
|
||||||
ServerCode string
|
ServerCode string
|
||||||
ToolName string
|
ToolName string
|
||||||
|
SourceType string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeTraceHandler struct {
|
type RuntimeTraceHandler struct {
|
||||||
@@ -58,6 +59,17 @@ func (h *RuntimeTraceHandler) WrapInvokableToolCall(_ context.Context, endpoint
|
|||||||
item.ErrorMessage = err.Error()
|
item.ErrorMessage = err.Error()
|
||||||
}
|
}
|
||||||
h.collector.AddToolItem(item)
|
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 {
|
if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearchToolCode {
|
||||||
h.collector.AddToolSearchItem(h.buildToolSearchTraceItem(argumentsInJSON, result, err))
|
h.collector.AddToolSearchItem(h.buildToolSearchTraceItem(argumentsInJSON, result, err))
|
||||||
}
|
}
|
||||||
@@ -78,6 +90,7 @@ func (h *RuntimeTraceHandler) resolveToolMetadata(modelToolName string) (ToolMet
|
|||||||
ToolCode: toolx.BuiltinToolSearchToolCode,
|
ToolCode: toolx.BuiltinToolSearchToolCode,
|
||||||
ServerCode: toolx.BuiltinToolCatalogServerCode,
|
ServerCode: toolx.BuiltinToolCatalogServerCode,
|
||||||
ToolName: toolx.BuiltinToolSearchToolName,
|
ToolName: toolx.BuiltinToolSearchToolName,
|
||||||
|
SourceType: toolx.BuiltinToolCatalogServerCode,
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
metadata, ok := h.toolMetadataBy[modelToolName]
|
metadata, ok := h.toolMetadataBy[modelToolName]
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m
|
|||||||
if toolCode == toolx.BuiltinToolSearchToolCode {
|
if toolCode == toolx.BuiltinToolSearchToolCode {
|
||||||
serverCode = toolx.BuiltinToolCatalogServerCode
|
serverCode = toolx.BuiltinToolCatalogServerCode
|
||||||
toolName = toolx.BuiltinToolSearchToolName
|
toolName = toolx.BuiltinToolSearchToolName
|
||||||
} else if toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
} else if toolCode == toolx.GraphCreateTicketConfirmToolCode {
|
||||||
serverCode = toolx.BuiltinToolCatalogServerCode
|
serverCode = toolx.GraphToolCatalogServerCode
|
||||||
toolName = toolx.BuiltinCreateTicketConfirmToolName
|
toolName = toolx.GraphCreateTicketConfirmToolName
|
||||||
}
|
}
|
||||||
toolMetadataBy[modelName] = einocallbacks.ToolMetadata{
|
toolMetadataBy[modelName] = einocallbacks.ToolMetadata{
|
||||||
ToolCode: toolCode,
|
ToolCode: toolCode,
|
||||||
@@ -126,13 +126,13 @@ func buildAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.SkillD
|
|||||||
3. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。
|
3. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。
|
||||||
`))
|
`))
|
||||||
}
|
}
|
||||||
if hasToolCode(extraToolCodes, toolx.BuiltinCreateTicketConfirmToolCode) {
|
if hasToolCode(extraToolCodes, toolx.GraphCreateTicketConfirmToolCode) {
|
||||||
appendixParts = append(appendixParts, strings.TrimSpace(`
|
appendixParts = append(appendixParts, strings.TrimSpace(`
|
||||||
你可以在确认信息充分后调用 create_ticket_with_confirmation 工具来创建工单,但必须遵守以下规则:
|
你可以在确认信息充分后调用 create_ticket_with_confirmation 这个 Graph Tool 来创建工单,但必须遵守以下规则:
|
||||||
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。
|
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。
|
||||||
2. 调用前你必须已经整理出清晰的工单标题和问题描述;如果信息不足,先继续追问,不要过早调用。
|
2. 调用前你必须已经整理出清晰的工单标题和问题描述;如果信息不足,先继续追问,不要过早调用。
|
||||||
3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation 工具,禁止直接口头宣称“已经创建工单”。
|
3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation 工具,禁止直接口头宣称“已经创建工单”。
|
||||||
4. 该工具会先向用户发起确认。用户确认后才会真正创建工单;用户取消则结束本次建单流程。
|
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正创建工单;用户取消则结束本次建单流程。
|
||||||
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
|
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
|
||||||
`))
|
`))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func (f *ToolFactory) BuildMCPTools(aiAgent *models.AIAgent) ([]impladapter.MCPT
|
|||||||
ret := make([]impladapter.MCPToolDefinition, 0, len(raw))
|
ret := make([]impladapter.MCPToolDefinition, 0, len(raw))
|
||||||
for _, item := range raw {
|
for _, item := range raw {
|
||||||
toolCode := strings.TrimSpace(item.ToolCode)
|
toolCode := strings.TrimSpace(item.ToolCode)
|
||||||
|
toolCode = toolx.NormalizeToolCodeAlias(toolCode)
|
||||||
if toolCode == "" {
|
if toolCode == "" {
|
||||||
toolCode = toolx.BuildMCPToolCode(item.ServerCode, item.ToolName)
|
toolCode = toolx.BuildMCPToolCode(item.ServerCode, item.ToolName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func newService() *service {
|
|||||||
return &service{
|
return &service{
|
||||||
runtime: engine.NewService(),
|
runtime: engine.NewService(),
|
||||||
registry: registry.NewRegistry(
|
registry: registry.NewRegistry(
|
||||||
tools.NewCreateTicketConfirmTool(),
|
tools.NewCreateTicketGraphTool(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,6 +219,7 @@ func parseSkillAllowedToolCodes(skill *models.SkillDefinition) []string {
|
|||||||
ret := make([]string, 0, len(items))
|
ret := make([]string, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
item = strings.TrimSpace(item)
|
item = strings.TrimSpace(item)
|
||||||
|
item = toolx.NormalizeToolCodeAlias(item)
|
||||||
if item == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -238,6 +239,7 @@ func parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string {
|
|||||||
ret := make([]string, 0, len(items))
|
ret := make([]string, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
toolCode := strings.TrimSpace(item.ToolCode)
|
toolCode := strings.TrimSpace(item.ToolCode)
|
||||||
|
toolCode = toolx.NormalizeToolCodeAlias(toolCode)
|
||||||
if toolCode == "" {
|
if toolCode == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -258,6 +260,7 @@ func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinit
|
|||||||
skillSet := make(map[string]struct{}, len(skillAllowed))
|
skillSet := make(map[string]struct{}, len(skillAllowed))
|
||||||
for _, item := range skillAllowed {
|
for _, item := range skillAllowed {
|
||||||
item = strings.TrimSpace(item)
|
item = strings.TrimSpace(item)
|
||||||
|
item = toolx.NormalizeToolCodeAlias(item)
|
||||||
if item == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -266,6 +269,7 @@ func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinit
|
|||||||
ret := make([]string, 0, len(agentAllowed))
|
ret := make([]string, 0, len(agentAllowed))
|
||||||
for _, item := range agentAllowed {
|
for _, item := range agentAllowed {
|
||||||
item = strings.TrimSpace(item)
|
item = strings.TrimSpace(item)
|
||||||
|
item = toolx.NormalizeToolCodeAlias(item)
|
||||||
if item == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,13 @@ package tools
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"cs-agent/internal/ai/runtime/graphs"
|
||||||
"cs-agent/internal/ai/runtime/registry"
|
"cs-agent/internal/ai/runtime/registry"
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
"cs-agent/internal/pkg/dto"
|
|
||||||
"cs-agent/internal/pkg/dto/request"
|
|
||||||
"cs-agent/internal/pkg/toolx"
|
"cs-agent/internal/pkg/toolx"
|
||||||
"cs-agent/internal/services"
|
|
||||||
|
|
||||||
componenttool "github.com/cloudwego/eino/components/tool"
|
|
||||||
einotool "github.com/cloudwego/eino/components/tool"
|
einotool "github.com/cloudwego/eino/components/tool"
|
||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
einojsonschema "github.com/eino-contrib/jsonschema"
|
einojsonschema "github.com/eino-contrib/jsonschema"
|
||||||
@@ -21,59 +16,45 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CreateTicketConfirmToolCode = toolx.BuiltinCreateTicketConfirmToolCode
|
CreateTicketConfirmToolCode = toolx.GraphCreateTicketConfirmToolCode
|
||||||
CreateTicketConfirmToolName = toolx.BuiltinCreateTicketConfirmToolName
|
CreateTicketConfirmToolName = toolx.GraphCreateTicketConfirmToolName
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreateTicketConfirmState struct {
|
type CreateTicketGraphTool 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 {
|
|
||||||
conversation *models.Conversation
|
conversation *models.Conversation
|
||||||
aiAgent *models.AIAgent
|
aiAgent *models.AIAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCreateTicketConfirmTool() *CreateTicketConfirmTool {
|
func NewCreateTicketGraphTool() *CreateTicketGraphTool {
|
||||||
return &CreateTicketConfirmTool{}
|
return &CreateTicketGraphTool{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CreateTicketConfirmTool) Name() string {
|
func (t *CreateTicketGraphTool) Name() string {
|
||||||
return CreateTicketConfirmToolName
|
return CreateTicketConfirmToolName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CreateTicketConfirmTool) Code() string {
|
func (t *CreateTicketGraphTool) Code() string {
|
||||||
return CreateTicketConfirmToolCode
|
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
|
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) {
|
if !t.Enabled(ctx) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return &CreateTicketConfirmTool{
|
return &CreateTicketGraphTool{
|
||||||
conversation: ctx.Conversation,
|
conversation: ctx.Conversation,
|
||||||
aiAgent: ctx.AIAgent,
|
aiAgent: ctx.AIAgent,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CreateTicketConfirmTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
func (t *CreateTicketGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||||
return &schema.ToolInfo{
|
return &schema.ToolInfo{
|
||||||
Name: CreateTicketConfirmToolName,
|
Name: CreateTicketConfirmToolName,
|
||||||
Desc: "当用户明确希望创建工单、投诉单、报障单,且你已经整理出工单标题和描述后,调用此工具。该工具不会立即创建工单,而是会先向用户发起确认;只有用户确认后才真正创建。不要在信息不足时调用。",
|
Desc: "Graph Tool。用于封装建单参数整理、用户确认、真正创建工单和结果返回的确定性流程。仅在用户明确要求建单且标题、描述已整理清楚后调用。",
|
||||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||||
Version: einojsonschema.Version,
|
Version: einojsonschema.Version,
|
||||||
Type: "object",
|
Type: "object",
|
||||||
@@ -114,103 +95,14 @@ func (t *CreateTicketConfirmTool) Info(ctx context.Context) (*schema.ToolInfo, e
|
|||||||
}),
|
}),
|
||||||
Extra: map[string]any{
|
Extra: map[string]any{
|
||||||
"toolCode": CreateTicketConfirmToolCode,
|
"toolCode": CreateTicketConfirmToolCode,
|
||||||
|
"sourceType": "graph",
|
||||||
},
|
},
|
||||||
}, nil
|
}, 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 {
|
if t == nil || t.conversation == nil || t.aiAgent == nil {
|
||||||
return "", fmt.Errorf("ticket confirmation tool not initialized")
|
return "", fmt.Errorf("create ticket graph 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 graphs.NewCreateTicketGraph(t.conversation, t.aiAgent).Run(ctx, argumentsInJSON)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
if toolCode == "" {
|
if toolCode == "" {
|
||||||
toolCode = toolx.BuildMCPToolCode(tool.ServerCode, tool.ToolName)
|
toolCode = toolx.BuildMCPToolCode(tool.ServerCode, tool.ToolName)
|
||||||
}
|
}
|
||||||
|
toolCode = toolx.NormalizeToolCodeAlias(toolCode)
|
||||||
if toolx.IsAutoInjectedToolCode(toolCode) {
|
if toolx.IsAutoInjectedToolCode(toolCode) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -182,9 +183,9 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
if toolCode == toolx.BuiltinToolSearchToolCode {
|
if toolCode == toolx.BuiltinToolSearchToolCode {
|
||||||
serverCode = toolx.BuiltinToolCatalogServerCode
|
serverCode = toolx.BuiltinToolCatalogServerCode
|
||||||
toolName = toolx.BuiltinToolSearchToolName
|
toolName = toolx.BuiltinToolSearchToolName
|
||||||
} else if toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
} else if toolCode == toolx.GraphCreateTicketConfirmToolCode {
|
||||||
serverCode = toolx.BuiltinToolCatalogServerCode
|
serverCode = toolx.GraphToolCatalogServerCode
|
||||||
toolName = toolx.BuiltinCreateTicketConfirmToolName
|
toolName = toolx.GraphCreateTicketConfirmToolName
|
||||||
} 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
|
||||||
@@ -194,8 +195,8 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
switch toolCode {
|
switch toolCode {
|
||||||
case toolx.BuiltinToolSearchToolCode:
|
case toolx.BuiltinToolSearchToolCode:
|
||||||
title = toolx.BuiltinToolSearchToolTitle
|
title = toolx.BuiltinToolSearchToolTitle
|
||||||
case toolx.BuiltinCreateTicketConfirmToolCode:
|
case toolx.GraphCreateTicketConfirmToolCode:
|
||||||
title = toolx.BuiltinCreateTicketConfirmToolTitle
|
title = toolx.GraphCreateTicketConfirmToolTitle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
description := strings.TrimSpace(tool.Description)
|
description := strings.TrimSpace(tool.Description)
|
||||||
@@ -203,8 +204,8 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
switch toolCode {
|
switch toolCode {
|
||||||
case toolx.BuiltinToolSearchToolCode:
|
case toolx.BuiltinToolSearchToolCode:
|
||||||
description = toolx.BuiltinToolSearchToolDescription
|
description = toolx.BuiltinToolSearchToolDescription
|
||||||
case toolx.BuiltinCreateTicketConfirmToolCode:
|
case toolx.GraphCreateTicketConfirmToolCode:
|
||||||
description = toolx.BuiltinCreateTicketConfirmToolDescription
|
description = toolx.GraphCreateTicketConfirmToolDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{
|
ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ const (
|
|||||||
BuiltinToolSearchToolName = "tool_search"
|
BuiltinToolSearchToolName = "tool_search"
|
||||||
BuiltinToolSearchToolTitle = "搜索并调用动态工具"
|
BuiltinToolSearchToolTitle = "搜索并调用动态工具"
|
||||||
BuiltinToolSearchToolDescription = "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。"
|
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"
|
BuiltinCreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation"
|
||||||
BuiltinCreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
BuiltinCreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
||||||
BuiltinCreateTicketConfirmToolTitle = "创建工单并发起确认"
|
BuiltinCreateTicketConfirmToolTitle = "创建工单并发起确认"
|
||||||
@@ -15,3 +20,12 @@ const (
|
|||||||
func IsAutoInjectedToolCode(toolCode string) bool {
|
func IsAutoInjectedToolCode(toolCode string) bool {
|
||||||
return toolCode == BuiltinToolSearchToolCode
|
return toolCode == BuiltinToolSearchToolCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NormalizeToolCodeAlias(toolCode string) string {
|
||||||
|
switch toolCode {
|
||||||
|
case BuiltinCreateTicketConfirmToolCode:
|
||||||
|
return GraphCreateTicketConfirmToolCode
|
||||||
|
default:
|
||||||
|
return toolCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func SplitMCPToolCode(toolCode string) (string, string) {
|
|||||||
|
|
||||||
func NormalizeMCPToolRequest(item request.AIAgentMCPToolRequest) (request.AIAgentMCPToolRequest, error) {
|
func NormalizeMCPToolRequest(item request.AIAgentMCPToolRequest) (request.AIAgentMCPToolRequest, error) {
|
||||||
toolCode := strings.TrimSpace(item.ToolCode)
|
toolCode := strings.TrimSpace(item.ToolCode)
|
||||||
|
toolCode = NormalizeToolCodeAlias(toolCode)
|
||||||
serverCode := strings.TrimSpace(item.ServerCode)
|
serverCode := strings.TrimSpace(item.ServerCode)
|
||||||
toolName := strings.TrimSpace(item.ToolName)
|
toolName := strings.TrimSpace(item.ToolName)
|
||||||
if toolCode != "" {
|
if toolCode != "" {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"cs-agent/internal/pkg/dto/request"
|
"cs-agent/internal/pkg/dto/request"
|
||||||
"cs-agent/internal/pkg/enums"
|
"cs-agent/internal/pkg/enums"
|
||||||
"cs-agent/internal/pkg/errorsx"
|
"cs-agent/internal/pkg/errorsx"
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
"cs-agent/internal/pkg/utils"
|
"cs-agent/internal/pkg/utils"
|
||||||
"cs-agent/internal/repositories"
|
"cs-agent/internal/repositories"
|
||||||
|
|
||||||
@@ -210,6 +211,7 @@ func normalizeSkillStringArray(input []string) ([]string, error) {
|
|||||||
seen := make(map[string]struct{}, len(ret))
|
seen := make(map[string]struct{}, len(ret))
|
||||||
for _, item := range ret {
|
for _, item := range ret {
|
||||||
item = strings.TrimSpace(item)
|
item = strings.TrimSpace(item)
|
||||||
|
item = toolx.NormalizeToolCodeAlias(item)
|
||||||
if item == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog
|
|||||||
cfg := config.Current()
|
cfg := config.Current()
|
||||||
ret := make([]MCPToolCatalogItem, 0, 2)
|
ret := make([]MCPToolCatalogItem, 0, 2)
|
||||||
ret = append(ret, MCPToolCatalogItem{
|
ret = append(ret, MCPToolCatalogItem{
|
||||||
ToolCode: toolx.BuiltinCreateTicketConfirmToolCode,
|
ToolCode: toolx.GraphCreateTicketConfirmToolCode,
|
||||||
ServerCode: toolx.BuiltinToolCatalogServerCode,
|
ServerCode: toolx.GraphToolCatalogServerCode,
|
||||||
ToolName: toolx.BuiltinCreateTicketConfirmToolName,
|
ToolName: toolx.GraphCreateTicketConfirmToolName,
|
||||||
SourceType: toolx.BuiltinToolCatalogServerCode,
|
SourceType: toolx.GraphToolCatalogServerCode,
|
||||||
AutoInjected: false,
|
AutoInjected: false,
|
||||||
Title: toolx.BuiltinCreateTicketConfirmToolTitle,
|
Title: toolx.GraphCreateTicketConfirmToolTitle,
|
||||||
Description: toolx.BuiltinCreateTicketConfirmToolDescription,
|
Description: toolx.GraphCreateTicketConfirmToolDescription,
|
||||||
})
|
})
|
||||||
if !cfg.MCP.Enabled {
|
if !cfg.MCP.Enabled {
|
||||||
return ret, nil
|
return ret, nil
|
||||||
@@ -96,7 +96,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:
|
case toolx.BuiltinToolSearchToolCode, toolx.BuiltinCreateTicketConfirmToolCode, toolx.GraphCreateTicketConfirmToolCode:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||||
|
|||||||
Reference in New Issue
Block a user