diff --git a/internal/agent/component/agent_test.go b/internal/agent/component/agent_test.go index 9f5246e3e2..ea89d21659 100644 --- a/internal/agent/component/agent_test.go +++ b/internal/agent/component/agent_test.go @@ -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 } diff --git a/internal/agent/tool/registry.go b/internal/agent/tool/registry.go index 674e831a6e..2487dce735 100644 --- a/internal/agent/tool/registry.go +++ b/internal/agent/tool/registry.go @@ -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 { diff --git a/internal/agent/tool/registry_test.go b/internal/agent/tool/registry_test.go index 27b695fbef..1f71a40f9f 100644 --- a/internal/agent/tool/registry_test.go +++ b/internal/agent/tool/registry_test.go @@ -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]