Files
ragflow/internal/agent/component/tool_component.go
Hz_ a7da78d0d7 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"
/>
2026-07-15 21:42:08 +08:00

132 lines
4.3 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package component
import (
"context"
"encoding/json"
"fmt"
"strings"
"ragflow/internal/agent/runtime"
agenttool "ragflow/internal/agent/tool"
)
// ToolBackedComponent is the single Canvas adapter for tools that implement
// agenttool.ToolComponent.
type ToolBackedComponent struct {
name string
tool agenttool.ToolComponent
spec agenttool.ComponentSpec
}
func newToolComponentFactory(componentName, toolName string) Factory {
return func(params map[string]any) (Component, error) {
base, err := agenttool.BuildByName(toolName, params)
if err != nil {
return nil, err
}
componentTool, ok := base.(agenttool.ToolComponent)
if !ok {
return nil, fmt.Errorf("%s: tool %q does not implement ToolComponent", componentName, toolName)
}
return &ToolBackedComponent{
name: componentName,
tool: componentTool,
spec: componentTool.ComponentSpec(),
}, nil
}
}
func (c *ToolBackedComponent) Name() string { return c.name }
func (c *ToolBackedComponent) Inputs() map[string]string { return c.spec.Inputs }
func (c *ToolBackedComponent) Outputs() map[string]string { return c.spec.Outputs }
func (c *ToolBackedComponent) GetInputForm() map[string]any { return c.spec.InputForm }
func (c *ToolBackedComponent) Invoke(ctx context.Context, inputs map[string]any) (map[string]any, error) {
argsJSON, err := json.Marshal(inputs)
if err != nil {
return nil, fmt.Errorf("canvas: %s: encode inputs: %w", c.name, err)
}
raw, invokeErr := c.tool.InvokableRun(ctx, string(argsJSON))
decoded := parseToolEnvelope(raw)
if rawValue, invalid := decoded["_raw"]; invalid {
if invokeErr != nil {
return nil, fmt.Errorf("canvas: %s: %w", c.name, invokeErr)
}
return nil, fmt.Errorf("canvas: %s: invalid tool result: %v", c.name, rawValue)
}
if existing, _ := decoded["_ERROR"].(string); strings.TrimSpace(existing) != "" {
outputs := c.tool.BuildComponentOutputs(decoded)
if outputs == nil {
outputs = make(map[string]any, 1)
}
outputs["_ERROR"] = existing
return outputs, nil
}
if invokeErr != nil {
return nil, fmt.Errorf("canvas: %s: %w", c.name, invokeErr)
}
if builder, ok := c.tool.(agenttool.ReferenceBuilder); ok {
chunks, docAggs := builder.BuildReferences(ctx, decoded)
if state, _, stateErr := runtime.GetStateFromContext[*runtime.CanvasState](ctx); stateErr == nil && state != nil {
state.SetRetrievalReferences(chunks, docAggs)
}
}
return c.tool.BuildComponentOutputs(decoded), nil
}
func (c *ToolBackedComponent) Stream(_ context.Context, _ map[string]any) (<-chan map[string]any, error) {
return nil, nil
}
var toolComponentRegistrations = []struct {
componentName string
toolName string
}{
{componentName: "GitHub", toolName: "github"},
{componentName: "BGPT", toolName: "bgpt"},
{componentName: "ArXiv", toolName: "arxiv"},
{componentName: "DuckDuckGo", toolName: "duckduckgo"},
{componentName: "Email", toolName: "email"},
{componentName: "ExeSQL", toolName: "execute_sql"},
{componentName: "Google", toolName: "google"},
{componentName: "GoogleScholar", toolName: "google_scholar"},
{componentName: "KeenableSearch", toolName: "keenable"},
{componentName: "PubMed", toolName: "pubmed"},
{componentName: "SearXNG", toolName: "searxng"},
{componentName: "TavilySearch", toolName: "tavily"},
{componentName: "TavilyExtract", toolName: "tavily_extract"},
{componentName: "WenCai", toolName: "wencai"},
{componentName: "Wikipedia", toolName: "wikipedia"},
{componentName: "YahooFinance", toolName: "yahoo_finance"},
}
func init() {
for _, registration := range toolComponentRegistrations {
Register(
registration.componentName,
newToolComponentFactory(registration.componentName, registration.toolName),
)
}
}