refactor(go-agent): unify tool-backed canvas components (#16912)

## Summary

This PR consolidates Eino-backed Agent tools behind the shared
ToolBackedComponent implementation and aligns their Canvas
configuration,
runtime inputs, output conversion, validation, and registration.

## What changed

- Migrated these Canvas components to the unified tool-backed path:
    - Tavily Search and Extract
    - Execute SQL
    - Google
    - Yahoo Finance
    - Email
    - DuckDuckGo
    - Wikipedia
    - Google Scholar
    - ArXiv
    - PubMed
    - BGPT
    - GitHub
    - WenCai
    - SearXNG
    - Keenable Search

- Removed superseded component wrappers and their duplicate tests.
- Added dedicated registry builders for node-level configuration and
validation.
- Kept model-emitted runtime inputs separate from Canvas node
configuration.
- Moved Email defaults, template resolution, recipient parsing, SMTP
execution, and output conversion into the owning tool.
- Added complete ToolComponent specifications for Canvas inputs,
outputs, and input forms.
- Preserved raw upstream fields where downstream workflows may depend on
them.
- Added workflow registration coverage for all migrated component names.
- Kept HTTP Request, Docs Generator, and Browser as standalone
components because they are not Eino-backed tools.

## Testing

Passed:

bash build.sh --test ./internal/agent/tool/...
bash build.sh --test ./internal/agent/component/...
bash build.sh --test ./internal/agent/runtime/...

<img width="2057" height="1111" alt="image"
src="https://github.com/user-attachments/assets/c728d7a3-9d15-4c5c-b0eb-6b77ad0e41ac"
/>
<img width="2057" height="1111" alt="image"
src="https://github.com/user-attachments/assets/ef13119a-ef92-4b43-9061-eee9511bf492"
/>
<img width="2057" height="1111" alt="image"
src="https://github.com/user-attachments/assets/f50f2c30-eab0-463c-b0b3-0a02523219f1"
/>
This commit is contained in:
Hz_
2026-07-15 21:42:08 +08:00
committed by GitHub
parent d55de09b7d
commit a7da78d0d7
57 changed files with 4779 additions and 5325 deletions

View File

@@ -147,21 +147,30 @@ func TestGitHub_ParseResponse(t *testing.T) {
}
}
func TestGitHub_RequiresQuery(t *testing.T) {
func TestGitHub_EmptyQueryReturnsEmptyResults(t *testing.T) {
t.Parallel()
tool := NewGitHubTool()
_, err := tool.InvokableRun(context.Background(), `{"query":""}`)
if err == nil {
t.Fatal("expected error for empty query")
out, err := tool.InvokableRun(context.Background(), `{"query":""}`)
if err != nil {
t.Fatalf("InvokableRun(empty query): %v", err)
}
if !strings.Contains(err.Error(), "query") {
t.Errorf("err = %v, want to mention query", err)
var envelope githubEnvelope
if err := json.Unmarshal([]byte(out), &envelope); err != nil {
t.Fatalf("decode empty result: %v", err)
}
if len(envelope.Results) != 0 || envelope.Error != "" {
t.Fatalf("empty query result = %#v", envelope)
}
}
func TestGitHub_BuildByNameUsesPythonNodeParams(t *testing.T) {
built, err := BuildByName("github", map[string]any{"top_n": float64(17)})
built, err := BuildByName("github", map[string]any{
"top_n": float64(17),
"query": "runtime query",
"outputs": map[string]any{"json": map[string]any{}},
"setups": map[string]any{"query": "configured query"},
})
if err != nil {
t.Fatalf("BuildByName(github): %v", err)
}
@@ -187,8 +196,74 @@ func TestGitHub_BuildByNameUsesPythonNodeParams(t *testing.T) {
if _, err := BuildByName("github", map[string]any{"top_n": 101}); err == nil {
t.Fatal("BuildByName(github) accepted top_n above GitHub's per_page limit")
}
if _, err := BuildByName("github", map[string]any{"max_results": 5}); err == nil {
t.Fatal("BuildByName(github) accepted removed max_results parameter")
ignored, err := BuildByName("github", map[string]any{"max_results": 5})
if err != nil {
t.Fatalf("BuildByName(github) rejected unrelated Canvas params: %v", err)
}
if ignored.(*GitHubTool).defaults.TopN != defaultGitHubTopN {
t.Fatalf("unrelated params changed top_n: %d", ignored.(*GitHubTool).defaults.TopN)
}
}
func TestGitHub_ComponentContractMatchesPython(t *testing.T) {
github := NewGitHubTool()
spec := github.ComponentSpec()
if _, ok := spec.Outputs["json"]; !ok {
t.Fatalf("component outputs missing json: %#v", spec.Outputs)
}
if _, ok := spec.Outputs["formalized_content"]; !ok {
t.Fatalf("component outputs missing formalized_content: %#v", spec.Outputs)
}
if query, ok := spec.InputForm["query"].(map[string]any); !ok || query["name"] != "Query" || query["type"] != "line" {
t.Fatalf("query input form = %#v", spec.InputForm["query"])
}
}
func TestGitHub_ReferencesAndOutputsPreserveRawResults(t *testing.T) {
github := NewGitHubTool()
results := []any{map[string]any{
"name": "ragflow",
"html_url": "https://github.com/infiniflow/ragflow",
"description": "RAG engine",
"watchers": float64(12000),
"private": false,
}}
repository := results[0].(map[string]any)
if repository["private"] != false {
t.Fatalf("raw repository fields were lost: %#v", repository)
}
envelope := map[string]any{"results": results}
chunks, docAggs := github.BuildReferences(context.Background(), envelope)
if len(chunks) != 1 || len(docAggs) != 1 {
t.Fatalf("references = %#v / %#v", chunks, docAggs)
}
if chunks[0]["document_name"] != "ragflow" || chunks[0]["similarity"] != 1 {
t.Fatalf("reference metadata = %#v", chunks[0])
}
outputs := github.BuildComponentOutputs(envelope)
if _, exists := envelope["chunks"]; exists {
t.Fatalf("component output conversion mutated the tool envelope: %#v", envelope)
}
if results, ok := outputs["json"].([]any); !ok || len(results) != 1 {
t.Fatalf("component json output = %#v", outputs["json"])
}
rendered, _ := outputs["formalized_content"].(string)
for _, want := range []string{"Title: ragflow", "URL: https://github.com/infiniflow/ragflow", "RAG engine\n stars:12000"} {
if !strings.Contains(rendered, want) {
t.Fatalf("rendered results missing %q: %q", want, rendered)
}
}
}
func TestGitHub_LimitReferencesKeepsBoundaryChunk(t *testing.T) {
chunks := []map[string]any{
{"content": "first repository description"},
{"content": "second repository description"},
}
limited := limitGitHubReferences(chunks, 1)
if len(limited) != 1 {
t.Fatalf("limited chunks = %d, want 1", len(limited))
}
}