Files
ai-agent/internal/pkg/toolx/builtin_tools.go
T
mlogclub 3439d20537 feat(i18n): Enhance internationalization support for tool specifications and notifications
- Added TitleKey and DescriptionKey fields to ToolSpec for dynamic localization.
- Updated tool specifications to use i18nx for titles, descriptions, and appendices.
- Refactored notification messages to utilize i18nx for localization in conversation and ticket assignment events.
- Improved localization in the debug dialog component for skill definitions.
- Added new translation keys in English and Chinese for ticket and conversation assignment notifications.
- Ensured that fallback messages are properly localized based on user locale.
2026-06-03 09:36:33 +08:00

465 lines
13 KiB
Go

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,
}
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,
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,
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,
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,
DirectAccess: true,
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,
DirectAccess: true,
RuntimeStatic: true,
Appendix: i18nx.Get("tool.graph.handoffConversation.appendix"),
AppendixKey: "tool.graph.handoffConversation.appendix",
}
RegisteredToolSpecs = []ToolSpec{
BuiltinToolSearch,
BuiltinSkill,
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 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
}