fix(go-agent): normalize canvas tool names (#17768)

## Summary

- Normalize Canvas component names before resolving Go Agent tools and
parameters.
- Add regression coverage for CodeExec and other Canvas tool mappings.

## Testing

- `CGO_ENABLED=0 go test -count=1 ./internal/agent/tool
./internal/agent/component`
This commit is contained in:
Hz_
2026-08-04 13:34:59 +08:00
committed by GitHub
parent 61dd263230
commit 539eb470e3
3 changed files with 137 additions and 34 deletions

View File

@@ -654,6 +654,43 @@ func TestAgent_NewAcceptsCanvasToolObjects(t *testing.T) {
}
}
func TestAgent_NewAcceptsCodeExecCanvasToolObject(t *testing.T) {
cmp, err := New("Agent", map[string]any{
"model_id": "stub",
"user_prompt": "x",
"tools": []any{
map[string]any{
"component_name": "CodeExec",
"params": map[string]any{},
},
},
})
if err != nil {
t.Fatalf("New(Agent): %v", err)
}
agent, ok := cmp.(*AgentComponent)
if !ok {
t.Fatalf("New(Agent) returned %T, want *AgentComponent", cmp)
}
if len(agent.param.Tools) != 1 || agent.param.Tools[0] != "CodeExec" {
t.Fatalf("agent.param.Tools = %#v, want [CodeExec]", agent.param.Tools)
}
tools, err := buildAgentTools(t.Context(), agent.param)
if err != nil {
t.Fatalf("buildAgentTools: %v", err)
}
if len(tools) != 1 {
t.Fatalf("len(tools) = %d, want 1", len(tools))
}
info, err := tools[0].Info(t.Context())
if err != nil {
t.Fatalf("CodeExec tool Info: %v", err)
}
if info.Name != "execute_code" {
t.Errorf("CodeExec tool Info().Name = %q, want execute_code", info.Name)
}
}
type fakeToolCallingChatModel struct {
tools []*schema.ToolInfo
}

View File

@@ -55,23 +55,32 @@ var registry = map[string]Factory{
"qweather": noConfig("qweather", func() einotool.BaseTool { return NewQWeatherTool() }),
"querit": buildQueritTool,
"querit_search": buildQueritTool,
"queritsearch": buildQueritTool,
"retrieval": buildRetrievalTool,
"search_my_dataset": buildRetrievalTool,
"search_my_dateset": buildRetrievalTool,
"searxng": buildSearXNGTool,
"tavily": buildTavilyTool,
// Agent DSL tool lists carry the Python Canvas component_name verbatim.
// BuildByName lower-cases names, so register those component names too.
"tavilysearch": buildTavilyTool,
"tavily_extract": buildTavilyExtractTool,
"tavilyextract": buildTavilyExtractTool,
"tushare": noConfig("tushare", func() einotool.BaseTool { return NewTushareTool() }),
"wencai": buildWencaiTool,
"web_crawler": noConfig("web_crawler", func() einotool.BaseTool { return NewCrawlerTool() }),
"wikipedia": buildWikipediaTool,
"wikipedia_search": buildWikipediaTool,
"yahoo_finance": buildYahooFinanceTool,
"tavily_extract": buildTavilyExtractTool,
"tushare": noConfig("tushare", func() einotool.BaseTool { return NewTushareTool() }),
"wencai": buildWencaiTool,
"web_crawler": noConfig("web_crawler", func() einotool.BaseTool { return NewCrawlerTool() }),
"wikipedia": buildWikipediaTool,
"wikipedia_search": buildWikipediaTool,
"yahoo_finance": buildYahooFinanceTool,
}
// canvasToolNames maps lower-cased Canvas component names to the canonical
// registry keys used by the Go Agent tool layer. Canvas preserves component
// names such as "CodeExec" and "GoogleScholar", while the tool registry uses
// snake_case names for several tools.
var canvasToolNames = map[string]string{
"codeexec": "code_exec",
"googlescholar": "google_scholar",
"keenablesearch": "keenable",
"queritsearch": "querit_search",
"tavilyextract": "tavily_extract",
"tavilysearch": "tavily",
"yahoofinance": "yahoo_finance",
}
func noConfig(name string, fn func() einotool.BaseTool) Factory {
@@ -85,7 +94,7 @@ func noConfig(name string, fn func() einotool.BaseTool) Factory {
// BuildByName resolves a tool name into an Eino BaseTool.
func BuildByName(name string, params map[string]any) (einotool.BaseTool, error) {
key := strings.ToLower(strings.TrimSpace(name))
key := normalizeToolName(name)
if key == "" {
return nil, fmt.Errorf("agent tool: empty tool name")
}
@@ -109,7 +118,14 @@ func BuildAll(names []string, perToolParams map[string]map[string]any) ([]einoto
for _, name := range names {
var params map[string]any
if perToolParams != nil {
params = perToolParams[strings.ToLower(strings.TrimSpace(name))]
// Prefer the canonical key so callers can provide params using the
// Go registry name even when the tool list uses a Canvas name.
params = perToolParams[normalizeToolName(name)]
if params == nil {
// Canvas DSL extraction currently keys object params by the
// lower-cased component name, so retain that lookup as a fallback.
params = perToolParams[strings.ToLower(strings.TrimSpace(name))]
}
if params == nil {
params = perToolParams[name]
}
@@ -123,6 +139,17 @@ func BuildAll(names []string, perToolParams map[string]map[string]any) ([]einoto
return tools, nil
}
// normalizeToolName returns the canonical registry key for a DSL or Agent
// tool name. It lower-cases ordinary names and translates Canvas component
// names whose spelling differs from the Go registry key.
func normalizeToolName(name string) string {
key := strings.ToLower(strings.TrimSpace(name))
if canonical, ok := canvasToolNames[key]; ok {
return canonical
}
return key
}
func buildAkShareTool(params map[string]any) (einotool.BaseTool, error) {
topN := defaultAkShareTopN
if len(params) != 0 {

View File

@@ -40,27 +40,67 @@ func TestBuildAll_UnknownTool(t *testing.T) {
}
}
func TestBuildByName_TavilyCanvasComponentNames(t *testing.T) {
for _, tc := range []struct {
name string
func TestBuildByName_CanvasComponentNames(t *testing.T) {
tests := []struct {
name string
wantToolName string
}{
{name: "TavilySearch"},
{name: "TavilyExtract"},
{name: "CodeExec", wantToolName: "execute_code"},
{name: "GoogleScholar", wantToolName: "google_scholar_search"},
{name: "KeenableSearch", wantToolName: "keenable_search"},
{name: "QueritSearch", wantToolName: "querit_search"},
{name: "TavilyExtract", wantToolName: "tavily_extract"},
{name: "TavilySearch", wantToolName: "tavily_search"},
{name: "YahooFinance", wantToolName: "yahoo_finance"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
built, err := BuildByName(tc.name, nil)
if err != nil {
t.Fatalf("BuildByName(%q): %v", tc.name, err)
}
info, err := built.Info(t.Context())
if err != nil {
t.Fatalf("BuildByName(%q).Info: %v", tc.name, err)
}
if info.Name != tc.wantToolName {
t.Errorf("BuildByName(%q).Info().Name = %q, want %q", tc.name, info.Name, tc.wantToolName)
}
})
}
}
func TestBuildAll_CanvasComponentNameUsesCanonicalParams(t *testing.T) {
for _, tc := range []struct {
name string
paramsKey string
topN int
}{
{name: "canonical key", paramsKey: "google_scholar", topN: 7},
{name: "canvas key", paramsKey: "googlescholar", topN: 9},
} {
built, err := BuildByName(tc.name, nil)
if err != nil {
t.Fatalf("BuildByName(%q): %v", tc.name, err)
}
switch tc.name {
case "TavilySearch":
if _, ok := built.(*TavilyTool); !ok {
t.Errorf("BuildByName(%q) returned %T, want *TavilyTool", tc.name, built)
t.Run(tc.name, func(t *testing.T) {
tools, err := BuildAll(
[]string{"GoogleScholar"},
map[string]map[string]any{
tc.paramsKey: {"top_n": tc.topN},
},
)
if err != nil {
t.Fatalf("BuildAll: %v", err)
}
case "TavilyExtract":
if _, ok := built.(*TavilyExtractTool); !ok {
t.Errorf("BuildByName(%q) returned %T, want *TavilyExtractTool", tc.name, built)
if len(tools) != 1 {
t.Fatalf("len(tools) = %d, want 1", len(tools))
}
}
scholar, ok := tools[0].(*GoogleScholarTool)
if !ok {
t.Fatalf("tools[0] = %T, want *GoogleScholarTool", tools[0])
}
if scholar.defaults.TopN != tc.topN {
t.Errorf("GoogleScholar defaults.TopN = %d, want %d", scholar.defaults.TopN, tc.topN)
}
})
}
}
@@ -100,7 +140,7 @@ func TestBuildAll_AllRegisteredTools(t *testing.T) {
"akshare", "arxiv", "bgpt", "code_exec", "crawler", "deepl",
"duckduckgo", "email", "exesql", "execute_sql", "github", "google",
"google_scholar", "google_scholar_search", "jin10", "keenable", "pubmed", "qweather",
"querit", "querit_search", "queritsearch",
"querit", "querit_search",
"retrieval", "search_my_dataset", "search_my_dateset", "searxng",
"tavily", "tavily_extract", "tushare", "web_crawler", "wencai", "wikipedia", "wikipedia_search",
"yahoo_finance",
@@ -164,7 +204,7 @@ func TestToolRegistry_SchemasAreComplete(t *testing.T) {
"akshare", "arxiv", "bgpt", "code_exec", "crawler", "deepl",
"duckduckgo", "email", "execute_sql", "exesql", "github", "google",
"google_scholar", "google_scholar_search", "jin10", "keenable", "pubmed", "qweather",
"querit", "querit_search", "queritsearch",
"querit", "querit_search",
"retrieval", "search_my_dataset", "search_my_dateset", "searxng",
"tavily", "tavily_extract", "tushare", "web_crawler", "wencai", "wikipedia", "wikipedia_search",
"yahoo_finance",
@@ -236,7 +276,6 @@ func TestToolRegistry_SchemasAreComplete(t *testing.T) {
"wikipedia_search": "wikipedia_search",
"querit": "querit_search",
"querit_search": "querit_search",
"queritsearch": "querit_search",
}
for _, name := range names {
canonical, ok := canonicalByAlias[name]