feat(go-agent): merge Google Scholar node params with runtime inputs (#16802)

## Summary

- align the Go Google Scholar component with the Python-side config
pattern
- merge node-level params with runtime inputs so canvas defaults are
preserved and per-run inputs can override them
- add tests covering node param fallback and runtime override behavior

## Verification

- `bash build.sh --test ./internal/agent/component/... -run
TestGoogleScholar`

<img width="1873" height="1165" alt="image"
src="https://github.com/user-attachments/assets/67198c6f-6a0e-43bf-a500-8e88d82b8751"
/>
This commit is contained in:
Hz_
2026-07-10 13:30:21 +08:00
committed by GitHub
parent 28340f6218
commit 5797f81fea
8 changed files with 574 additions and 82 deletions

View File

@@ -520,5 +520,6 @@ func init() {
Register("BGPT", newBGPTComponent)
Register("DuckDuckGo", newDuckDuckGoComponent)
Register("Google", newGoogleComponent)
Register("GoogleScholar", newGoogleScholarComponent)
Register("YahooFinance", newYahooFinanceComponent)
}

View File

@@ -0,0 +1,131 @@
//
// 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"
"testing"
einotool "github.com/cloudwego/eino/components/tool"
)
type fakeGoogleScholarInvoker struct {
args map[string]any
}
func (f *fakeGoogleScholarInvoker) InvokableRun(_ context.Context, argsJSON string, _ ...einotool.Option) (string, error) {
if err := json.Unmarshal([]byte(argsJSON), &f.args); err != nil {
return "", err
}
return `{"results":[{"title":"Paper","link":"https://example.com","authors":"A Author","year":"2024","snippet":"Abstract"}]}`, nil
}
func TestGoogleScholar_InvokePassesCanvasParams(t *testing.T) {
t.Parallel()
fake := &fakeGoogleScholarInvoker{}
c := newGoogleScholarComponentWithInvoker(fake, nil)
_, err := c.Invoke(context.Background(), map[string]any{
"query": " retrieval augmented generation ",
"top_n": float64(7),
"sort_by": "date",
"year_low": float64(2020),
"year_high": float64(2024),
"patents": false,
})
if err != nil {
t.Fatalf("Invoke errored: %v", err)
}
if got := fake.args["query"]; got != "retrieval augmented generation" {
t.Errorf("query arg = %v, want trimmed query", got)
}
if got := fake.args["top_n"]; got != float64(7) {
t.Errorf("top_n arg = %v, want 7", got)
}
if _, ok := fake.args["max_results"]; ok {
t.Fatalf("max_results should not be sent for GoogleScholar args: %#v", fake.args)
}
if got := fake.args["sort_by"]; got != "date" {
t.Errorf("sort_by arg = %v, want date", got)
}
if got := fake.args["year_low"]; got != float64(2020) {
t.Errorf("year_low arg = %v, want 2020", got)
}
if got := fake.args["year_high"]; got != float64(2024) {
t.Errorf("year_high arg = %v, want 2024", got)
}
if got := fake.args["patents"]; got != false {
t.Errorf("patents arg = %v, want false", got)
}
}
func TestGoogleScholar_InvokeMergesNodeParams(t *testing.T) {
t.Parallel()
fake := &fakeGoogleScholarInvoker{}
c := newGoogleScholarComponentWithInvoker(fake, map[string]any{
"top_n": float64(20),
"sort_by": "date",
"patents": false,
})
_, err := c.Invoke(context.Background(), map[string]any{
"query": "machine learning",
})
if err != nil {
t.Fatalf("Invoke errored: %v", err)
}
if got := fake.args["top_n"]; got != float64(20) {
t.Errorf("top_n arg = %v, want 20 (from node params)", got)
}
if got := fake.args["sort_by"]; got != "date" {
t.Errorf("sort_by arg = %v, want date (from node params)", got)
}
if got := fake.args["patents"]; got != false {
t.Errorf("patents arg = %v, want false (from node params)", got)
}
}
func TestGoogleScholar_InvokeInputsOverrideNodeParams(t *testing.T) {
t.Parallel()
fake := &fakeGoogleScholarInvoker{}
c := newGoogleScholarComponentWithInvoker(fake, map[string]any{
"top_n": float64(20),
"sort_by": "relevance",
})
_, err := c.Invoke(context.Background(), map[string]any{
"query": "deep learning",
"top_n": float64(5),
"sort_by": "date",
})
if err != nil {
t.Fatalf("Invoke errored: %v", err)
}
if got := fake.args["top_n"]; got != float64(5) {
t.Errorf("top_n arg = %v, want 5 (inputs override node params)", got)
}
if got := fake.args["sort_by"]; got != "date" {
t.Errorf("sort_by arg = %v, want date (inputs override node params)", got)
}
}

View File

@@ -1422,6 +1422,155 @@ func (c *yahooFinanceComponent) Stream(_ context.Context, _ map[string]any) (<-c
return nil, nil
}
// googleScholarComponent delegates to internal/agent/tool/GoogleScholarTool.
type googleScholarComponent struct {
inner googleScholarInvoker
params map[string]any
}
type googleScholarInvoker interface {
InvokableRun(ctx context.Context, argsJSON string, opts ...einotool.Option) (string, error)
}
func newGoogleScholarComponent(params map[string]any) (Component, error) {
cloned := make(map[string]any, len(params))
for k, v := range params {
cloned[k] = v
}
return &googleScholarComponent{
inner: agenttool.NewGoogleScholarTool(),
params: cloned,
}, nil
}
func newGoogleScholarComponentWithInvoker(inner googleScholarInvoker, params map[string]any) Component {
cloned := make(map[string]any, len(params))
for k, v := range params {
cloned[k] = v
}
return &googleScholarComponent{inner: inner, params: cloned}
}
func (c *googleScholarComponent) Name() string { return "GoogleScholar" }
func (c *googleScholarComponent) Inputs() map[string]string {
return map[string]string{
"query": "Search query.",
"top_n": "Maximum number of results (default 12).",
"sort_by": "Sort order: relevance or date.",
"year_low": "Earliest publication year to include.",
"year_high": "Latest publication year to include.",
"patents": "Whether to include patents, defaults to true.",
}
}
func (c *googleScholarComponent) Outputs() map[string]string {
return map[string]string{
"formalized_content": "Rendered search results for downstream LLM prompts.",
"json": "Raw result list.",
}
}
func (c *googleScholarComponent) GetInputForm() map[string]any {
return map[string]any{
"query": map[string]any{
"name": "Query",
"type": "line",
},
}
}
func (c *googleScholarComponent) Invoke(ctx context.Context, inputs map[string]any) (map[string]any, error) {
merged := make(map[string]any, len(c.params)+len(inputs))
for k, v := range c.params {
merged[k] = v
}
for k, v := range inputs {
merged[k] = v
}
query := strings.TrimSpace(stringParam(merged["query"]))
if query == "" {
return map[string]any{"formalized_content": "", "json": []any{}}, nil
}
args := map[string]any{
"query": query,
}
if topN := toIntParam(merged["top_n"]); topN > 0 {
args["top_n"] = topN
}
if sortBy := strings.TrimSpace(stringParam(merged["sort_by"])); sortBy != "" {
args["sort_by"] = sortBy
}
if yearLow := toIntParam(merged["year_low"]); yearLow > 0 {
args["year_low"] = yearLow
}
if yearHigh := toIntParam(merged["year_high"]); yearHigh > 0 {
args["year_high"] = yearHigh
}
if patents, ok := merged["patents"].(bool); ok {
args["patents"] = patents
} else {
args["patents"] = true
}
argsJSON, _ := json.Marshal(args)
out, err := c.inner.InvokableRun(ctx, string(argsJSON))
decoded := parseToolEnvelope(out)
if err != nil {
if len(decoded) > 0 {
return map[string]any{
"formalized_content": "",
"json": []any{},
"_ERROR": decoded["_ERROR"],
}, nil
}
return nil, fmt.Errorf("canvas: GoogleScholar: %w", err)
}
results := anySlice(decoded["results"])
return map[string]any{
"formalized_content": renderGoogleScholarResults(results),
"json": results,
}, nil
}
func (c *googleScholarComponent) Stream(_ context.Context, _ map[string]any) (<-chan map[string]any, error) {
return nil, nil
}
func renderGoogleScholarResults(results []any) string {
if len(results) == 0 {
return ""
}
blocks := make([]string, 0, len(results))
for _, item := range results {
m, ok := item.(map[string]any)
if !ok {
continue
}
field := func(key string) string {
v, ok := m[key]
if !ok || v == nil {
return "-"
}
text := strings.TrimSpace(fmt.Sprintf("%v", v))
if text == "" {
return "-"
}
return text
}
blocks = append(blocks, strings.Join([]string{
fmt.Sprintf("Title: %s", field("title")),
fmt.Sprintf("URL: %s", field("link")),
fmt.Sprintf("Authors: %s", field("authors")),
fmt.Sprintf("Year: %s", field("year")),
fmt.Sprintf("Snippet: %s", field("snippet")),
}, "\n"))
}
return strings.Join(blocks, "\n\n")
}
// Compile-time interface checks.
var (
_ Component = (*retrievalComponent)(nil)
@@ -1431,6 +1580,7 @@ var (
_ Component = (*duckDuckGoComponent)(nil)
_ Component = (*exesqlComponent)(nil)
_ Component = (*codeExecComponent)(nil)
_ Component = (*googleScholarComponent)(nil)
_ Component = (*yahooFinanceComponent)(nil)
)
@@ -1441,3 +1591,4 @@ var _ einotool.InvokableTool = (*agenttool.GoogleTool)(nil)
var _ einotool.InvokableTool = (*agenttool.TavilyExtractTool)(nil)
var _ einotool.InvokableTool = (*agenttool.DuckDuckGoTool)(nil)
var _ einotool.InvokableTool = (*agenttool.YahooFinanceTool)(nil)
var _ einotool.InvokableTool = (*agenttool.GoogleScholarTool)(nil)

View File

@@ -10,12 +10,12 @@ import (
// case-insensitive, and returned in sorted order. The expected count is
// read from plan §2.11.10 — P0 (8) + P1 (5) + P2 (4) + P3 (2) + P4 (3) = 22
// at plan completion, plus v1 fixture wrappers/stubs (including Retrieval,
// TavilySearch, TavilyExtract, ExeSQL, Google, BGPT, YahooFinance, Generate,
// Answer, Iteration, and IterationItem) registered by fixture_stubs.go to keep
// the dsl-examples and canvas tool surface compiling. The test allows counts
// between 12 (P0+P1 minus the removed ExitLoop) and 34 (the 22 plan components
// plus the wrappers/stubs currently registered by fixture_stubs.go) to roll
// forward as subsequent batches land.
// TavilySearch, TavilyExtract, ExeSQL, Google, BGPT, YahooFinance, GoogleScholar,
// Generate, Answer, Iteration, and IterationItem) registered by fixture_stubs.go
// to keep the dsl-examples and canvas tool surface compiling. The test allows
// counts between 12 (P0+P1 minus the removed ExitLoop) and 36 (the 22 plan
// components plus the wrappers/stubs currently registered by fixture_stubs.go)
// to roll forward as subsequent batches land.
//
// Note: ExitLoop is intentionally NOT in the registry anymore. The
// canvas engine (internal/agent/canvas/canvas.go's legacyNoOpNames)
@@ -45,8 +45,8 @@ func TestVerifyRegistration_P1(t *testing.T) {
if len(missing) > 0 {
t.Fatalf("missing P0/P1 components: %v (have %d: %v)", missing, len(names), names)
}
if got := len(names); got < 12 || got > 35 {
t.Errorf("expected 12-35 registered (current plan scope + v1 wrappers/stubs), got %d: %v", got, names)
if got := len(names); got < 12 || got > 36 {
t.Errorf("expected 12-36 registered (current plan scope + v1 wrappers/stubs), got %d: %v", got, names)
}
// ExitLoop must NOT be in the registry (legacy compat lives at