2026-04-10 14:43:57 +08:00
package toolx
2026-04-14 15:05:38 +08:00
import (
"strings"
2026-05-30 21:19:36 +08:00
"cs-ai-agent/internal/pkg/enums"
"cs-ai-agent/internal/pkg/i18nx"
2026-04-14 15:05:38 +08:00
)
2026-04-12 22:27:09 +08:00
type ToolSpec struct {
2026-04-14 09:39:56 +08:00
Code string
ServerCode string
Name string
Title string
Description string
2026-04-14 15:05:38 +08:00
SourceType enums . ToolSourceType
2026-04-14 09:39:56 +08:00
AutoInjected bool
DirectAccess bool
RuntimeStatic bool
Aliases [] string
Appendix string
2026-04-12 22:27:09 +08:00
}
2026-04-14 09:51:03 +08:00
type ToolMetadata struct {
ToolCode string
ServerCode string
ToolName string
2026-04-14 15:05:38 +08:00
SourceType enums . ToolSourceType
2026-04-14 09:51:03 +08:00
}
2026-04-12 22:27:09 +08:00
var (
BuiltinToolSearch = ToolSpec {
Code : "builtin/tool_search" ,
ServerCode : "builtin" ,
Name : "tool_search" ,
Title : "搜索并调用动态工具" ,
Description : "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。" ,
2026-04-14 15:05:38 +08:00
SourceType : enums . ToolSourceTypeBuiltin ,
2026-04-12 22:27:09 +08:00
AutoInjected : true ,
2026-04-14 09:39:56 +08:00
DirectAccess : true ,
2026-04-12 23:14:27 +08:00
Appendix : strings . TrimSpace ( `
当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则:
1. 先调用 tool_search 搜索需要的动态工具,再继续使用已选中的真实工具。
2. 不要假设所有长尾工具一开始就可见;只有被 tool_search 选中的工具,后续模型调用才会暴露出来。
3. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。
` ),
2026-04-12 22:27:09 +08:00
}
BuiltinSkill = ToolSpec {
Code : "builtin/skill" ,
ServerCode : "builtin" ,
Name : "skill" ,
Title : "加载专项技能说明" ,
2026-04-19 09:57:34 +08:00
Description : "用于按需加载当前 Agent 可用的专项技能说明文档,适合在需要专项处理规则时再注入上下文。" ,
2026-04-14 15:05:38 +08:00
SourceType : enums . ToolSourceTypeBuiltin ,
2026-04-12 22:27:09 +08:00
AutoInjected : true ,
}
GraphTriageServiceRequest = ToolSpec {
2026-04-14 09:39:56 +08:00
Code : "graph/triage_service_request" ,
ServerCode : "graph" ,
Name : "triage_service_request" ,
Title : "升级分流判断" ,
Description : "Graph Tool。用于综合分析当前对话,判断应继续解答、整理工单草稿还是转人工,并在需要建单时一并整理工单草稿。" ,
2026-04-14 15:05:38 +08:00
SourceType : enums . ToolSourceTypeGraph ,
2026-04-14 09:39:56 +08:00
RuntimeStatic : true ,
2026-04-12 23:14:27 +08:00
Appendix : strings . TrimSpace ( `
当你需要判断“继续解答 / 建单 / 转人工”这类复杂升级路径时,优先先调用 triage_service_request 这个 Graph Tool,并遵守以下规则:
1. 该工具会综合当前对话输出 recommendedAction,并在需要建单时附带 ticketDraft。
2. 如果 recommendedAction=continue_answering,则优先继续澄清或解答,不要直接升级。
3. 如果 recommendedAction=prepare_ticket,则优先使用 ticketDraft 或继续补充缺失字段,再调用 create_ticket_with_confirmation。
4. 如果 recommendedAction=handoff_to_human,则确认理由充分后再调用 handoff_to_human。
5. 当升级路径不明确时,优先使用该工具,而不是直接凭主 prompt 做复杂分流判断。
` ),
2026-04-12 22:27:09 +08:00
}
GraphAnalyzeConversation = ToolSpec {
2026-04-14 09:39:56 +08:00
Code : "graph/analyze_conversation" ,
ServerCode : "graph" ,
Name : "analyze_conversation" ,
Title : "分析对话风险与摘要" ,
Description : "Graph Tool。用于整理当前对话摘要、识别风险信号,并给出继续解答、建单或转人工的建议。" ,
2026-04-14 15:05:38 +08:00
SourceType : enums . ToolSourceTypeGraph ,
2026-04-14 09:39:56 +08:00
RuntimeStatic : true ,
2026-04-12 23:14:27 +08:00
Appendix : strings . TrimSpace ( `
当对话可能涉及投诉升级、退款赔偿、明显负面情绪、是否要建单、是否要转人工等复杂判断时,优先调用 analyze_conversation 这个 Graph Tool,并遵守以下规则:
1. 该工具用于输出结构化摘要、风险信号和下一步建议,不代表实际已经建单或转人工。
2. 如果工具建议为 handoff_to_human,应先确认是否满足转人工条件,再考虑调用 handoff_to_human。
3. 如果工具建议为 prepare_ticket,应优先调用 prepare_ticket_draft 或继续补充信息,而不是直接建单。
4. 如果工具建议为 continue_answering,优先继续澄清和解答,不要过早升级动作。
` ),
2026-04-12 22:27:09 +08:00
}
GraphPrepareTicketDraft = ToolSpec {
2026-04-14 09:39:56 +08:00
Code : "graph/prepare_ticket_draft" ,
ServerCode : "graph" ,
Name : "prepare_ticket_draft" ,
Title : "整理工单草稿" ,
Description : "Graph Tool。用于根据当前会话和已收集信息整理工单草稿,输出建议标题、描述、缺失字段和追问建议。" ,
2026-04-14 15:05:38 +08:00
SourceType : enums . ToolSourceTypeGraph ,
2026-04-14 09:39:56 +08:00
RuntimeStatic : true ,
2026-04-12 23:14:27 +08:00
Appendix : strings . TrimSpace ( `
当用户已经表达了建单、投诉、报障、售后处理等诉求,但工单标题、描述或问题整理还比较散乱时,优先调用 prepare_ticket_draft 这个 Graph Tool,并遵守以下规则:
1. 该工具用于整理工单草稿,会返回建议标题、建议描述、缺失字段和追问建议。
2. 如果工具返回 ready=false,优先根据 missingFields 和 followUpQuestions 继续追问,不要直接创建工单。
3. 如果工具返回 ready=true,再结合结果考虑调用 create_ticket_with_confirmation。
4. 该工具用于“整理草稿”,不代表已经创建工单。
` ),
2026-04-12 22:27:09 +08:00
}
GraphCreateTicketConfirm = ToolSpec {
2026-04-14 09:39:56 +08:00
Code : "graph/create_ticket_with_confirmation" ,
ServerCode : "graph" ,
Name : "create_ticket_with_confirmation" ,
Title : "创建工单确认流程" ,
Description : "Graph Tool。用于封装建单参数整理、用户确认、真正建单和结果返回的确定性流程。" ,
2026-04-14 15:05:38 +08:00
SourceType : enums . ToolSourceTypeGraph ,
2026-04-14 09:39:56 +08:00
DirectAccess : true ,
RuntimeStatic : true ,
Aliases : [] string { "builtin/create_ticket_with_confirmation" },
2026-04-12 23:14:27 +08:00
Appendix : strings . TrimSpace ( `
你可以在确认信息充分后调用 create_ticket_with_confirmation 这个 Graph Tool 来创建工单,但必须遵守以下规则:
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。
2. 调用前你必须已经整理出清晰的工单标题和问题描述;如果信息还比较散乱,优先先调用 prepare_ticket_draft 或继续追问,不要过早调用。
3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation 工具,禁止直接口头宣称“已经创建工单”。
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正创建工单;用户取消则结束本次建单流程。
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
` ),
2026-04-12 22:27:09 +08:00
}
GraphHandoffConversation = ToolSpec {
2026-04-14 09:39:56 +08:00
Code : "graph/handoff_to_human" ,
ServerCode : "graph" ,
Name : "handoff_to_human" ,
Title : "转人工确认流程" ,
Description : "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。" ,
2026-04-14 15:05:38 +08:00
SourceType : enums . ToolSourceTypeGraph ,
2026-04-14 09:39:56 +08:00
DirectAccess : true ,
RuntimeStatic : true ,
2026-04-12 23:14:27 +08:00
Appendix : strings . TrimSpace ( `
你可以在确认需要人工介入后调用 handoff_to_human 这个 Graph Tool 来转人工,但必须遵守以下规则:
1. 只有在用户明确要求人工客服,或你已经判断该问题必须由人工继续处理时,才调用该工具。
2. 调用前先尽量整理清楚转人工原因;如果理由含糊,先追问或澄清,不要直接转人工。
3. 一旦决定转人工,必须调用 handoff_to_human 工具,禁止只在回复里口头说“我帮你转人工了”。
4. 该 Graph Tool 会先向用户发起确认。用户确认后才会真正转人工;用户取消则结束本次转人工流程。
5. 如果问题仍可由当前对话继续解决,优先继续解答,不要过早转人工。
2026-05-10 00:02:23 +08:00
6. 如果工具返回 terminal=true 且 shouldRetry=false,说明转人工流程已经结束,禁止重复调用该工具。
2026-04-12 23:14:27 +08:00
` ),
2026-04-12 22:27:09 +08:00
}
RegisteredToolSpecs = [] ToolSpec {
BuiltinToolSearch ,
BuiltinSkill ,
GraphTriageServiceRequest ,
GraphAnalyzeConversation ,
GraphPrepareTicketDraft ,
GraphCreateTicketConfirm ,
GraphHandoffConversation ,
}
)
var (
toolSpecByCode = buildToolSpecByCode ()
2026-04-12 22:30:00 +08:00
toolSpecByName = buildToolSpecByName ()
2026-04-12 22:27:09 +08:00
toolAliasToCanonical = buildToolAliasToCanonical ()
2026-04-10 14:43:57 +08:00
)
2026-04-10 15:41:02 +08:00
2026-04-12 22:27:09 +08:00
func buildToolSpecByCode () map [ string ] ToolSpec {
ret := make ( map [ string ] ToolSpec , len ( RegisteredToolSpecs ))
for _ , spec := range RegisteredToolSpecs {
if strings . TrimSpace ( spec . Code ) == "" {
continue
}
ret [ spec . Code ] = spec
}
return ret
}
func buildToolAliasToCanonical () map [ string ] string {
ret := make ( map [ string ] string )
for _ , spec := range RegisteredToolSpecs {
for _ , alias := range spec . Aliases {
alias = strings . TrimSpace ( alias )
if alias == "" {
continue
}
ret [ alias ] = spec . Code
}
}
return ret
}
2026-04-12 22:30:00 +08:00
func buildToolSpecByName () map [ string ] ToolSpec {
ret := make ( map [ string ] ToolSpec , len ( RegisteredToolSpecs ))
for _ , spec := range RegisteredToolSpecs {
name := strings . TrimSpace ( spec . Name )
if name == "" {
continue
}
ret [ name ] = spec
}
return ret
}
2026-04-12 22:27:09 +08:00
func ListRegisteredToolSpecs () [] ToolSpec {
return append ([] ToolSpec ( nil ), RegisteredToolSpecs ... )
}
func GetRegisteredToolSpec ( toolCode string ) ( ToolSpec , bool ) {
toolCode = NormalizeToolCodeAlias ( strings . TrimSpace ( toolCode ))
spec , ok := toolSpecByCode [ toolCode ]
return spec , ok
}
2026-04-12 22:30:00 +08:00
func GetRegisteredToolSpecByName ( name string ) ( ToolSpec , bool ) {
name = strings . TrimSpace ( name )
spec , ok := toolSpecByName [ name ]
return spec , ok
}
2026-04-12 22:27:09 +08:00
func GetRegisteredToolTitle ( toolCode string ) string {
spec , ok := GetRegisteredToolSpec ( toolCode )
if ! ok {
return ""
}
return spec . Title
}
2026-05-25 12:06:15 +08:00
func GetRegisteredToolTitleLocale ( toolCode string , locale string ) string {
spec , ok := GetRegisteredToolSpec ( toolCode )
if ! ok {
return ""
}
if i18nx . NormalizeLocale ( locale ) != i18nx . LocaleEnUS {
return spec . Title
}
if text := registeredToolEnglishTitle ( spec . Code ); text != "" {
return text
}
return spec . Title
}
2026-04-12 22:27:09 +08:00
func GetRegisteredToolDescription ( toolCode string ) string {
spec , ok := GetRegisteredToolSpec ( toolCode )
if ! ok {
return ""
}
return spec . Description
}
2026-05-25 12:06:15 +08:00
func GetRegisteredToolDescriptionLocale ( toolCode string , locale string ) string {
spec , ok := GetRegisteredToolSpec ( toolCode )
if ! ok {
return ""
}
if i18nx . NormalizeLocale ( locale ) != i18nx . LocaleEnUS {
return spec . Description
}
if text := registeredToolEnglishDescription ( spec . Code ); text != "" {
return text
}
return spec . Description
}
func registeredToolEnglishTitle ( toolCode string ) string {
switch toolCode {
case BuiltinToolSearch . Code :
return "Search and Run Dynamic Tools"
case BuiltinSkill . Code :
return "Load Skill Instructions"
case GraphTriageServiceRequest . Code :
return "Route Service Request"
case GraphAnalyzeConversation . Code :
return "Analyze Conversation Risk and Summary"
case GraphPrepareTicketDraft . Code :
return "Prepare Ticket Draft"
case GraphCreateTicketConfirm . Code :
return "Create Ticket With Confirmation"
case GraphHandoffConversation . Code :
return "Handoff to Human With Confirmation"
default :
return ""
}
}
func registeredToolEnglishDescription ( toolCode string ) string {
switch toolCode {
case BuiltinToolSearch . Code :
return "Searches the MCP tools currently available to the agent and runs the selected tool after its toolCode is confirmed. Best for long-tail tools; it should not replace fixed built-in workflow tools."
case BuiltinSkill . Code :
return "Loads specialized skill instructions for the current agent when extra task-specific guidance is needed."
case GraphTriageServiceRequest . Code :
return "Analyzes the current conversation to decide whether to keep answering, prepare a ticket draft, or hand off to a human, including a ticket draft when ticket creation is appropriate."
case GraphAnalyzeConversation . Code :
return "Summarizes the current conversation, identifies risk signals, and recommends whether to keep answering, create a ticket, or hand off to a human."
case GraphPrepareTicketDraft . Code :
return "Turns the current conversation and collected details into a ticket draft with a suggested title, description, missing fields, and follow-up questions."
case GraphCreateTicketConfirm . Code :
return "Guides ticket creation with parameter preparation, customer confirmation, actual ticket creation, and final result delivery."
case GraphHandoffConversation . Code :
return "Guides human handoff with reason preparation, customer confirmation, actual transfer, and final result delivery."
default :
return ""
}
}
2026-04-12 22:27:09 +08:00
func GetRegisteredToolIdentity ( toolCode string ) ( serverCode , toolName string , ok bool ) {
spec , ok := GetRegisteredToolSpec ( toolCode )
if ! ok {
return "" , "" , false
}
return spec . ServerCode , spec . Name , true
}
2026-04-14 15:05:38 +08:00
func ResolveToolSourceType ( toolCode string ) enums . ToolSourceType {
2026-04-12 22:27:09 +08:00
if spec , ok := GetRegisteredToolSpec ( toolCode ); ok {
return spec . SourceType
}
toolCode = strings . TrimSpace ( toolCode )
switch {
case strings . HasPrefix ( toolCode , "graph/" ):
2026-04-14 15:05:38 +08:00
return enums . ToolSourceTypeGraph
2026-04-12 22:27:09 +08:00
case strings . HasPrefix ( toolCode , "builtin/" ):
2026-04-14 15:05:38 +08:00
return enums . ToolSourceTypeBuiltin
2026-04-12 22:27:09 +08:00
default :
2026-04-14 15:05:38 +08:00
return enums . ToolSourceTypeMCP
2026-04-12 22:27:09 +08:00
}
}
2026-04-10 15:41:02 +08:00
func IsAutoInjectedToolCode ( toolCode string ) bool {
2026-04-12 22:27:09 +08:00
spec , ok := GetRegisteredToolSpec ( toolCode )
return ok && spec . AutoInjected
2026-04-10 15:41:02 +08:00
}
2026-04-10 15:45:51 +08:00
2026-04-12 22:30:00 +08:00
func ListAgentDirectToolSpecs () [] ToolSpec {
2026-04-14 09:39:56 +08:00
ret := make ([] ToolSpec , 0 , len ( RegisteredToolSpecs ))
for _ , spec := range RegisteredToolSpecs {
if ! spec . DirectAccess {
continue
}
ret = append ( ret , spec )
}
return ret
2026-04-12 22:30:00 +08:00
}
2026-04-14 09:39:56 +08:00
func ListRuntimeStaticToolSpecs () [] ToolSpec {
ret := make ([] ToolSpec , 0 , len ( RegisteredToolSpecs ))
for _ , spec := range RegisteredToolSpecs {
if ! spec . RuntimeStatic {
continue
2026-04-12 22:30:00 +08:00
}
2026-04-14 09:39:56 +08:00
ret = append ( ret , spec )
2026-04-12 22:30:00 +08:00
}
2026-04-14 09:39:56 +08:00
return ret
}
func IsAgentDirectToolCode ( toolCode string ) bool {
toolCode = NormalizeToolCodeAlias ( strings . TrimSpace ( toolCode ))
spec , ok := GetRegisteredToolSpec ( toolCode )
return ok && spec . DirectAccess
2026-04-12 22:30:00 +08:00
}
2026-04-14 15:10:50 +08:00
func IsAgentDirectGraphToolCode ( toolCode string ) bool {
toolCode = NormalizeToolCodeAlias ( strings . TrimSpace ( toolCode ))
spec , ok := GetRegisteredToolSpec ( toolCode )
return ok && spec . DirectAccess && spec . SourceType == enums . ToolSourceTypeGraph
}
2026-04-10 15:45:51 +08:00
func NormalizeToolCodeAlias ( toolCode string ) string {
2026-04-12 22:27:09 +08:00
toolCode = strings . TrimSpace ( toolCode )
if canonical , ok := toolAliasToCanonical [ toolCode ]; ok {
return canonical
2026-04-10 15:45:51 +08:00
}
2026-04-12 22:27:09 +08:00
return toolCode
2026-04-10 15:45:51 +08:00
}
2026-04-12 23:14:27 +08:00
func BuildToolAppendices ( hasDynamicMCPTools bool , toolCodes map [ string ] string ) [] string {
2026-04-14 09:47:42 +08:00
return BuildToolAppendicesForCodes ( hasDynamicMCPTools , toolCodesFromMap ( toolCodes ))
}
func BuildToolAppendicesForCodes ( hasDynamicMCPTools bool , toolCodes [] string ) [] string {
2026-04-12 23:14:27 +08:00
ret := make ([] string , 0 , len ( toolCodes ) + 1 )
2026-04-14 09:47:42 +08:00
normalizedToolCodes := NormalizeToolCodes ( toolCodes )
2026-04-12 23:14:27 +08:00
if hasDynamicMCPTools && strings . TrimSpace ( BuiltinToolSearch . Appendix ) != "" {
ret = append ( ret , BuiltinToolSearch . Appendix )
}
for _ , spec := range RegisteredToolSpecs {
if strings . TrimSpace ( spec . Appendix ) == "" {
continue
}
if spec . Code == BuiltinToolSearch . Code {
continue
}
2026-04-14 09:47:42 +08:00
if containsNormalizedToolCode ( normalizedToolCodes , spec . Code ) {
2026-04-12 23:14:27 +08:00
ret = append ( ret , spec . Appendix )
}
}
return ret
}
2026-04-14 15:05:38 +08:00
func BuildToolMetadata ( toolCode string ) ( serverCode , toolName string , sourceType enums . ToolSourceType , ok bool ) {
2026-04-12 23:15:27 +08:00
spec , ok := GetRegisteredToolSpec ( toolCode )
if ! ok {
return "" , "" , ResolveToolSourceType ( toolCode ), false
}
return spec . ServerCode , spec . Name , spec . SourceType , true
}
2026-04-14 09:51:03 +08:00
func ResolveToolMetadata ( toolCode string , fallbackName string ) ToolMetadata {
toolCode = NormalizeToolCodeAlias ( strings . TrimSpace ( toolCode ))
fallbackName = strings . TrimSpace ( fallbackName )
serverCode , toolName , sourceType , ok := BuildToolMetadata ( toolCode )
if ! ok && toolName == "" {
toolName = fallbackName
}
if toolName == "" {
toolName = fallbackName
}
return ToolMetadata {
ToolCode : toolCode ,
ServerCode : strings . TrimSpace ( serverCode ),
ToolName : strings . TrimSpace ( toolName ),
2026-04-14 15:05:38 +08:00
SourceType : sourceType ,
2026-04-14 09:51:03 +08:00
}
}
2026-04-12 23:15:27 +08:00
func IsAlwaysAllowedToolCode ( toolCode string ) bool {
return NormalizeToolCodeAlias ( strings . TrimSpace ( toolCode )) == GraphHandoffConversation . Code
}
func IsImpliedAllowedToolCode ( toolCode string , allowedToolCodes map [ string ] struct {}) bool {
toolCode = NormalizeToolCodeAlias ( strings . TrimSpace ( toolCode ))
if toolCode == "" || len ( allowedToolCodes ) == 0 {
return false
}
switch toolCode {
case GraphTriageServiceRequest . Code , GraphAnalyzeConversation . Code :
if _ , ok := allowedToolCodes [ GraphCreateTicketConfirm . Code ]; ok {
return true
}
if _ , ok := allowedToolCodes [ GraphHandoffConversation . Code ]; ok {
return true
}
case GraphPrepareTicketDraft . Code :
_ , ok := allowedToolCodes [ GraphCreateTicketConfirm . Code ]
return ok
}
return false
}
2026-04-12 23:14:27 +08:00
func hasToolCode ( toolCodes map [ string ] string , target string ) bool {
2026-04-14 09:47:42 +08:00
return containsNormalizedToolCode ( toolCodesFromMap ( toolCodes ), target )
}
func containsNormalizedToolCode ( toolCodes [] string , target string ) bool {
2026-04-12 23:14:27 +08:00
target = strings . TrimSpace ( target )
if target == "" || len ( toolCodes ) == 0 {
return false
}
2026-04-14 09:47:42 +08:00
for _ , item := range NormalizeToolCodes ( toolCodes ) {
if item == target {
2026-04-12 23:14:27 +08:00
return true
}
}
return false
}
2026-04-14 09:39:56 +08:00
2026-04-14 09:47:42 +08:00
func toolCodesFromMap ( toolCodes map [ string ] string ) [] string {
if len ( toolCodes ) == 0 {
return nil
}
ret := make ([] string , 0 , len ( toolCodes ))
for _ , item := range toolCodes {
ret = append ( ret , item )
}
return ret
}
2026-04-14 09:39:56 +08:00
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
}