Files
ragflow/internal/agent/tool/yahoo_finance.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

244 lines
8.0 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 tool
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
)
const yahooFinanceToolName = "yahoo_finance"
const yahooFinanceToolDescription = "The Yahoo Finance service provides access to real-time and historical stock market data, company profiles, and financial news."
const yahooFinanceStockCodeDescription = "The stock code or company name."
// yahooFinanceParams contains the model-emitted stock code and the supported
// Canvas-side information switch. Info exposes only StockCode.
type yahooFinanceParams struct {
StockCode string `json:"stock_code"`
Info bool `json:"info"`
}
// yahooFinanceResponse is the upstream Yahoo Finance /v7/finance/quote
// envelope.
type yahooFinanceResponse struct {
QuoteResponse struct {
Result []map[string]any `json:"result"`
Error any `json:"error,omitempty"`
} `json:"quoteResponse"`
}
// yahooFinanceEnvelope is the tool-to-component transport shape.
type yahooFinanceEnvelope struct {
Report string `json:"report"`
Error string `json:"_ERROR,omitempty"`
}
// yahooFinanceEndpoint is the Yahoo Finance quote URL. Exposed as a
// package var so tests can substitute a httptest.Server URL.
var yahooFinanceEndpoint = "https://query1.finance.yahoo.com/v7/finance/quote"
// YahooFinanceTool is the
// Yahoo Finance quote tool.
// It performs an unauthenticated GET against the public quote API
// via the shared HTTPHelper and returns the parsed quote records.
type YahooFinanceTool struct {
helper *HTTPHelper
defaults yahooFinanceParams
}
var _ ToolComponent = (*YahooFinanceTool)(nil)
// NewYahooFinanceTool returns a YahooFinanceTool using the default
// HTTPHelper.
func NewYahooFinanceTool() *YahooFinanceTool {
return NewYahooFinanceToolWithDefaults(nil, defaultYahooFinanceParams())
}
// NewYahooFinanceToolWith returns a YahooFinanceTool that uses the
// provided HTTPHelper. Useful for tests.
func NewYahooFinanceToolWith(h *HTTPHelper) *YahooFinanceTool {
return NewYahooFinanceToolWithDefaults(h, defaultYahooFinanceParams())
}
// NewYahooFinanceToolWithDefaults returns a YahooFinanceTool with Canvas-side
// defaults. This follows the same constructor pattern as GitHubTool.
func NewYahooFinanceToolWithDefaults(h *HTTPHelper, defaults yahooFinanceParams) *YahooFinanceTool {
if h == nil {
// ToolParamBase defaults to max_retries=0, so the tool itself performs
// one request unless a caller injects a differently configured helper.
h = NewHTTPHelperWithRetry(RetryConfig{MaxAttempts: 1})
}
return &YahooFinanceTool{helper: h, defaults: defaults}
}
func defaultYahooFinanceParams() yahooFinanceParams {
return yahooFinanceParams{Info: true}
}
// Info returns the tool's metadata for the chat model.
func (y *YahooFinanceTool) Info(_ context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: yahooFinanceToolName,
Desc: yahooFinanceToolDescription,
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"stock_code": {
Type: schema.String,
Desc: yahooFinanceStockCodeDescription,
Required: true,
},
}),
}, nil
}
func (y *YahooFinanceTool) ComponentSpec() ComponentSpec {
return ComponentSpec{
Inputs: map[string]string{
"stock_code": yahooFinanceStockCodeDescription,
},
Outputs: map[string]string{"report": "Yahoo Finance data formatted as Markdown."},
InputForm: map[string]any{
"stock_code": map[string]any{"type": "line", "name": "Stock code/Company name"},
},
}
}
// buildYahooFinanceURL composes the quote URL for one Python-compatible
// stock_code input. Centralized for testability.
func buildYahooFinanceURL(stockCode string) string {
q := url.Values{}
q.Set("symbols", stockCode)
return yahooFinanceEndpoint + "?" + q.Encode()
}
// InvokableRun performs the Yahoo Finance quote lookup.
func (y *YahooFinanceTool) InvokableRun(ctx context.Context, argsJSON string, _ ...tool.Option) (string, error) {
var p yahooFinanceParams
if err := json.Unmarshal([]byte(argsJSON), &p); err != nil {
return yahooFinanceErrJSON(fmt.Errorf("yahoo_finance: parse arguments: %w", err)),
fmt.Errorf("yahoo_finance: parse arguments: %w", err)
}
p = mergeYahooFinanceParams(y.defaults, p)
p.StockCode = strings.TrimSpace(p.StockCode)
if p.StockCode == "" || !p.Info {
return yahooFinanceJSON(yahooFinanceEnvelope{Report: ""}), nil
}
endpoint := buildYahooFinanceURL(p.StockCode)
// Yahoo Finance returns 401 unless we send a User-Agent that
// looks like a real browser. curl-style UA is the conventional
// workaround for the public (unauthenticated) endpoint.
headers := map[string]string{
"User-Agent": "Mozilla/5.0 (compatible; ragflow/1.0)",
"Accept": "application/json",
}
resp, err := y.helper.Do(ctx, http.MethodGet, endpoint, "", "", headers)
if err != nil {
return yahooFinanceErrJSON(err), err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return yahooFinanceErrJSON(fmt.Errorf("yahoo_finance: upstream returned %d", resp.StatusCode)),
fmt.Errorf("yahoo_finance: upstream returned %d", resp.StatusCode)
}
var raw yahooFinanceResponse
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return yahooFinanceErrJSON(fmt.Errorf("yahoo_finance: decode response: %w", err)),
fmt.Errorf("yahoo_finance: decode response: %w", err)
}
if raw.QuoteResponse.Error != nil {
err := fmt.Errorf("yahoo_finance: upstream error: %v", raw.QuoteResponse.Error)
return yahooFinanceErrJSON(err), err
}
return yahooFinanceJSON(yahooFinanceEnvelope{Report: renderYahooFinanceReport(raw.QuoteResponse.Result)}), nil
}
func (y *YahooFinanceTool) BuildComponentOutputs(envelope map[string]any) map[string]any {
report, _ := envelope["report"].(string)
return map[string]any{"report": report}
}
func mergeYahooFinanceParams(defaults, params yahooFinanceParams) yahooFinanceParams {
params.Info = defaults.Info
return params
}
// renderYahooFinanceReport keeps the existing public quote endpoint while
// returning the string report expected by the Canvas component.
func renderYahooFinanceReport(quotes []map[string]any) string {
if len(quotes) == 0 {
return ""
}
sections := make([]string, 0, len(quotes))
for _, quote := range quotes {
keys := make([]string, 0, len(quote))
for key := range quote {
keys = append(keys, key)
}
sort.Strings(keys)
rows := []string{"# Information:", "| | 0 |", "|:---|:---|"}
for _, key := range keys {
rows = append(rows, fmt.Sprintf("| %s | %s |", markdownCell(key), markdownCell(yahooFinanceValue(quote[key]))))
}
sections = append(sections, strings.Join(rows, "\n"))
}
return strings.Join(sections, "\n\n")
}
func yahooFinanceValue(value any) string {
if value == nil {
return "None"
}
if text, ok := value.(string); ok {
return text
}
if encoded, err := json.Marshal(value); err == nil {
return string(encoded)
}
return fmt.Sprint(value)
}
func markdownCell(value string) string {
value = strings.ReplaceAll(value, "|", `\|`)
return strings.ReplaceAll(value, "\n", "<br>")
}
func yahooFinanceJSON(env yahooFinanceEnvelope) string {
b, err := json.Marshal(env)
if err != nil {
return fmt.Sprintf(`{"_ERROR":"yahoo_finance: marshal result: %s"}`, err)
}
return string(b)
}
func yahooFinanceErrJSON(err error) string {
return yahooFinanceJSON(yahooFinanceEnvelope{Error: err.Error()})
}