feat: enhance tool search functionality and streamline tool management in AI agent
This commit is contained in:
@@ -81,6 +81,10 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) {
|
|||||||
summary.ToolCodes = append(summary.ToolCodes, item.ToolCode)
|
summary.ToolCodes = append(summary.ToolCodes, item.ToolCode)
|
||||||
toolDefsByModelName[item.ModelName] = item.ToolCode
|
toolDefsByModelName[item.ModelName] = item.ToolCode
|
||||||
}
|
}
|
||||||
|
if len(filteredToolDefs) > 0 {
|
||||||
|
summary.ToolCodes = appendIfMissing(summary.ToolCodes, "builtin/tool_search")
|
||||||
|
toolDefsByModelName["tool_search"] = "builtin/tool_search"
|
||||||
|
}
|
||||||
for modelName, toolCode := range req.ExtraToolCodes {
|
for modelName, toolCode := range req.ExtraToolCodes {
|
||||||
toolCode = strings.TrimSpace(toolCode)
|
toolCode = strings.TrimSpace(toolCode)
|
||||||
modelName = strings.TrimSpace(modelName)
|
modelName = strings.TrimSpace(modelName)
|
||||||
@@ -221,6 +225,10 @@ func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, erro
|
|||||||
summary.ToolCodes = append(summary.ToolCodes, item.ToolCode)
|
summary.ToolCodes = append(summary.ToolCodes, item.ToolCode)
|
||||||
toolDefsByModelName[item.ModelName] = item.ToolCode
|
toolDefsByModelName[item.ModelName] = item.ToolCode
|
||||||
}
|
}
|
||||||
|
if len(toolDefs) > 0 {
|
||||||
|
summary.ToolCodes = appendIfMissing(summary.ToolCodes, "builtin/tool_search")
|
||||||
|
toolDefsByModelName["tool_search"] = "builtin/tool_search"
|
||||||
|
}
|
||||||
for modelName, toolCode := range req.ExtraToolCodes {
|
for modelName, toolCode := range req.ExtraToolCodes {
|
||||||
toolCode = strings.TrimSpace(toolCode)
|
toolCode = strings.TrimSpace(toolCode)
|
||||||
modelName = strings.TrimSpace(modelName)
|
modelName = strings.TrimSpace(modelName)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func (h *RuntimeTraceHandler) WrapInvokableToolCall(_ context.Context, endpoint
|
|||||||
}
|
}
|
||||||
h.collector.AddToolItem(item)
|
h.collector.AddToolItem(item)
|
||||||
if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearchToolCode {
|
if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearchToolCode {
|
||||||
h.collector.AddToolSearchItem(buildToolSearchTraceItem(argumentsInJSON, result, err))
|
h.collector.AddToolSearchItem(h.buildToolSearchTraceItem(argumentsInJSON, result, err))
|
||||||
}
|
}
|
||||||
return result, err
|
return result, err
|
||||||
}, nil
|
}, nil
|
||||||
@@ -73,6 +73,13 @@ func (h *RuntimeTraceHandler) resolveToolMetadata(modelToolName string) (ToolMet
|
|||||||
if modelToolName == "" {
|
if modelToolName == "" {
|
||||||
return ToolMetadata{}, false
|
return ToolMetadata{}, false
|
||||||
}
|
}
|
||||||
|
if modelToolName == toolx.BuiltinToolSearchToolName {
|
||||||
|
return ToolMetadata{
|
||||||
|
ToolCode: toolx.BuiltinToolSearchToolCode,
|
||||||
|
ServerCode: toolx.BuiltinToolCatalogServerCode,
|
||||||
|
ToolName: toolx.BuiltinToolSearchToolName,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
metadata, ok := h.toolMetadataBy[modelToolName]
|
metadata, ok := h.toolMetadataBy[modelToolName]
|
||||||
return metadata, ok
|
return metadata, ok
|
||||||
}
|
}
|
||||||
@@ -101,19 +108,13 @@ func previewToolText(text string, limit int) string {
|
|||||||
return string(runes[:limit]) + "..."
|
return string(runes[:limit]) + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildToolSearchTraceItem(argumentsInJSON string, result string, runErr error) ToolSearchTraceItem {
|
func (h *RuntimeTraceHandler) buildToolSearchTraceItem(argumentsInJSON string, result string, runErr error) ToolSearchTraceItem {
|
||||||
item := ToolSearchTraceItem{
|
item := ToolSearchTraceItem{
|
||||||
|
Action: "search",
|
||||||
Status: "ok",
|
Status: "ok",
|
||||||
}
|
}
|
||||||
args := parseToolArguments(argumentsInJSON)
|
args := parseToolArguments(argumentsInJSON)
|
||||||
item.Query = strings.TrimSpace(readToolSearchString(args, "query"))
|
item.Query = strings.TrimSpace(readToolSearchString(args, "regex_pattern"))
|
||||||
item.TargetToolCode = strings.TrimSpace(readToolSearchString(args, "toolCode"))
|
|
||||||
if item.TargetToolCode != "" {
|
|
||||||
item.Action = "invoke"
|
|
||||||
item.TargetServerCode, item.TargetToolName = toolx.SplitMCPToolCode(item.TargetToolCode)
|
|
||||||
} else {
|
|
||||||
item.Action = "search"
|
|
||||||
}
|
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
item.Status = "error"
|
item.Status = "error"
|
||||||
item.ErrorMessage = runErr.Error()
|
item.ErrorMessage = runErr.Error()
|
||||||
@@ -123,8 +124,8 @@ func buildToolSearchTraceItem(argumentsInJSON string, result string, runErr erro
|
|||||||
if err := json.Unmarshal([]byte(strings.TrimSpace(result)), &payload); err != nil {
|
if err := json.Unmarshal([]byte(strings.TrimSpace(result)), &payload); err != nil {
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
candidateItems, _ := payload["candidates"].([]any)
|
selectedTools, _ := payload["selectedTools"].([]any)
|
||||||
item.CandidateToolCodes = extractCandidateToolCodes(candidateItems)
|
item.CandidateToolCodes = h.extractSelectedToolCodes(selectedTools)
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,18 +141,24 @@ func readToolSearchString(data map[string]any, key string) string {
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractCandidateToolCodes(items []any) []string {
|
func (h *RuntimeTraceHandler) extractSelectedToolCodes(items []any) []string {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ret := make([]string, 0, len(items))
|
ret := make([]string, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
payload, ok := item.(map[string]any)
|
toolName, ok := item.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
toolCode, _ := payload["toolCode"].(string)
|
toolName = strings.TrimSpace(toolName)
|
||||||
toolCode = strings.TrimSpace(toolCode)
|
if toolName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
toolCode := toolName
|
||||||
|
if metadata, ok := h.resolveToolMetadata(toolName); ok && strings.TrimSpace(metadata.ToolCode) != "" {
|
||||||
|
toolCode = strings.TrimSpace(metadata.ToolCode)
|
||||||
|
}
|
||||||
if toolCode == "" {
|
if toolCode == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"cs-agent/internal/pkg/toolx"
|
"cs-agent/internal/pkg/toolx"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/adk"
|
"github.com/cloudwego/eino/adk"
|
||||||
|
einotoolsearch "github.com/cloudwego/eino/adk/middlewares/dynamictool/toolsearch"
|
||||||
einobasetool "github.com/cloudwego/eino/components/tool"
|
einobasetool "github.com/cloudwego/eino/components/tool"
|
||||||
"github.com/cloudwego/eino/compose"
|
"github.com/cloudwego/eino/compose"
|
||||||
)
|
)
|
||||||
@@ -43,14 +44,22 @@ 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, mcpToolDefinitions)
|
dynamicTools, err := f.toolFactory.BuildBaseToolsByDefinitions(ctx, mcpToolDefinitions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allTools := make([]einobasetool.BaseTool, 0, len(baseTools)+len(extraTools))
|
allTools := make([]einobasetool.BaseTool, 0, len(extraTools))
|
||||||
allTools = append(allTools, extraTools...)
|
allTools = append(allTools, extraTools...)
|
||||||
allTools = append(allTools, baseTools...)
|
|
||||||
handlers := make([]adk.ChatModelAgentMiddleware, 0, 1)
|
handlers := make([]adk.ChatModelAgentMiddleware, 0, 1)
|
||||||
|
if len(dynamicTools) > 0 {
|
||||||
|
toolSearchHandler, toolSearchErr := einotoolsearch.New(ctx, &einotoolsearch.Config{
|
||||||
|
DynamicTools: dynamicTools,
|
||||||
|
})
|
||||||
|
if toolSearchErr != nil {
|
||||||
|
return nil, toolSearchErr
|
||||||
|
}
|
||||||
|
handlers = append(handlers, toolSearchHandler)
|
||||||
|
}
|
||||||
if collector != nil {
|
if collector != nil {
|
||||||
toolMetadataBy := make(map[string]einocallbacks.ToolMetadata, len(mcpToolDefinitions)+len(extraToolCodes))
|
toolMetadataBy := make(map[string]einocallbacks.ToolMetadata, len(mcpToolDefinitions)+len(extraToolCodes))
|
||||||
for _, item := range mcpToolDefinitions {
|
for _, item := range mcpToolDefinitions {
|
||||||
@@ -109,13 +118,12 @@ 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, toolx.BuiltinToolSearchToolCode) {
|
if len(toolDefinitions) > 0 {
|
||||||
appendixParts = append(appendixParts, strings.TrimSpace(`
|
appendixParts = append(appendixParts, strings.TrimSpace(`
|
||||||
当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则:
|
当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则:
|
||||||
1. 先用 query 搜索候选工具,再根据返回的 toolCode 选择真正的目标工具。
|
1. 先调用 tool_search 搜索需要的动态工具,再继续使用已选中的真实工具。
|
||||||
2. 只有在你已经明确要调用哪个动态工具时,才传入 toolCode 和 arguments 进行执行。
|
2. 不要假设所有长尾工具一开始就可见;只有被 tool_search 选中的工具,后续模型调用才会暴露出来。
|
||||||
3. 不要臆造 toolCode;必须以 tool_search 返回的候选结果为准。
|
3. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。
|
||||||
4. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。
|
|
||||||
`))
|
`))
|
||||||
}
|
}
|
||||||
if hasToolCode(extraToolCodes, toolx.BuiltinCreateTicketConfirmToolCode) {
|
if hasToolCode(extraToolCodes, toolx.BuiltinCreateTicketConfirmToolCode) {
|
||||||
|
|||||||
@@ -580,6 +580,12 @@ func firstToolSearchTargetToolCode(summary *Summary) string {
|
|||||||
if toolCode != "" {
|
if toolCode != "" {
|
||||||
return toolCode
|
return toolCode
|
||||||
}
|
}
|
||||||
|
if len(item.CandidateToolCodes) == 1 {
|
||||||
|
toolCode = strings.TrimSpace(item.CandidateToolCodes[0])
|
||||||
|
if toolCode != "" {
|
||||||
|
return toolCode
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -588,7 +594,8 @@ type runtimeTraceProjection struct {
|
|||||||
ToolSearch struct {
|
ToolSearch struct {
|
||||||
Raw json.RawMessage `json:"-"`
|
Raw json.RawMessage `json:"-"`
|
||||||
Items []struct {
|
Items []struct {
|
||||||
TargetToolCode string `json:"targetToolCode"`
|
TargetToolCode string `json:"targetToolCode"`
|
||||||
|
CandidateToolCodes []string `json:"candidateToolCodes"`
|
||||||
} `json:"items"`
|
} `json:"items"`
|
||||||
} `json:"toolSearch"`
|
} `json:"toolSearch"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -95,7 +94,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: expandRuntimeAllowedToolCodes(resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill)),
|
AllowedToolCodes: resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -113,7 +112,7 @@ func (s *service) prepareToolsForResume(req *ResumeRequest) error {
|
|||||||
Conversation: req.Conversation,
|
Conversation: req.Conversation,
|
||||||
AIAgent: req.AIAgent,
|
AIAgent: req.AIAgent,
|
||||||
AIConfig: req.AIConfig,
|
AIConfig: req.AIConfig,
|
||||||
AllowedToolCodes: expandRuntimeAllowedToolCodes(parseAgentAllowedToolCodes(req.AIAgent)),
|
AllowedToolCodes: parseAgentAllowedToolCodes(req.AIAgent),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -277,36 +276,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user