From d03a360fb12554076ac02686062af0dc5b2aa53d Mon Sep 17 00:00:00 2001 From: Hz_ Date: Tue, 7 Jul 2026 11:15:05 +0800 Subject: [PATCH] fix(go-agent): add BGPT component and input form (#16684) ## Summary Adds the missing input form metadata for the Go BGPT canvas component. ## Root Cause The standalone BGPT component was registered in Go, but it did not implement GetInputForm(). During component trial run, the backend asks the component for its input_form. Since BGPT had none, the API returned: component has no input_form: BGPT: Python BGPT already exposes the query input form, so the Go component needed the same contract. ## Change Added GetInputForm() to the Go BGPT component with a single query line input. Added test coverage to ensure BGPT exposes the input form. ## Validation Backend: bash build.sh --test -run TestBGPT ./internal/agent/component image image --- .../agent/component/bgpt_component_test.go | 108 ++++++++++++ internal/agent/component/fixture_stubs.go | 1 + .../agent/component/universe_a_wrappers.go | 159 ++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 internal/agent/component/bgpt_component_test.go diff --git a/internal/agent/component/bgpt_component_test.go b/internal/agent/component/bgpt_component_test.go new file mode 100644 index 000000000..8b7265a8c --- /dev/null +++ b/internal/agent/component/bgpt_component_test.go @@ -0,0 +1,108 @@ +// +// 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" + "strings" + "testing" + + einotool "github.com/cloudwego/eino/components/tool" +) + +type fakeBGPTInvoker struct { + args map[string]any +} + +func (f *fakeBGPTInvoker) 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 A","authors":"Lee, Kim","journal":"Science","year":"2026","doi":"10.1/a","abstract":"Abstract A","methods":"RCT","sample_size":"120","results":"Improved outcomes","limitations":"Small cohort","conflict_of_interest":"None","data_availability":"Available","blind_spots":"Long term effects","falsify":"Run a larger trial"}]}`, nil +} + +func TestBGPT_RegisteredFactory(t *testing.T) { + c, err := New("BGPT", nil) + if err != nil { + t.Fatalf("New(BGPT) errored: %v", err) + } + if got := c.Name(); got != "BGPT" { + t.Fatalf("Name() = %q, want BGPT", got) + } + formGetter, ok := c.(interface{ GetInputForm() map[string]any }) + if !ok { + t.Fatal("BGPT component does not expose GetInputForm") + } + form := formGetter.GetInputForm() + query, ok := form["query"].(map[string]any) + if !ok { + t.Fatalf("GetInputForm()[query] has type %T, want map", form["query"]) + } + if query["type"] != "line" { + t.Fatalf("GetInputForm()[query][type] = %v, want line", query["type"]) + } + if _, ok := c.Outputs()["formalized_content"]; !ok { + t.Fatal("Outputs() missing formalized_content") + } + if _, ok := c.Outputs()["json"]; !ok { + t.Fatal("Outputs() missing json") + } +} + +func TestBGPT_InvokeAdaptsCanvasInputsAndOutputs(t *testing.T) { + fake := &fakeBGPTInvoker{} + c := newBGPTComponentWithInvoker(fake) + + out, err := c.Invoke(context.Background(), map[string]any{ + "query": " cancer therapy ", + "api_key": "key-1", + "days_back": float64(30), + "top_n": float64(3), + }) + if err != nil { + t.Fatalf("Invoke errored: %v", err) + } + + if got := fake.args["query"]; got != "cancer therapy" { + t.Errorf("query arg = %v, want trimmed query", got) + } + if got := fake.args["api_key"]; got != "key-1" { + t.Errorf("api_key arg = %v, want key-1", got) + } + if got := fake.args["days_back"]; got != float64(30) { + t.Errorf("days_back arg = %v, want 30", got) + } + if got := fake.args["num_results"]; got != float64(3) { + t.Errorf("num_results arg = %v, want 3 from top_n", got) + } + + formalized, _ := out["formalized_content"].(string) + for _, want := range []string{"Paper A", "Lee, Kim", "Improved outcomes", "Run a larger trial"} { + if !strings.Contains(formalized, want) { + t.Errorf("formalized_content missing %q: %s", want, formalized) + } + } + + results, ok := out["json"].([]any) + if !ok { + t.Fatalf("json output has type %T, want []any", out["json"]) + } + if len(results) != 1 { + t.Fatalf("json output length = %d, want 1", len(results)) + } +} diff --git a/internal/agent/component/fixture_stubs.go b/internal/agent/component/fixture_stubs.go index 2e8042747..c8d52a909 100644 --- a/internal/agent/component/fixture_stubs.go +++ b/internal/agent/component/fixture_stubs.go @@ -505,5 +505,6 @@ func init() { Register(componentNameAnswer, NewAnswerStub) Register(componentNameIteration, NewIterationStub) Register(componentNameIterationItem, NewIterationItemStub) + Register("BGPT", newBGPTComponent) Register("YahooFinance", newYahooFinanceComponent) } diff --git a/internal/agent/component/universe_a_wrappers.go b/internal/agent/component/universe_a_wrappers.go index 3ec8f7e6a..6f292ea39 100644 --- a/internal/agent/component/universe_a_wrappers.go +++ b/internal/agent/component/universe_a_wrappers.go @@ -91,6 +91,165 @@ func (c *tavilySearchComponent) Stream(_ context.Context, _ map[string]any) (<-c return nil, nil } +// bgptInvoker is the subset of BGPTTool used by the canvas wrapper. +type bgptInvoker interface { + InvokableRun(ctx context.Context, argsJSON string, opts ...einotool.Option) (string, error) +} + +// bgptComponent delegates to internal/agent/tool/BGPTTool and adapts +// the tool envelope to the BGPT canvas output contract. +type bgptComponent struct { + inner bgptInvoker +} + +func newBGPTComponent(_ map[string]any) (Component, error) { + return newBGPTComponentWithInvoker(agenttool.NewBGPTTool()), nil +} + +func newBGPTComponentWithInvoker(inner bgptInvoker) Component { + return &bgptComponent{inner: inner} +} + +func (c *bgptComponent) Name() string { return "BGPT" } + +func (c *bgptComponent) Inputs() map[string]string { + return map[string]string{ + "query": "Scientific search query.", + "api_key": "Optional BGPT API key.", + "days_back": "Optional recency filter in days.", + "top_n": "Maximum number of results.", + } +} + +func (c *bgptComponent) GetInputForm() map[string]any { + return map[string]any{ + "query": map[string]any{ + "name": "Query", + "type": "line", + }, + } +} + +func (c *bgptComponent) Outputs() map[string]string { + return map[string]string{ + "formalized_content": "Rendered scientific paper evidence for downstream LLM prompts.", + "json": "Raw BGPT result list.", + } +} + +func (c *bgptComponent) Invoke(ctx context.Context, inputs map[string]any) (map[string]any, error) { + query := strings.TrimSpace(stringParam(inputs["query"])) + if query == "" { + return map[string]any{"formalized_content": "", "json": []any{}}, nil + } + args := map[string]any{ + "query": query, + } + if apiKey := strings.TrimSpace(stringParam(inputs["api_key"])); apiKey != "" { + args["api_key"] = apiKey + } + if daysBack := toIntParam(inputs["days_back"]); daysBack > 0 { + args["days_back"] = daysBack + } + if topN := toIntParam(inputs["top_n"]); topN > 0 { + args["num_results"] = topN + } + + 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: BGPT: %w", err) + } + + results := anySlice(decoded["results"]) + return map[string]any{ + "formalized_content": renderBGPTResults(results), + "json": results, + }, nil +} + +func (c *bgptComponent) Stream(_ context.Context, _ map[string]any) (<-chan map[string]any, error) { + return nil, nil +} + +func stringParam(v any) string { + if s, ok := v.(string); ok { + return s + } + return "" +} + +func anySlice(v any) []any { + switch x := v.(type) { + case []any: + return x + case []map[string]any: + out := make([]any, 0, len(x)) + for _, item := range x { + out = append(out, item) + } + return out + default: + return []any{} + } +} + +func renderBGPTResults(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 + } + bgptField := func(key string) string { + v, ok := m[key] + if !ok || v == nil { + return "-" + } + switch vv := v.(type) { + case string: + if text := strings.TrimSpace(vv); text != "" { + return text + } + default: + if text := strings.TrimSpace(fmt.Sprintf("%v", vv)); text != "" { + return text + } + } + return "-" + } + lines := []string{ + fmt.Sprintf("Title: %s", bgptField("title")), + fmt.Sprintf("Authors: %s", bgptField("authors")), + fmt.Sprintf("Journal: %s", bgptField("journal")), + fmt.Sprintf("Year: %s", bgptField("year")), + fmt.Sprintf("DOI: %s", bgptField("doi")), + fmt.Sprintf("Abstract: %s", bgptField("abstract")), + fmt.Sprintf("Methods: %s", bgptField("methods")), + fmt.Sprintf("Sample size / population: %s", bgptField("sample_size")), + fmt.Sprintf("Results: %s", bgptField("results")), + fmt.Sprintf("Limitations: %s", bgptField("limitations")), + fmt.Sprintf("Conflicts of interest: %s", bgptField("conflict_of_interest")), + fmt.Sprintf("Data availability: %s", bgptField("data_availability")), + fmt.Sprintf("Blind spots: %s", bgptField("blind_spots")), + fmt.Sprintf("How to falsify: %s", bgptField("falsify")), + } + blocks = append(blocks, strings.Join(lines, "\n")) + } + return strings.Join(blocks, "\n\n") +} + // retrievalParams mirrors the Python RetrievalParam shape: the // values the canvas node declares at build time, applied as // defaults to the per-invocation RetrievalRequest. The fields are