chore: import clean Agent Desk snapshot (2/7)
This commit is contained in:
541
internal/pkg/toolx/builtin_tools.go
Normal file
541
internal/pkg/toolx/builtin_tools.go
Normal file
@@ -0,0 +1,541 @@
|
||||
package toolx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
)
|
||||
|
||||
type ToolSpec struct {
|
||||
Code string
|
||||
ServerCode string
|
||||
Name string
|
||||
Title string
|
||||
TitleKey string
|
||||
Description string
|
||||
DescriptionKey string
|
||||
SourceType enums.ToolSourceType
|
||||
AutoInjected bool
|
||||
DirectAccess bool
|
||||
RuntimeStatic bool
|
||||
Aliases []string
|
||||
Appendix string
|
||||
AppendixKey string
|
||||
}
|
||||
|
||||
type ToolMetadata struct {
|
||||
ToolCode string
|
||||
ServerCode string
|
||||
ToolName string
|
||||
SourceType enums.ToolSourceType
|
||||
}
|
||||
|
||||
var (
|
||||
BuiltinToolSearch = ToolSpec{
|
||||
Code: "builtin/tool_search",
|
||||
ServerCode: "builtin",
|
||||
Name: "tool_search",
|
||||
Title: i18nx.Get("tool.builtin.toolSearch.title"),
|
||||
TitleKey: "tool.builtin.toolSearch.title",
|
||||
Description: i18nx.Get("tool.builtin.toolSearch.description"),
|
||||
DescriptionKey: "tool.builtin.toolSearch.description",
|
||||
SourceType: enums.ToolSourceTypeBuiltin,
|
||||
AutoInjected: true,
|
||||
DirectAccess: true,
|
||||
Appendix: i18nx.Get("tool.builtin.toolSearch.appendix"),
|
||||
AppendixKey: "tool.builtin.toolSearch.appendix",
|
||||
}
|
||||
BuiltinSkill = ToolSpec{
|
||||
Code: "builtin/skill",
|
||||
ServerCode: "builtin",
|
||||
Name: "skill",
|
||||
Title: i18nx.Get("tool.builtin.skill.title"),
|
||||
TitleKey: "tool.builtin.skill.title",
|
||||
Description: i18nx.Get("tool.builtin.skill.description"),
|
||||
DescriptionKey: "tool.builtin.skill.description",
|
||||
SourceType: enums.ToolSourceTypeBuiltin,
|
||||
AutoInjected: true,
|
||||
}
|
||||
BuiltinConversationContext = ToolSpec{
|
||||
Code: "builtin/conversation_context",
|
||||
ServerCode: "builtin",
|
||||
Name: "conversation_context",
|
||||
Title: "会话上下文",
|
||||
Description: "读取当前客户基础信息和会话摘要。",
|
||||
SourceType: enums.ToolSourceTypeBuiltin,
|
||||
DirectAccess: true,
|
||||
}
|
||||
BuiltinKnowledgeRetrieve = ToolSpec{
|
||||
Code: "builtin/knowledge_retrieve",
|
||||
ServerCode: "builtin",
|
||||
Name: "knowledge_retrieve",
|
||||
Title: "知识检索",
|
||||
Description: "在当前 Agent 已绑定的知识库中检索证据。",
|
||||
SourceType: enums.ToolSourceTypeBuiltin,
|
||||
DirectAccess: true,
|
||||
}
|
||||
GraphTriageServiceRequest = ToolSpec{
|
||||
Code: "graph/triage_service_request",
|
||||
ServerCode: "graph",
|
||||
Name: "triage_service_request",
|
||||
Title: i18nx.Get("tool.graph.triageServiceRequest.title"),
|
||||
TitleKey: "tool.graph.triageServiceRequest.title",
|
||||
Description: i18nx.Get("tool.graph.triageServiceRequest.description"),
|
||||
DescriptionKey: "tool.graph.triageServiceRequest.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
DirectAccess: true,
|
||||
RuntimeStatic: true,
|
||||
Appendix: i18nx.Get("tool.graph.triageServiceRequest.appendix"),
|
||||
AppendixKey: "tool.graph.triageServiceRequest.appendix",
|
||||
}
|
||||
GraphAnalyzeConversation = ToolSpec{
|
||||
Code: "graph/analyze_conversation",
|
||||
ServerCode: "graph",
|
||||
Name: "analyze_conversation",
|
||||
Title: i18nx.Get("tool.graph.analyzeConversation.title"),
|
||||
TitleKey: "tool.graph.analyzeConversation.title",
|
||||
Description: i18nx.Get("tool.graph.analyzeConversation.description"),
|
||||
DescriptionKey: "tool.graph.analyzeConversation.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
RuntimeStatic: true,
|
||||
DirectAccess: true,
|
||||
Appendix: i18nx.Get("tool.graph.analyzeConversation.appendix"),
|
||||
AppendixKey: "tool.graph.analyzeConversation.appendix",
|
||||
}
|
||||
GraphPrepareTicketDraft = ToolSpec{
|
||||
Code: "graph/prepare_ticket_draft",
|
||||
ServerCode: "graph",
|
||||
Name: "prepare_ticket_draft",
|
||||
Title: i18nx.Get("tool.graph.prepareTicketDraft.title"),
|
||||
TitleKey: "tool.graph.prepareTicketDraft.title",
|
||||
Description: i18nx.Get("tool.graph.prepareTicketDraft.description"),
|
||||
DescriptionKey: "tool.graph.prepareTicketDraft.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
DirectAccess: true,
|
||||
RuntimeStatic: true,
|
||||
Appendix: i18nx.Get("tool.graph.prepareTicketDraft.appendix"),
|
||||
AppendixKey: "tool.graph.prepareTicketDraft.appendix",
|
||||
}
|
||||
GraphCreateTicketConfirm = ToolSpec{
|
||||
Code: "graph/create_ticket_with_confirmation",
|
||||
ServerCode: "graph",
|
||||
Name: "create_ticket_with_confirmation",
|
||||
Title: i18nx.Get("tool.graph.createTicketConfirm.title"),
|
||||
TitleKey: "tool.graph.createTicketConfirm.title",
|
||||
Description: i18nx.Get("tool.graph.createTicketConfirm.description"),
|
||||
DescriptionKey: "tool.graph.createTicketConfirm.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
RuntimeStatic: true,
|
||||
Aliases: []string{"builtin/create_ticket_with_confirmation"},
|
||||
Appendix: i18nx.Get("tool.graph.createTicketConfirm.appendix"),
|
||||
AppendixKey: "tool.graph.createTicketConfirm.appendix",
|
||||
}
|
||||
GraphHandoffConversation = ToolSpec{
|
||||
Code: "graph/handoff_to_human",
|
||||
ServerCode: "graph",
|
||||
Name: "handoff_to_human",
|
||||
Title: i18nx.Get("tool.graph.handoffConversation.title"),
|
||||
TitleKey: "tool.graph.handoffConversation.title",
|
||||
Description: i18nx.Get("tool.graph.handoffConversation.description"),
|
||||
DescriptionKey: "tool.graph.handoffConversation.description",
|
||||
SourceType: enums.ToolSourceTypeGraph,
|
||||
RuntimeStatic: true,
|
||||
Appendix: i18nx.Get("tool.graph.handoffConversation.appendix"),
|
||||
AppendixKey: "tool.graph.handoffConversation.appendix",
|
||||
}
|
||||
RegisteredToolSpecs = []ToolSpec{
|
||||
BuiltinToolSearch,
|
||||
BuiltinSkill,
|
||||
BuiltinConversationContext,
|
||||
BuiltinKnowledgeRetrieve,
|
||||
GraphTriageServiceRequest,
|
||||
GraphAnalyzeConversation,
|
||||
GraphPrepareTicketDraft,
|
||||
GraphCreateTicketConfirm,
|
||||
GraphHandoffConversation,
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
toolSpecByCode = buildToolSpecByCode()
|
||||
toolSpecByName = buildToolSpecByName()
|
||||
toolAliasToCanonical = buildToolAliasToCanonical()
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func GetRegisteredToolSpecByName(name string) (ToolSpec, bool) {
|
||||
name = strings.TrimSpace(name)
|
||||
spec, ok := toolSpecByName[name]
|
||||
return spec, ok
|
||||
}
|
||||
|
||||
func GetRegisteredToolTitle(toolCode string) string {
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return spec.Title
|
||||
}
|
||||
|
||||
func GetRegisteredToolTitleLocale(toolCode string, locale string) string {
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if strings.TrimSpace(spec.TitleKey) != "" {
|
||||
return i18nx.Getf(locale, spec.TitleKey)
|
||||
}
|
||||
return spec.Title
|
||||
}
|
||||
|
||||
func GetRegisteredToolDescription(toolCode string) string {
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return spec.Description
|
||||
}
|
||||
|
||||
func GetRegisteredToolDescriptionLocale(toolCode string, locale string) string {
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if strings.TrimSpace(spec.DescriptionKey) != "" {
|
||||
return i18nx.Getf(locale, spec.DescriptionKey)
|
||||
}
|
||||
return spec.Description
|
||||
}
|
||||
|
||||
func GetRegisteredToolAppendixLocale(toolCode string, locale string) string {
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if strings.TrimSpace(spec.AppendixKey) != "" {
|
||||
return i18nx.Getf(locale, spec.AppendixKey)
|
||||
}
|
||||
return spec.Appendix
|
||||
}
|
||||
|
||||
func GetRegisteredToolIdentity(toolCode string) (serverCode, toolName string, ok bool) {
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
return spec.ServerCode, spec.Name, true
|
||||
}
|
||||
|
||||
func ResolveToolSourceType(toolCode string) enums.ToolSourceType {
|
||||
if spec, ok := GetRegisteredToolSpec(toolCode); ok {
|
||||
return spec.SourceType
|
||||
}
|
||||
toolCode = strings.TrimSpace(toolCode)
|
||||
switch {
|
||||
case strings.HasPrefix(toolCode, "graph/"):
|
||||
return enums.ToolSourceTypeGraph
|
||||
case strings.HasPrefix(toolCode, "builtin/"):
|
||||
return enums.ToolSourceTypeBuiltin
|
||||
default:
|
||||
return enums.ToolSourceTypeMCP
|
||||
}
|
||||
}
|
||||
|
||||
func IsAutoInjectedToolCode(toolCode string) bool {
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
return ok && spec.AutoInjected
|
||||
}
|
||||
|
||||
func ListAgentDirectToolSpecs() []ToolSpec {
|
||||
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 {
|
||||
toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
return ok && spec.DirectAccess
|
||||
}
|
||||
|
||||
func IsAgentDirectGraphToolCode(toolCode string) bool {
|
||||
toolCode = NormalizeToolCodeAlias(strings.TrimSpace(toolCode))
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
return ok && spec.DirectAccess && spec.SourceType == enums.ToolSourceTypeGraph
|
||||
}
|
||||
|
||||
func NormalizeToolCodeAlias(toolCode string) string {
|
||||
toolCode = strings.TrimSpace(toolCode)
|
||||
if canonical, ok := toolAliasToCanonical[toolCode]; ok {
|
||||
return canonical
|
||||
}
|
||||
return toolCode
|
||||
}
|
||||
|
||||
func SanitizeToolFunctionName(toolCode string) string {
|
||||
toolCode = strings.TrimSpace(toolCode)
|
||||
if toolCode == "" {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(toolCode))
|
||||
for _, r := range toolCode {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
|
||||
sb.WriteRune(r)
|
||||
} else {
|
||||
sb.WriteByte('_')
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func ResolveToolFunctionName(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || name == "tool_search" || name == "conversation_decision" {
|
||||
return name
|
||||
}
|
||||
if _, ok := toolSpecByCode[name]; ok {
|
||||
return NormalizeToolCodeAlias(name)
|
||||
}
|
||||
if canonical, ok := toolAliasToCanonical[name]; ok {
|
||||
return canonical
|
||||
}
|
||||
for _, spec := range RegisteredToolSpecs {
|
||||
if SanitizeToolFunctionName(spec.Code) == name {
|
||||
return spec.Code
|
||||
}
|
||||
for _, alias := range spec.Aliases {
|
||||
if SanitizeToolFunctionName(alias) == name {
|
||||
return spec.Code
|
||||
}
|
||||
}
|
||||
}
|
||||
if spec, ok := toolSpecByName[name]; ok {
|
||||
return spec.Code
|
||||
}
|
||||
if strings.HasPrefix(name, "skill_") {
|
||||
return "skill/" + strings.TrimPrefix(name, "skill_")
|
||||
}
|
||||
if strings.HasPrefix(name, "workflow_") {
|
||||
return "workflow/" + strings.TrimPrefix(name, "workflow_")
|
||||
}
|
||||
if strings.HasPrefix(name, "builtin_") {
|
||||
return "builtin/" + strings.TrimPrefix(name, "builtin_")
|
||||
}
|
||||
if strings.HasPrefix(name, "graph_") {
|
||||
return "graph/" + strings.TrimPrefix(name, "graph_")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func BuildToolAppendices(hasDynamicMCPTools bool, toolCodes map[string]string) []string {
|
||||
return BuildToolAppendicesForCodes(hasDynamicMCPTools, toolCodesFromMap(toolCodes))
|
||||
}
|
||||
|
||||
func BuildToolAppendicesForCodes(hasDynamicMCPTools bool, toolCodes []string) []string {
|
||||
return BuildToolAppendicesForCodesLocale(hasDynamicMCPTools, toolCodes, i18nx.DefaultLocale)
|
||||
}
|
||||
|
||||
func BuildToolAppendicesForCodesLocale(hasDynamicMCPTools bool, toolCodes []string, locale string) []string {
|
||||
ret := make([]string, 0, len(toolCodes)+1)
|
||||
normalizedToolCodes := NormalizeToolCodes(toolCodes)
|
||||
if hasDynamicMCPTools {
|
||||
if appendix := strings.TrimSpace(GetRegisteredToolAppendixLocale(BuiltinToolSearch.Code, locale)); appendix != "" {
|
||||
ret = append(ret, appendix)
|
||||
}
|
||||
}
|
||||
for _, spec := range RegisteredToolSpecs {
|
||||
if spec.Code == BuiltinToolSearch.Code {
|
||||
continue
|
||||
}
|
||||
if containsNormalizedToolCode(normalizedToolCodes, spec.Code) {
|
||||
if appendix := strings.TrimSpace(GetRegisteredToolAppendixLocale(spec.Code, locale)); appendix != "" {
|
||||
ret = append(ret, appendix)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func BuildToolMetadata(toolCode string) (serverCode, toolName string, sourceType enums.ToolSourceType, ok bool) {
|
||||
spec, ok := GetRegisteredToolSpec(toolCode)
|
||||
if !ok {
|
||||
return "", "", ResolveToolSourceType(toolCode), false
|
||||
}
|
||||
return spec.ServerCode, spec.Name, spec.SourceType, true
|
||||
}
|
||||
|
||||
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),
|
||||
SourceType: sourceType,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func hasToolCode(toolCodes map[string]string, target string) bool {
|
||||
return containsNormalizedToolCode(toolCodesFromMap(toolCodes), target)
|
||||
}
|
||||
|
||||
func containsNormalizedToolCode(toolCodes []string, target string) bool {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" || len(toolCodes) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, item := range NormalizeToolCodes(toolCodes) {
|
||||
if item == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
95
internal/pkg/toolx/builtin_tools_test.go
Normal file
95
internal/pkg/toolx/builtin_tools_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package toolx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
)
|
||||
|
||||
func TestResolveToolMetadata(t *testing.T) {
|
||||
item := ResolveToolMetadata("builtin/create_ticket_with_confirmation", "")
|
||||
if item.ToolCode != GraphCreateTicketConfirm.Code {
|
||||
t.Fatalf("unexpected tool code: %s", item.ToolCode)
|
||||
}
|
||||
if item.ServerCode != GraphCreateTicketConfirm.ServerCode {
|
||||
t.Fatalf("unexpected server code: %s", item.ServerCode)
|
||||
}
|
||||
if item.ToolName != GraphCreateTicketConfirm.Name {
|
||||
t.Fatalf("unexpected tool name: %s", item.ToolName)
|
||||
}
|
||||
if item.SourceType != GraphCreateTicketConfirm.SourceType {
|
||||
t.Fatalf("unexpected source type: %s", item.SourceType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveToolMetadataFallsBackToName(t *testing.T) {
|
||||
item := ResolveToolMetadata("mcp/demo_tool", "demo_tool")
|
||||
if item.ToolCode != "mcp/demo_tool" {
|
||||
t.Fatalf("unexpected tool code: %s", item.ToolCode)
|
||||
}
|
||||
if item.ServerCode != "" {
|
||||
t.Fatalf("unexpected server code: %s", item.ServerCode)
|
||||
}
|
||||
if item.ToolName != "demo_tool" {
|
||||
t.Fatalf("unexpected tool name: %s", item.ToolName)
|
||||
}
|
||||
if item.SourceType != "mcp" {
|
||||
t.Fatalf("unexpected source type: %s", item.SourceType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredToolTextUsesEnglishLocale(t *testing.T) {
|
||||
title := GetRegisteredToolTitleLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleEnUS)
|
||||
if title != "Create Ticket With Confirmation" {
|
||||
t.Fatalf("unexpected english title: %q", title)
|
||||
}
|
||||
|
||||
description := GetRegisteredToolDescriptionLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleEnUS)
|
||||
want := "Guides ticket creation with parameter preparation, customer confirmation, actual ticket creation, and final result delivery."
|
||||
if description != want {
|
||||
t.Fatalf("unexpected english description: %q", description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredToolTextKeepsChineseLocale(t *testing.T) {
|
||||
title := GetRegisteredToolTitleLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleZhCN)
|
||||
if title != "创建工单确认流程" {
|
||||
t.Fatalf("unexpected chinese title: %q", title)
|
||||
}
|
||||
|
||||
description := GetRegisteredToolDescriptionLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleZhCN)
|
||||
if description != "Graph Tool。处理工单参数准备、用户确认、实际创建工单和结果返回。" {
|
||||
t.Fatalf("unexpected chinese description: %q", description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeToolFunctionName(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"builtin/knowledge_retrieve": "builtin_knowledge_retrieve",
|
||||
"graph/triage_service_request": "graph_triage_service_request",
|
||||
"skill/12": "skill_12",
|
||||
"workflow/99": "workflow_99",
|
||||
"tool_search": "tool_search",
|
||||
}
|
||||
for input, expected := range cases {
|
||||
if got := SanitizeToolFunctionName(input); got != expected {
|
||||
t.Errorf("SanitizeToolFunctionName(%q) = %q, want %q", input, got, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveToolFunctionName(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"builtin_knowledge_retrieve": "builtin/knowledge_retrieve",
|
||||
"graph_triage_service_request": "graph/triage_service_request",
|
||||
"knowledge_retrieve": "builtin/knowledge_retrieve",
|
||||
"skill_12": "skill/12",
|
||||
"workflow_99": "workflow/99",
|
||||
"tool_search": "tool_search",
|
||||
}
|
||||
for input, expected := range cases {
|
||||
if got := ResolveToolFunctionName(input); got != expected {
|
||||
t.Errorf("ResolveToolFunctionName(%q) = %q, want %q", input, got, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
51
internal/pkg/toolx/mcp_policy.go
Normal file
51
internal/pkg/toolx/mcp_policy.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package toolx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
)
|
||||
|
||||
const (
|
||||
MCPRiskLevelRead = "read"
|
||||
MCPRiskLevelWrite = "write"
|
||||
)
|
||||
|
||||
type TrustedMCPToolPolicy struct {
|
||||
ToolCode string
|
||||
Title string
|
||||
RiskLevel string
|
||||
RequireConfirmation bool
|
||||
}
|
||||
|
||||
var trustedMCPToolPolicies = map[string]TrustedMCPToolPolicy{
|
||||
"system/server_time": {
|
||||
ToolCode: "system/server_time",
|
||||
Title: "获取当前时间",
|
||||
RiskLevel: MCPRiskLevelRead,
|
||||
RequireConfirmation: false,
|
||||
},
|
||||
"system/service_info": {
|
||||
ToolCode: "system/service_info",
|
||||
Title: "查看服务信息",
|
||||
RiskLevel: MCPRiskLevelRead,
|
||||
RequireConfirmation: false,
|
||||
},
|
||||
}
|
||||
|
||||
func GetTrustedMCPToolPolicy(toolCode string) (TrustedMCPToolPolicy, bool) {
|
||||
policy, ok := trustedMCPToolPolicies[NormalizeToolCodeAlias(strings.TrimSpace(toolCode))]
|
||||
return policy, ok
|
||||
}
|
||||
|
||||
func ApplyTrustedMCPToolPolicy(item request.AIAgentMCPToolRequest) request.AIAgentMCPToolRequest {
|
||||
policy, ok := GetTrustedMCPToolPolicy(item.ToolCode)
|
||||
if !ok {
|
||||
return item
|
||||
}
|
||||
item.ToolCode = policy.ToolCode
|
||||
item.Title = policy.Title
|
||||
item.RiskLevel = policy.RiskLevel
|
||||
item.RequireConfirmation = policy.RequireConfirmation
|
||||
return item
|
||||
}
|
||||
98
internal/pkg/toolx/mcp_tool.go
Normal file
98
internal/pkg/toolx/mcp_tool.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package toolx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
)
|
||||
|
||||
func BuildMCPToolCode(serverCode, toolName string) string {
|
||||
serverCode = strings.TrimSpace(serverCode)
|
||||
toolName = strings.TrimSpace(toolName)
|
||||
if serverCode == "" || toolName == "" {
|
||||
return ""
|
||||
}
|
||||
return serverCode + "/" + toolName
|
||||
}
|
||||
|
||||
func SplitMCPToolCode(toolCode string) (string, string) {
|
||||
toolCode = strings.TrimSpace(toolCode)
|
||||
if toolCode == "" {
|
||||
return "", ""
|
||||
}
|
||||
idx := strings.Index(toolCode, "/")
|
||||
if idx <= 0 || idx >= len(toolCode)-1 {
|
||||
return "", ""
|
||||
}
|
||||
return strings.TrimSpace(toolCode[:idx]), strings.TrimSpace(toolCode[idx+1:])
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode)
|
||||
if parsedServerCode != "" && parsedToolName != "" {
|
||||
if serverCode != "" && !strings.EqualFold(serverCode, parsedServerCode) {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0016")
|
||||
}
|
||||
if toolName != "" && !strings.EqualFold(toolName, parsedToolName) {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0017")
|
||||
}
|
||||
serverCode = parsedServerCode
|
||||
toolName = parsedToolName
|
||||
} else {
|
||||
serverCode = ""
|
||||
toolName = ""
|
||||
}
|
||||
} else {
|
||||
toolCode = BuildMCPToolCode(serverCode, toolName)
|
||||
}
|
||||
if toolCode == "" {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0019")
|
||||
}
|
||||
if parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
|
||||
serverCode = parsedServerCode
|
||||
toolName = parsedToolName
|
||||
}
|
||||
if serverCode == "" && toolName == "" && strings.Contains(toolCode, "/") && !strings.HasPrefix(toolCode, "builtin/") {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0018")
|
||||
}
|
||||
ret := request.AIAgentMCPToolRequest{
|
||||
ToolCode: toolCode,
|
||||
ServerCode: serverCode,
|
||||
ToolName: toolName,
|
||||
Title: strings.TrimSpace(item.Title),
|
||||
Description: strings.TrimSpace(item.Description),
|
||||
RiskLevel: strings.ToLower(strings.TrimSpace(item.RiskLevel)),
|
||||
RequireConfirmation: item.RequireConfirmation,
|
||||
}
|
||||
if len(item.Arguments) > 0 {
|
||||
ret.Arguments = make(map[string]string, len(item.Arguments))
|
||||
for key, value := range item.Arguments {
|
||||
key = strings.TrimSpace(key)
|
||||
value = strings.TrimSpace(value)
|
||||
if key == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
ret.Arguments[key] = value
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ParseAgentMCPToolsJSON(raw string) ([]request.AIAgentMCPToolRequest, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var ret []request.AIAgentMCPToolRequest
|
||||
if err := json.Unmarshal([]byte(raw), &ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
Reference in New Issue
Block a user