feat: refactor tool catalog to build static tools dynamically and improve tool code normalization
This commit is contained in:
@@ -16,16 +16,29 @@ type toolCatalog struct {
|
|||||||
|
|
||||||
func newToolCatalog() *toolCatalog {
|
func newToolCatalog() *toolCatalog {
|
||||||
return &toolCatalog{
|
return &toolCatalog{
|
||||||
registry: registry.NewRegistry(
|
registry: registry.NewRegistry(buildRuntimeStaticTools()...),
|
||||||
tools.NewTriageServiceRequestTool(),
|
|
||||||
tools.NewAnalyzeConversationTool(),
|
|
||||||
tools.NewPrepareTicketDraftTool(),
|
|
||||||
tools.NewCreateTicketGraphTool(),
|
|
||||||
tools.NewHandoffGraphTool(),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildRuntimeStaticTools() []registry.Tool {
|
||||||
|
builders := map[string]func() registry.Tool{
|
||||||
|
toolx.GraphTriageServiceRequest.Code: func() registry.Tool { return tools.NewTriageServiceRequestTool() },
|
||||||
|
toolx.GraphAnalyzeConversation.Code: func() registry.Tool { return tools.NewAnalyzeConversationTool() },
|
||||||
|
toolx.GraphPrepareTicketDraft.Code: func() registry.Tool { return tools.NewPrepareTicketDraftTool() },
|
||||||
|
toolx.GraphCreateTicketConfirm.Code: func() registry.Tool { return tools.NewCreateTicketGraphTool() },
|
||||||
|
toolx.GraphHandoffConversation.Code: func() registry.Tool { return tools.NewHandoffGraphTool() },
|
||||||
|
}
|
||||||
|
ret := make([]registry.Tool, 0, len(builders))
|
||||||
|
for _, spec := range toolx.ListRuntimeStaticToolSpecs() {
|
||||||
|
build := builders[strings.TrimSpace(spec.Code)]
|
||||||
|
if build == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, build())
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
func (c *toolCatalog) resolveForRun(req *Request) (*registry.ToolSet, error) {
|
func (c *toolCatalog) resolveForRun(req *Request) (*registry.ToolSet, error) {
|
||||||
if req == nil || req.ToolSet != nil || c == nil || c.registry == nil {
|
if req == nil || req.ToolSet != nil || c == nil || c.registry == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -63,7 +76,7 @@ func (c *toolCatalog) parseSkillAllowedToolCodes(skill *models.SkillDefinition)
|
|||||||
if err := json.Unmarshal([]byte(raw), &items); err != nil {
|
if err := json.Unmarshal([]byte(raw), &items); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return normalizeAllowedToolCodes(items)
|
return toolx.NormalizeToolCodes(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *toolCatalog) parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string {
|
func (c *toolCatalog) parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string {
|
||||||
@@ -78,48 +91,9 @@ func (c *toolCatalog) parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []stri
|
|||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
ret = append(ret, item.ToolCode)
|
ret = append(ret, item.ToolCode)
|
||||||
}
|
}
|
||||||
return normalizeAllowedToolCodes(ret)
|
return toolx.NormalizeToolCodes(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *toolCatalog) resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinition) []string {
|
func (c *toolCatalog) resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinition) []string {
|
||||||
agentAllowed := c.parseAgentAllowedToolCodes(aiAgent)
|
return toolx.IntersectToolCodes(c.parseAgentAllowedToolCodes(aiAgent), c.parseSkillAllowedToolCodes(skill))
|
||||||
skillAllowed := c.parseSkillAllowedToolCodes(skill)
|
|
||||||
switch {
|
|
||||||
case len(agentAllowed) == 0:
|
|
||||||
return skillAllowed
|
|
||||||
case len(skillAllowed) == 0:
|
|
||||||
return agentAllowed
|
|
||||||
default:
|
|
||||||
skillSet := make(map[string]struct{}, len(skillAllowed))
|
|
||||||
for _, item := range skillAllowed {
|
|
||||||
skillSet[item] = struct{}{}
|
|
||||||
}
|
|
||||||
ret := make([]string, 0, len(agentAllowed))
|
|
||||||
for _, item := range agentAllowed {
|
|
||||||
if _, ok := skillSet[item]; ok {
|
|
||||||
ret = append(ret, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeAllowedToolCodes(items []string) []string {
|
|
||||||
if len(items) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ret := make([]string, 0, len(items))
|
|
||||||
seen := make(map[string]struct{}, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
item = toolx.NormalizeToolCodeAlias(strings.TrimSpace(item))
|
|
||||||
if item == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[item]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[item] = struct{}{}
|
|
||||||
ret = append(ret, item)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeAllowedToolCodes(t *testing.T) {
|
func TestNormalizeAllowedToolCodes(t *testing.T) {
|
||||||
ret := normalizeAllowedToolCodes([]string{
|
ret := toolx.NormalizeToolCodes([]string{
|
||||||
" ",
|
" ",
|
||||||
"graph/create_ticket_with_confirmation",
|
"graph/create_ticket_with_confirmation",
|
||||||
"builtin/create_ticket_with_confirmation",
|
"builtin/create_ticket_with_confirmation",
|
||||||
@@ -52,3 +53,10 @@ func TestToolCatalogResolveAllowedToolCodesFallsBackWhenSkillEmpty(t *testing.T)
|
|||||||
t.Fatalf("expected 2 tool codes, got %d: %#v", len(ret), ret)
|
t.Fatalf("expected 2 tool codes, got %d: %#v", len(ret), ret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildRuntimeStaticTools(t *testing.T) {
|
||||||
|
ret := buildRuntimeStaticTools()
|
||||||
|
if len(ret) != len(toolx.ListRuntimeStaticToolSpecs()) {
|
||||||
|
t.Fatalf("expected %d runtime static tools, got %d", len(toolx.ListRuntimeStaticToolSpecs()), len(ret))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,15 +3,17 @@ package toolx
|
|||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
type ToolSpec struct {
|
type ToolSpec struct {
|
||||||
Code string
|
Code string
|
||||||
ServerCode string
|
ServerCode string
|
||||||
Name string
|
Name string
|
||||||
Title string
|
Title string
|
||||||
Description string
|
Description string
|
||||||
SourceType string
|
SourceType string
|
||||||
AutoInjected bool
|
AutoInjected bool
|
||||||
Aliases []string
|
DirectAccess bool
|
||||||
Appendix string
|
RuntimeStatic bool
|
||||||
|
Aliases []string
|
||||||
|
Appendix string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -23,6 +25,7 @@ var (
|
|||||||
Description: "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。",
|
Description: "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。",
|
||||||
SourceType: "builtin",
|
SourceType: "builtin",
|
||||||
AutoInjected: true,
|
AutoInjected: true,
|
||||||
|
DirectAccess: true,
|
||||||
Appendix: strings.TrimSpace(`
|
Appendix: strings.TrimSpace(`
|
||||||
当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则:
|
当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则:
|
||||||
1. 先调用 tool_search 搜索需要的动态工具,再继续使用已选中的真实工具。
|
1. 先调用 tool_search 搜索需要的动态工具,再继续使用已选中的真实工具。
|
||||||
@@ -40,12 +43,13 @@ var (
|
|||||||
AutoInjected: true,
|
AutoInjected: true,
|
||||||
}
|
}
|
||||||
GraphTriageServiceRequest = ToolSpec{
|
GraphTriageServiceRequest = ToolSpec{
|
||||||
Code: "graph/triage_service_request",
|
Code: "graph/triage_service_request",
|
||||||
ServerCode: "graph",
|
ServerCode: "graph",
|
||||||
Name: "triage_service_request",
|
Name: "triage_service_request",
|
||||||
Title: "升级分流判断",
|
Title: "升级分流判断",
|
||||||
Description: "Graph Tool。用于综合分析当前对话,判断应继续解答、整理工单草稿还是转人工,并在需要建单时一并整理工单草稿。",
|
Description: "Graph Tool。用于综合分析当前对话,判断应继续解答、整理工单草稿还是转人工,并在需要建单时一并整理工单草稿。",
|
||||||
SourceType: "graph",
|
SourceType: "graph",
|
||||||
|
RuntimeStatic: true,
|
||||||
Appendix: strings.TrimSpace(`
|
Appendix: strings.TrimSpace(`
|
||||||
当你需要判断“继续解答 / 建单 / 转人工”这类复杂升级路径时,优先先调用 triage_service_request 这个 Graph Tool,并遵守以下规则:
|
当你需要判断“继续解答 / 建单 / 转人工”这类复杂升级路径时,优先先调用 triage_service_request 这个 Graph Tool,并遵守以下规则:
|
||||||
1. 该工具会综合当前对话输出 recommendedAction,并在需要建单时附带 ticketDraft。
|
1. 该工具会综合当前对话输出 recommendedAction,并在需要建单时附带 ticketDraft。
|
||||||
@@ -56,12 +60,13 @@ var (
|
|||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
GraphAnalyzeConversation = ToolSpec{
|
GraphAnalyzeConversation = ToolSpec{
|
||||||
Code: "graph/analyze_conversation",
|
Code: "graph/analyze_conversation",
|
||||||
ServerCode: "graph",
|
ServerCode: "graph",
|
||||||
Name: "analyze_conversation",
|
Name: "analyze_conversation",
|
||||||
Title: "分析对话风险与摘要",
|
Title: "分析对话风险与摘要",
|
||||||
Description: "Graph Tool。用于整理当前对话摘要、识别风险信号,并给出继续解答、建单或转人工的建议。",
|
Description: "Graph Tool。用于整理当前对话摘要、识别风险信号,并给出继续解答、建单或转人工的建议。",
|
||||||
SourceType: "graph",
|
SourceType: "graph",
|
||||||
|
RuntimeStatic: true,
|
||||||
Appendix: strings.TrimSpace(`
|
Appendix: strings.TrimSpace(`
|
||||||
当对话可能涉及投诉升级、退款赔偿、明显负面情绪、是否要建单、是否要转人工等复杂判断时,优先调用 analyze_conversation 这个 Graph Tool,并遵守以下规则:
|
当对话可能涉及投诉升级、退款赔偿、明显负面情绪、是否要建单、是否要转人工等复杂判断时,优先调用 analyze_conversation 这个 Graph Tool,并遵守以下规则:
|
||||||
1. 该工具用于输出结构化摘要、风险信号和下一步建议,不代表实际已经建单或转人工。
|
1. 该工具用于输出结构化摘要、风险信号和下一步建议,不代表实际已经建单或转人工。
|
||||||
@@ -71,12 +76,13 @@ var (
|
|||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
GraphPrepareTicketDraft = ToolSpec{
|
GraphPrepareTicketDraft = ToolSpec{
|
||||||
Code: "graph/prepare_ticket_draft",
|
Code: "graph/prepare_ticket_draft",
|
||||||
ServerCode: "graph",
|
ServerCode: "graph",
|
||||||
Name: "prepare_ticket_draft",
|
Name: "prepare_ticket_draft",
|
||||||
Title: "整理工单草稿",
|
Title: "整理工单草稿",
|
||||||
Description: "Graph Tool。用于根据当前会话和已收集信息整理工单草稿,输出建议标题、描述、缺失字段和追问建议。",
|
Description: "Graph Tool。用于根据当前会话和已收集信息整理工单草稿,输出建议标题、描述、缺失字段和追问建议。",
|
||||||
SourceType: "graph",
|
SourceType: "graph",
|
||||||
|
RuntimeStatic: true,
|
||||||
Appendix: strings.TrimSpace(`
|
Appendix: strings.TrimSpace(`
|
||||||
当用户已经表达了建单、投诉、报障、售后处理等诉求,但工单标题、描述或问题整理还比较散乱时,优先调用 prepare_ticket_draft 这个 Graph Tool,并遵守以下规则:
|
当用户已经表达了建单、投诉、报障、售后处理等诉求,但工单标题、描述或问题整理还比较散乱时,优先调用 prepare_ticket_draft 这个 Graph Tool,并遵守以下规则:
|
||||||
1. 该工具用于整理工单草稿,会返回建议标题、建议描述、缺失字段和追问建议。
|
1. 该工具用于整理工单草稿,会返回建议标题、建议描述、缺失字段和追问建议。
|
||||||
@@ -86,13 +92,15 @@ var (
|
|||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
GraphCreateTicketConfirm = ToolSpec{
|
GraphCreateTicketConfirm = ToolSpec{
|
||||||
Code: "graph/create_ticket_with_confirmation",
|
Code: "graph/create_ticket_with_confirmation",
|
||||||
ServerCode: "graph",
|
ServerCode: "graph",
|
||||||
Name: "create_ticket_with_confirmation",
|
Name: "create_ticket_with_confirmation",
|
||||||
Title: "创建工单确认流程",
|
Title: "创建工单确认流程",
|
||||||
Description: "Graph Tool。用于封装建单参数整理、用户确认、真正建单和结果返回的确定性流程。",
|
Description: "Graph Tool。用于封装建单参数整理、用户确认、真正建单和结果返回的确定性流程。",
|
||||||
SourceType: "graph",
|
SourceType: "graph",
|
||||||
Aliases: []string{"builtin/create_ticket_with_confirmation"},
|
DirectAccess: true,
|
||||||
|
RuntimeStatic: true,
|
||||||
|
Aliases: []string{"builtin/create_ticket_with_confirmation"},
|
||||||
Appendix: strings.TrimSpace(`
|
Appendix: strings.TrimSpace(`
|
||||||
你可以在确认信息充分后调用 create_ticket_with_confirmation 这个 Graph Tool 来创建工单,但必须遵守以下规则:
|
你可以在确认信息充分后调用 create_ticket_with_confirmation 这个 Graph Tool 来创建工单,但必须遵守以下规则:
|
||||||
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。
|
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。
|
||||||
@@ -103,12 +111,14 @@ var (
|
|||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
GraphHandoffConversation = ToolSpec{
|
GraphHandoffConversation = ToolSpec{
|
||||||
Code: "graph/handoff_to_human",
|
Code: "graph/handoff_to_human",
|
||||||
ServerCode: "graph",
|
ServerCode: "graph",
|
||||||
Name: "handoff_to_human",
|
Name: "handoff_to_human",
|
||||||
Title: "转人工确认流程",
|
Title: "转人工确认流程",
|
||||||
Description: "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。",
|
Description: "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。",
|
||||||
SourceType: "graph",
|
SourceType: "graph",
|
||||||
|
DirectAccess: true,
|
||||||
|
RuntimeStatic: true,
|
||||||
Appendix: strings.TrimSpace(`
|
Appendix: strings.TrimSpace(`
|
||||||
你可以在确认需要人工介入后调用 handoff_to_human 这个 Graph Tool 来转人工,但必须遵守以下规则:
|
你可以在确认需要人工介入后调用 handoff_to_human 这个 Graph Tool 来转人工,但必须遵守以下规则:
|
||||||
1. 只有在用户明确要求人工客服,或你已经判断该问题必须由人工继续处理时,才调用该工具。
|
1. 只有在用户明确要求人工客服,或你已经判断该问题必须由人工继续处理时,才调用该工具。
|
||||||
@@ -127,11 +137,6 @@ var (
|
|||||||
GraphCreateTicketConfirm,
|
GraphCreateTicketConfirm,
|
||||||
GraphHandoffConversation,
|
GraphHandoffConversation,
|
||||||
}
|
}
|
||||||
AgentDirectToolSpecs = []ToolSpec{
|
|
||||||
GraphCreateTicketConfirm,
|
|
||||||
GraphHandoffConversation,
|
|
||||||
BuiltinToolSearch,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -238,17 +243,31 @@ func IsAutoInjectedToolCode(toolCode string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ListAgentDirectToolSpecs() []ToolSpec {
|
func ListAgentDirectToolSpecs() []ToolSpec {
|
||||||
return append([]ToolSpec(nil), AgentDirectToolSpecs...)
|
ret := make([]ToolSpec, 0, len(RegisteredToolSpecs))
|
||||||
|
for _, spec := range RegisteredToolSpecs {
|
||||||
|
if !spec.DirectAccess {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, spec)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRuntimeStaticToolSpecs() []ToolSpec {
|
||||||
|
ret := make([]ToolSpec, 0, len(RegisteredToolSpecs))
|
||||||
|
for _, spec := range RegisteredToolSpecs {
|
||||||
|
if !spec.RuntimeStatic {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, spec)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsAgentDirectToolCode(toolCode string) bool {
|
func IsAgentDirectToolCode(toolCode string) bool {
|
||||||
toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
|
toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
|
||||||
for _, spec := range AgentDirectToolSpecs {
|
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||||
if spec.Code == toolCode {
|
return ok && spec.DirectAccess
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NormalizeToolCodeAlias(toolCode string) string {
|
func NormalizeToolCodeAlias(toolCode string) string {
|
||||||
@@ -322,3 +341,45 @@ func hasToolCode(toolCodes map[string]string, target string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NormalizeToolCodes(items []string) []string {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := make([]string, 0, len(items))
|
||||||
|
seen := make(map[string]struct{}, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
item = NormalizeToolCodeAlias(strings.TrimSpace(item))
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[item]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[item] = struct{}{}
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntersectToolCodes(left []string, right []string) []string {
|
||||||
|
left = NormalizeToolCodes(left)
|
||||||
|
right = NormalizeToolCodes(right)
|
||||||
|
switch {
|
||||||
|
case len(left) == 0:
|
||||||
|
return right
|
||||||
|
case len(right) == 0:
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
rightSet := make(map[string]struct{}, len(right))
|
||||||
|
for _, item := range right {
|
||||||
|
rightSet[item] = struct{}{}
|
||||||
|
}
|
||||||
|
ret := make([]string, 0, len(left))
|
||||||
|
for _, item := range left {
|
||||||
|
if _, ok := rightSet[item]; ok {
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user