mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-27 10:52:03 +08:00
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:
@@ -520,5 +520,6 @@ func init() {
|
||||
Register("BGPT", newBGPTComponent)
|
||||
Register("DuckDuckGo", newDuckDuckGoComponent)
|
||||
Register("Google", newGoogleComponent)
|
||||
Register("GoogleScholar", newGoogleScholarComponent)
|
||||
Register("YahooFinance", newYahooFinanceComponent)
|
||||
}
|
||||
|
||||
131
internal/agent/component/google_scholar_component_test.go
Normal file
131
internal/agent/component/google_scholar_component_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user