feat: add tool search functionality and enhance tool management in AI agent
This commit is contained in:
@@ -109,7 +109,7 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) {
|
|||||||
collector.Data.Skill.RouteReason = summary.SkillRouteReason
|
collector.Data.Skill.RouteReason = summary.SkillRouteReason
|
||||||
collector.Data.Skill.RouteTrace = summary.SkillRouteTrace
|
collector.Data.Skill.RouteTrace = summary.SkillRouteTrace
|
||||||
|
|
||||||
agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, req.SelectedSkill, filteredToolDefs, req.ExtraTools, req.ExtraToolCodes, collector)
|
agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, req.SelectedSkill, filteredToolDefs, nil, req.ExtraTools, req.ExtraToolCodes, collector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
summary.Status = "error"
|
summary.Status = "error"
|
||||||
summary.ErrorMessage = err.Error()
|
summary.ErrorMessage = err.Error()
|
||||||
@@ -233,7 +233,7 @@ func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, erro
|
|||||||
collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...)
|
collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...)
|
||||||
collector.Data.Model.Provider = string(req.AIConfig.Provider)
|
collector.Data.Model.Provider = string(req.AIConfig.Provider)
|
||||||
collector.Data.Model.Name = req.AIConfig.ModelName
|
collector.Data.Model.Name = req.AIConfig.ModelName
|
||||||
agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, nil, toolDefs, req.ExtraTools, req.ExtraToolCodes, collector)
|
agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, nil, nil, nil, req.ExtraTools, req.ExtraToolCodes, collector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
summary.Status = "error"
|
summary.Status = "error"
|
||||||
summary.ErrorMessage = err.Error()
|
summary.ErrorMessage = err.Error()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
einoagents "cs-agent/internal/ai/runtime/internal/impl/agents"
|
einoagents "cs-agent/internal/ai/runtime/internal/impl/agents"
|
||||||
einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks"
|
einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks"
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/adk"
|
"github.com/cloudwego/eino/adk"
|
||||||
einobasetool "github.com/cloudwego/eino/components/tool"
|
einobasetool "github.com/cloudwego/eino/components/tool"
|
||||||
@@ -32,7 +33,8 @@ func NewAgentFactory() *AgentFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *models.AIAgent, aiConfig *models.AIConfig,
|
func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *models.AIAgent, aiConfig *models.AIConfig,
|
||||||
selectedSkill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition, extraTools []einobasetool.BaseTool, extraToolCodes map[string]string,
|
selectedSkill *models.SkillDefinition, instructionToolDefinitions []einoadapter.MCPToolDefinition, mcpToolDefinitions []einoadapter.MCPToolDefinition,
|
||||||
|
extraTools []einobasetool.BaseTool, extraToolCodes map[string]string,
|
||||||
collector *einocallbacks.RuntimeTraceCollector) (*einoagents.CustomerServiceAgent, error) {
|
collector *einocallbacks.RuntimeTraceCollector) (*einoagents.CustomerServiceAgent, error) {
|
||||||
if aiAgent == nil || aiConfig == nil {
|
if aiAgent == nil || aiConfig == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -41,7 +43,7 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
baseTools, err := f.toolFactory.BuildBaseToolsByDefinitions(ctx, toolDefinitions)
|
baseTools, err := f.toolFactory.BuildBaseToolsByDefinitions(ctx, mcpToolDefinitions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -50,20 +52,40 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m
|
|||||||
allTools = append(allTools, baseTools...)
|
allTools = append(allTools, baseTools...)
|
||||||
handlers := make([]adk.ChatModelAgentMiddleware, 0, 1)
|
handlers := make([]adk.ChatModelAgentMiddleware, 0, 1)
|
||||||
if collector != nil {
|
if collector != nil {
|
||||||
toolMetadataBy := make(map[string]einocallbacks.ToolMetadata, len(toolDefinitions))
|
toolMetadataBy := make(map[string]einocallbacks.ToolMetadata, len(mcpToolDefinitions)+len(extraToolCodes))
|
||||||
for _, item := range toolDefinitions {
|
for _, item := range mcpToolDefinitions {
|
||||||
toolMetadataBy[item.ModelName] = einocallbacks.ToolMetadata{
|
toolMetadataBy[item.ModelName] = einocallbacks.ToolMetadata{
|
||||||
ToolCode: item.ToolCode,
|
ToolCode: item.ToolCode,
|
||||||
ServerCode: item.ServerCode,
|
ServerCode: item.ServerCode,
|
||||||
ToolName: item.ToolName,
|
ToolName: item.ToolName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for modelName, toolCode := range extraToolCodes {
|
||||||
|
modelName = strings.TrimSpace(modelName)
|
||||||
|
toolCode = strings.TrimSpace(toolCode)
|
||||||
|
if modelName == "" || toolCode == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
serverCode, toolName := "", ""
|
||||||
|
if toolCode == toolx.BuiltinToolSearchToolCode {
|
||||||
|
serverCode = toolx.BuiltinToolCatalogServerCode
|
||||||
|
toolName = toolx.BuiltinToolSearchToolName
|
||||||
|
} else if toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
||||||
|
serverCode = toolx.BuiltinToolCatalogServerCode
|
||||||
|
toolName = toolx.BuiltinCreateTicketConfirmToolName
|
||||||
|
}
|
||||||
|
toolMetadataBy[modelName] = einocallbacks.ToolMetadata{
|
||||||
|
ToolCode: toolCode,
|
||||||
|
ServerCode: serverCode,
|
||||||
|
ToolName: toolName,
|
||||||
|
}
|
||||||
|
}
|
||||||
handlers = append(handlers, einocallbacks.NewRuntimeTraceHandler(collector, toolMetadataBy))
|
handlers = append(handlers, einocallbacks.NewRuntimeTraceHandler(collector, toolMetadataBy))
|
||||||
}
|
}
|
||||||
inner, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
inner, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
||||||
Name: strings.TrimSpace(aiAgent.Name),
|
Name: strings.TrimSpace(aiAgent.Name),
|
||||||
Description: strings.TrimSpace(aiAgent.Description),
|
Description: strings.TrimSpace(aiAgent.Description),
|
||||||
Instruction: buildAgentInstruction(aiAgent, selectedSkill, toolDefinitions, extraToolCodes),
|
Instruction: buildAgentInstruction(aiAgent, selectedSkill, instructionToolDefinitions, extraToolCodes),
|
||||||
Model: chatModel,
|
Model: chatModel,
|
||||||
ToolsConfig: adk.ToolsConfig{
|
ToolsConfig: adk.ToolsConfig{
|
||||||
ToolsNodeConfig: compose.ToolsNodeConfig{
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
||||||
@@ -87,7 +109,16 @@ func buildAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.SkillD
|
|||||||
if skillInstruction := buildSelectedSkillInstruction(selectedSkill, toolDefinitions); skillInstruction != "" {
|
if skillInstruction := buildSelectedSkillInstruction(selectedSkill, toolDefinitions); skillInstruction != "" {
|
||||||
appendixParts = append(appendixParts, skillInstruction)
|
appendixParts = append(appendixParts, skillInstruction)
|
||||||
}
|
}
|
||||||
if hasToolCode(extraToolCodes, "builtin/create_ticket_with_confirmation") {
|
if hasToolCode(extraToolCodes, toolx.BuiltinToolSearchToolCode) {
|
||||||
|
appendixParts = append(appendixParts, strings.TrimSpace(`
|
||||||
|
当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则:
|
||||||
|
1. 先用 query 搜索候选工具,再根据返回的 toolCode 选择真正的目标工具。
|
||||||
|
2. 只有在你已经明确要调用哪个动态工具时,才传入 toolCode 和 arguments 进行执行。
|
||||||
|
3. 不要臆造 toolCode;必须以 tool_search 返回的候选结果为准。
|
||||||
|
4. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
if hasToolCode(extraToolCodes, toolx.BuiltinCreateTicketConfirmToolCode) {
|
||||||
appendixParts = append(appendixParts, strings.TrimSpace(`
|
appendixParts = append(appendixParts, strings.TrimSpace(`
|
||||||
你可以在确认信息充分后调用 create_ticket_with_confirmation 工具来创建工单,但必须遵守以下规则:
|
你可以在确认信息充分后调用 create_ticket_with_confirmation 工具来创建工单,但必须遵守以下规则:
|
||||||
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。
|
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func newService() *service {
|
|||||||
return &service{
|
return &service{
|
||||||
runtime: engine.NewService(),
|
runtime: engine.NewService(),
|
||||||
registry: registry.NewRegistry(
|
registry: registry.NewRegistry(
|
||||||
|
tools.NewToolSearchTool(),
|
||||||
tools.NewCreateTicketConfirmTool(),
|
tools.NewCreateTicketConfirmTool(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -94,7 +95,7 @@ func (s *service) prepareToolsForRun(req *Request) error {
|
|||||||
AIAgent: req.AIAgent,
|
AIAgent: req.AIAgent,
|
||||||
AIConfig: req.AIConfig,
|
AIConfig: req.AIConfig,
|
||||||
UserMessage: req.UserMessage,
|
UserMessage: req.UserMessage,
|
||||||
AllowedToolCodes: resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill),
|
AllowedToolCodes: expandRuntimeAllowedToolCodes(resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -109,9 +110,10 @@ func (s *service) prepareToolsForResume(req *ResumeRequest) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
toolSet, err := s.registry.Resolve(registry.Context{
|
toolSet, err := s.registry.Resolve(registry.Context{
|
||||||
Conversation: req.Conversation,
|
Conversation: req.Conversation,
|
||||||
AIAgent: req.AIAgent,
|
AIAgent: req.AIAgent,
|
||||||
AIConfig: req.AIConfig,
|
AIConfig: req.AIConfig,
|
||||||
|
AllowedToolCodes: expandRuntimeAllowedToolCodes(parseAgentAllowedToolCodes(req.AIAgent)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -275,3 +277,36 @@ func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinit
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expandRuntimeAllowedToolCodes(items []string) []string {
|
||||||
|
ret := make([]string, 0, len(items)+1)
|
||||||
|
hasMCPTool := false
|
||||||
|
for _, item := range items {
|
||||||
|
item = strings.TrimSpace(item)
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, item)
|
||||||
|
serverCode, toolName := toolx.SplitMCPToolCode(item)
|
||||||
|
if serverCode != "" && toolName != "" {
|
||||||
|
hasMCPTool = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasMCPTool {
|
||||||
|
ret = appendIfMissingString(ret, toolx.BuiltinToolSearchToolCode)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendIfMissingString(items []string, value string) []string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if strings.TrimSpace(item) == value {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(items, value)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"cs-agent/internal/ai/mcps"
|
||||||
|
"cs-agent/internal/ai/runtime/registry"
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
|
|
||||||
|
einotool "github.com/cloudwego/eino/components/tool"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
einojsonschema "github.com/eino-contrib/jsonschema"
|
||||||
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ToolSearchToolCode = toolx.BuiltinToolSearchToolCode
|
||||||
|
ToolSearchToolName = toolx.BuiltinToolSearchToolName
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToolSearchTool struct {
|
||||||
|
allowedToolCodes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewToolSearchTool() *ToolSearchTool {
|
||||||
|
return &ToolSearchTool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolSearchTool) Name() string {
|
||||||
|
return ToolSearchToolName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolSearchTool) Code() string {
|
||||||
|
return ToolSearchToolCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolSearchTool) Enabled(ctx registry.Context) bool {
|
||||||
|
return len(filterAllowedMCPToolCodes(ctx.AllowedToolCodes)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolSearchTool) Build(ctx registry.Context) (einotool.BaseTool, error) {
|
||||||
|
if !t.Enabled(ctx) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &ToolSearchTool{
|
||||||
|
allowedToolCodes: filterAllowedMCPToolCodes(ctx.AllowedToolCodes),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolSearchTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||||
|
return &schema.ToolInfo{
|
||||||
|
Name: ToolSearchToolName,
|
||||||
|
Desc: "当你需要使用当前会话允许的长尾 MCP 工具时,先调用本工具搜索合适的 toolCode;确认目标后,可再次调用本工具并传入 toolCode 与 arguments 代理执行。不要用它替代明确固定的内置流程工具。",
|
||||||
|
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
|
||||||
|
Version: einojsonschema.Version,
|
||||||
|
Type: "object",
|
||||||
|
Properties: orderedmap.New[string, *einojsonschema.Schema](orderedmap.WithInitialData(
|
||||||
|
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||||
|
Key: "query",
|
||||||
|
Value: &einojsonschema.Schema{
|
||||||
|
Type: "string",
|
||||||
|
Description: "要搜索的工具意图、能力或关键词;当只想列出候选工具时使用。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||||
|
Key: "toolCode",
|
||||||
|
Value: &einojsonschema.Schema{
|
||||||
|
Type: "string",
|
||||||
|
Description: "已确定目标后要调用的 MCP toolCode,例如 mcp_server/tool_name。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderedmap.Pair[string, *einojsonschema.Schema]{
|
||||||
|
Key: "arguments",
|
||||||
|
Value: &einojsonschema.Schema{
|
||||||
|
Type: "object",
|
||||||
|
Description: "调用目标工具时传入的参数对象。",
|
||||||
|
AdditionalProperties: &einojsonschema.Schema{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
Extra: map[string]any{
|
||||||
|
"toolCode": ToolSearchToolCode,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolSearchTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) {
|
||||||
|
if t == nil {
|
||||||
|
return "", fmt.Errorf("tool search tool is nil")
|
||||||
|
}
|
||||||
|
req, err := parseToolSearchRequest(argumentsInJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if req.ToolCode != "" {
|
||||||
|
return t.invokeTargetTool(ctx, req.ToolCode, req.Arguments)
|
||||||
|
}
|
||||||
|
return t.searchCandidates(ctx, req.Query)
|
||||||
|
}
|
||||||
|
|
||||||
|
type toolSearchRequest struct {
|
||||||
|
Query string
|
||||||
|
ToolCode string
|
||||||
|
Arguments map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
type toolSearchCandidate struct {
|
||||||
|
ToolCode string `json:"toolCode"`
|
||||||
|
ServerCode string `json:"serverCode"`
|
||||||
|
ToolName string `json:"toolName"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseToolSearchRequest(argumentsInJSON string) (*toolSearchRequest, error) {
|
||||||
|
argumentsInJSON = strings.TrimSpace(argumentsInJSON)
|
||||||
|
if argumentsInJSON == "" {
|
||||||
|
return &toolSearchRequest{}, nil
|
||||||
|
}
|
||||||
|
raw := make(map[string]any)
|
||||||
|
if err := json.Unmarshal([]byte(argumentsInJSON), &raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid tool_search arguments: %w", err)
|
||||||
|
}
|
||||||
|
req := &toolSearchRequest{
|
||||||
|
Query: strings.TrimSpace(getStringValue(raw, "query")),
|
||||||
|
ToolCode: strings.TrimSpace(getStringValue(raw, "toolCode")),
|
||||||
|
}
|
||||||
|
if value, ok := raw["arguments"]; ok {
|
||||||
|
args, ok := value.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("tool_search arguments must be an object")
|
||||||
|
}
|
||||||
|
req.Arguments = args
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolSearchTool) searchCandidates(ctx context.Context, query string) (string, error) {
|
||||||
|
candidates, err := t.loadAllowedCandidates(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
matched := filterCandidatesByQuery(candidates, query)
|
||||||
|
if len(matched) == 0 {
|
||||||
|
return "未找到匹配的动态工具,请换个关键词,或继续向用户追问后再搜索。", nil
|
||||||
|
}
|
||||||
|
if len(matched) > 8 {
|
||||||
|
matched = matched[:8]
|
||||||
|
}
|
||||||
|
buf, err := json.Marshal(map[string]any{
|
||||||
|
"query": strings.TrimSpace(query),
|
||||||
|
"total": len(matched),
|
||||||
|
"candidates": matched,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolSearchTool) invokeTargetTool(ctx context.Context, toolCode string, arguments map[string]any) (string, error) {
|
||||||
|
toolCode = strings.TrimSpace(toolCode)
|
||||||
|
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||||
|
if serverCode == "" || toolName == "" {
|
||||||
|
return "", fmt.Errorf("tool_search 只支持调用 MCP toolCode")
|
||||||
|
}
|
||||||
|
if !containsToolCode(t.allowedToolCodes, toolCode) {
|
||||||
|
return "", fmt.Errorf("目标工具未被当前会话授权")
|
||||||
|
}
|
||||||
|
result, err := mcps.Runtime.CallTool(ctx, serverCode, toolName, cloneArguments(arguments))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buildToolCallResultSummary(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolSearchTool) loadAllowedCandidates(ctx context.Context) ([]toolSearchCandidate, error) {
|
||||||
|
serverToToolCodes := make(map[string]map[string]struct{})
|
||||||
|
for _, toolCode := range t.allowedToolCodes {
|
||||||
|
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||||
|
if serverCode == "" || toolName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := serverToToolCodes[serverCode]; !ok {
|
||||||
|
serverToToolCodes[serverCode] = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
serverToToolCodes[serverCode][toolCode] = struct{}{}
|
||||||
|
}
|
||||||
|
serverCodes := make([]string, 0, len(serverToToolCodes))
|
||||||
|
for serverCode := range serverToToolCodes {
|
||||||
|
serverCodes = append(serverCodes, serverCode)
|
||||||
|
}
|
||||||
|
slices.Sort(serverCodes)
|
||||||
|
ret := make([]toolSearchCandidate, 0)
|
||||||
|
for _, serverCode := range serverCodes {
|
||||||
|
tools, err := mcps.Runtime.ListTools(ctx, serverCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allowed := serverToToolCodes[serverCode]
|
||||||
|
for _, item := range tools {
|
||||||
|
toolCode := toolx.BuildMCPToolCode(serverCode, item.Name)
|
||||||
|
if _, ok := allowed[toolCode]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, toolSearchCandidate{
|
||||||
|
ToolCode: toolCode,
|
||||||
|
ServerCode: serverCode,
|
||||||
|
ToolName: strings.TrimSpace(item.Name),
|
||||||
|
Title: strings.TrimSpace(item.Title),
|
||||||
|
Description: strings.TrimSpace(item.Description),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterAllowedMCPToolCodes(input []string) []string {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := make([]string, 0, len(input))
|
||||||
|
for _, item := range input {
|
||||||
|
item = strings.TrimSpace(item)
|
||||||
|
serverCode, toolName := toolx.SplitMCPToolCode(item)
|
||||||
|
if serverCode == "" || toolName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsToolCode(items []string, target string) bool {
|
||||||
|
target = strings.TrimSpace(target)
|
||||||
|
if target == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if strings.TrimSpace(item) == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterCandidatesByQuery(candidates []toolSearchCandidate, query string) []toolSearchCandidate {
|
||||||
|
query = strings.TrimSpace(strings.ToLower(query))
|
||||||
|
if query == "" {
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
ret := make([]toolSearchCandidate, 0, len(candidates))
|
||||||
|
for _, item := range candidates {
|
||||||
|
searchText := strings.ToLower(strings.Join([]string{
|
||||||
|
item.ToolCode,
|
||||||
|
item.ServerCode,
|
||||||
|
item.ToolName,
|
||||||
|
item.Title,
|
||||||
|
item.Description,
|
||||||
|
}, "\n"))
|
||||||
|
if strings.Contains(searchText, query) {
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneArguments(input map[string]any) map[string]any {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return map[string]any{}
|
||||||
|
}
|
||||||
|
ret := make(map[string]any, len(input))
|
||||||
|
for key, value := range input {
|
||||||
|
ret[key] = value
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildToolCallResultSummary(result *mcps.ToolCallResult) string {
|
||||||
|
if result == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
lines := make([]string, 0, len(result.Content)+2)
|
||||||
|
if result.IsError {
|
||||||
|
lines = append(lines, "tool returned an error")
|
||||||
|
}
|
||||||
|
if result.StructuredContent != nil {
|
||||||
|
if data, err := json.Marshal(result.StructuredContent); err == nil {
|
||||||
|
lines = append(lines, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, item := range result.Content {
|
||||||
|
switch item.Type {
|
||||||
|
case "text":
|
||||||
|
if text := strings.TrimSpace(item.Text); text != "" {
|
||||||
|
lines = append(lines, text)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if item.Data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if data, err := json.Marshal(item.Data); err == nil {
|
||||||
|
lines = append(lines, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(strings.Join(lines, "\n"))
|
||||||
|
}
|
||||||
@@ -176,7 +176,10 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
}
|
}
|
||||||
serverCode := strings.TrimSpace(tool.ServerCode)
|
serverCode := strings.TrimSpace(tool.ServerCode)
|
||||||
toolName := strings.TrimSpace(tool.ToolName)
|
toolName := strings.TrimSpace(tool.ToolName)
|
||||||
if toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
if toolCode == toolx.BuiltinToolSearchToolCode {
|
||||||
|
serverCode = toolx.BuiltinToolCatalogServerCode
|
||||||
|
toolName = toolx.BuiltinToolSearchToolName
|
||||||
|
} else if toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
||||||
serverCode = toolx.BuiltinToolCatalogServerCode
|
serverCode = toolx.BuiltinToolCatalogServerCode
|
||||||
toolName = toolx.BuiltinCreateTicketConfirmToolName
|
toolName = toolx.BuiltinCreateTicketConfirmToolName
|
||||||
} else if parsedServerCode, parsedToolName := toolx.SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
|
} else if parsedServerCode, parsedToolName := toolx.SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
|
||||||
@@ -184,12 +187,22 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
toolName = parsedToolName
|
toolName = parsedToolName
|
||||||
}
|
}
|
||||||
title := strings.TrimSpace(tool.Title)
|
title := strings.TrimSpace(tool.Title)
|
||||||
if title == "" && toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
if title == "" {
|
||||||
title = toolx.BuiltinCreateTicketConfirmToolTitle
|
switch toolCode {
|
||||||
|
case toolx.BuiltinToolSearchToolCode:
|
||||||
|
title = toolx.BuiltinToolSearchToolTitle
|
||||||
|
case toolx.BuiltinCreateTicketConfirmToolCode:
|
||||||
|
title = toolx.BuiltinCreateTicketConfirmToolTitle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
description := strings.TrimSpace(tool.Description)
|
description := strings.TrimSpace(tool.Description)
|
||||||
if description == "" && toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
if description == "" {
|
||||||
description = toolx.BuiltinCreateTicketConfirmToolDescription
|
switch toolCode {
|
||||||
|
case toolx.BuiltinToolSearchToolCode:
|
||||||
|
description = toolx.BuiltinToolSearchToolDescription
|
||||||
|
case toolx.BuiltinCreateTicketConfirmToolCode:
|
||||||
|
description = toolx.BuiltinCreateTicketConfirmToolDescription
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{
|
ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{
|
||||||
ToolCode: toolCode,
|
ToolCode: toolCode,
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package toolx
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
BuiltinToolCatalogServerCode = "builtin"
|
BuiltinToolCatalogServerCode = "builtin"
|
||||||
|
BuiltinToolSearchToolCode = "builtin/tool_search"
|
||||||
|
BuiltinToolSearchToolName = "tool_search"
|
||||||
|
BuiltinToolSearchToolTitle = "搜索并调用动态工具"
|
||||||
|
BuiltinToolSearchToolDescription = "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。"
|
||||||
BuiltinCreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation"
|
BuiltinCreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation"
|
||||||
BuiltinCreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
BuiltinCreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
||||||
BuiltinCreateTicketConfirmToolTitle = "创建工单并发起确认"
|
BuiltinCreateTicketConfirmToolTitle = "创建工单并发起确认"
|
||||||
|
|||||||
@@ -32,21 +32,7 @@ type MCPToolCatalogItem struct {
|
|||||||
|
|
||||||
func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalogItem, error) {
|
func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalogItem, error) {
|
||||||
cfg := config.Current()
|
cfg := config.Current()
|
||||||
if !cfg.MCP.Enabled {
|
ret := make([]MCPToolCatalogItem, 0, 2)
|
||||||
return nil, errorsx.InvalidParam("MCP未启用")
|
|
||||||
}
|
|
||||||
if len(cfg.MCP.Servers) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
serverCodes := make([]string, 0, len(cfg.MCP.Servers))
|
|
||||||
for serverCode, server := range cfg.MCP.Servers {
|
|
||||||
if !server.Enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
serverCodes = append(serverCodes, serverCode)
|
|
||||||
}
|
|
||||||
slices.Sort(serverCodes)
|
|
||||||
ret := make([]MCPToolCatalogItem, 0)
|
|
||||||
ret = append(ret, MCPToolCatalogItem{
|
ret = append(ret, MCPToolCatalogItem{
|
||||||
ToolCode: toolx.BuiltinCreateTicketConfirmToolCode,
|
ToolCode: toolx.BuiltinCreateTicketConfirmToolCode,
|
||||||
ServerCode: toolx.BuiltinToolCatalogServerCode,
|
ServerCode: toolx.BuiltinToolCatalogServerCode,
|
||||||
@@ -55,6 +41,25 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog
|
|||||||
Title: toolx.BuiltinCreateTicketConfirmToolTitle,
|
Title: toolx.BuiltinCreateTicketConfirmToolTitle,
|
||||||
Description: toolx.BuiltinCreateTicketConfirmToolDescription,
|
Description: toolx.BuiltinCreateTicketConfirmToolDescription,
|
||||||
})
|
})
|
||||||
|
if !cfg.MCP.Enabled {
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
ret = append(ret, MCPToolCatalogItem{
|
||||||
|
ToolCode: toolx.BuiltinToolSearchToolCode,
|
||||||
|
ServerCode: toolx.BuiltinToolCatalogServerCode,
|
||||||
|
ToolName: toolx.BuiltinToolSearchToolName,
|
||||||
|
SourceType: toolx.BuiltinToolCatalogServerCode,
|
||||||
|
Title: toolx.BuiltinToolSearchToolTitle,
|
||||||
|
Description: toolx.BuiltinToolSearchToolDescription,
|
||||||
|
})
|
||||||
|
serverCodes := make([]string, 0, len(cfg.MCP.Servers))
|
||||||
|
for serverCode, server := range cfg.MCP.Servers {
|
||||||
|
if !server.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
serverCodes = append(serverCodes, serverCode)
|
||||||
|
}
|
||||||
|
slices.Sort(serverCodes)
|
||||||
for _, serverCode := range serverCodes {
|
for _, serverCode := range serverCodes {
|
||||||
tools, err := mcps.Runtime.ListTools(ctx, serverCode)
|
tools, err := mcps.Runtime.ListTools(ctx, serverCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -86,7 +91,8 @@ func (s *toolCatalogService) ValidateToolCode(toolCode string) error {
|
|||||||
if toolCode == "" {
|
if toolCode == "" {
|
||||||
return errorsx.InvalidParam("toolCode不能为空")
|
return errorsx.InvalidParam("toolCode不能为空")
|
||||||
}
|
}
|
||||||
if toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
switch toolCode {
|
||||||
|
case toolx.BuiltinToolSearchToolCode, toolx.BuiltinCreateTicketConfirmToolCode:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||||
|
|||||||
Reference in New Issue
Block a user