mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-17 05:07:23 +08:00
## Summary - Align Go WenCai and SearXNG behavior, schemas, and node parameters with Python. - Add the `WenCai` and `SearXNG` Canvas components and register their tool factories. - Match Python's current WenCai behavior by returning an empty report while its upstream request is disabled. - Add SearXNG request validation, SSRF-safe DNS pinning, raw result preservation, and reference rendering. - Support context cancellation, error envelopes, and lock-safe retrieval references. ## Tests Passed: - `bash build.sh --test ./internal/agent/tool/...` - `bash build.sh --test ./internal/agent/component/...` - `bash build.sh --test ./internal/agent/runtime/...` - `bash build.sh --test ./internal/agent/...` - `cd web && npm run type-check` <img width="1900" height="1102" alt="image" src="https://github.com/user-attachments/assets/ec77d217-d9fd-455a-96ec-9aabf6841109" /> <img width="1900" height="1102" alt="image" src="https://github.com/user-attachments/assets/52ac129f-cb65-453d-ae48-cc518803ac23" />
191 lines
5.3 KiB
Go
191 lines
5.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"
|
|
"errors"
|
|
"testing"
|
|
|
|
einotool "github.com/cloudwego/eino/components/tool"
|
|
|
|
agentcanvas "ragflow/internal/agent/canvas"
|
|
)
|
|
|
|
type fakeWencaiInvoker struct {
|
|
args map[string]any
|
|
calls int
|
|
out string
|
|
err error
|
|
}
|
|
|
|
func (f *fakeWencaiInvoker) InvokableRun(_ context.Context, argsJSON string, _ ...einotool.Option) (string, error) {
|
|
f.calls++
|
|
if err := json.Unmarshal([]byte(argsJSON), &f.args); err != nil {
|
|
return "", err
|
|
}
|
|
if f.out == "" && f.err == nil {
|
|
return `{"report":""}`, nil
|
|
}
|
|
return f.out, f.err
|
|
}
|
|
|
|
func TestWencai_RegisteredFactoryMatchesPythonSurface(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c, err := New("WenCai", map[string]any{
|
|
"top_n": float64(20),
|
|
"query_type": "stock",
|
|
"outputs": map[string]any{"report": map[string]any{}},
|
|
"setups": map[string]any{"query": "configured query"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("New(WenCai): %v", err)
|
|
}
|
|
if got := c.Name(); got != "WenCai" {
|
|
t.Fatalf("Name() = %q, want WenCai", got)
|
|
}
|
|
if _, ok := c.Inputs()["query"]; !ok {
|
|
t.Fatal("Inputs() missing query")
|
|
}
|
|
if _, ok := c.Outputs()["report"]; !ok {
|
|
t.Fatal("Outputs() missing report")
|
|
}
|
|
formGetter, ok := c.(interface{ GetInputForm() map[string]any })
|
|
if !ok {
|
|
t.Fatal("WenCai component does not expose GetInputForm")
|
|
}
|
|
query, ok := formGetter.GetInputForm()["query"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("query form has type %T, want map", formGetter.GetInputForm()["query"])
|
|
}
|
|
if query["name"] != "Query" || query["type"] != "line" {
|
|
t.Fatalf("query form = %#v, want name=Query type=line", query)
|
|
}
|
|
}
|
|
|
|
func TestWencai_InvokeReturnsEmptyReportWithoutError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fake := &fakeWencaiInvoker{}
|
|
c := newWencaiComponentWithInvoker(fake)
|
|
out, err := c.Invoke(context.Background(), map[string]any{
|
|
"query": "商业航天",
|
|
"top_n": 20,
|
|
"query_type": "stock",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if got := out["report"]; got != "" {
|
|
t.Fatalf("report = %v, want empty string", got)
|
|
}
|
|
if fake.calls != 1 {
|
|
t.Fatalf("calls = %d, want 1", fake.calls)
|
|
}
|
|
if got := fake.args["query"]; got != "商业航天" {
|
|
t.Fatalf("runtime query = %v, want 商业航天", got)
|
|
}
|
|
if len(fake.args) != 1 {
|
|
t.Fatalf("runtime args = %#v, want query only", fake.args)
|
|
}
|
|
}
|
|
|
|
func TestWencai_InvokeEmptyQuerySkipsTool(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fake := &fakeWencaiInvoker{}
|
|
c := newWencaiComponentWithInvoker(fake)
|
|
out, err := c.Invoke(context.Background(), map[string]any{"query": ""})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if got := out["report"]; got != "" {
|
|
t.Fatalf("report = %v, want empty string", got)
|
|
}
|
|
if fake.calls != 0 {
|
|
t.Fatalf("calls = %d, want 0", fake.calls)
|
|
}
|
|
}
|
|
|
|
func TestWencai_InvokePreservesErrorEnvelope(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fake := &fakeWencaiInvoker{
|
|
out: `{"report":"","_ERROR":"upstream failed"}`,
|
|
err: errors.New("upstream failed"),
|
|
}
|
|
c := newWencaiComponentWithInvoker(fake)
|
|
out, err := c.Invoke(context.Background(), map[string]any{"query": "商业航天"})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if got := out["report"]; got != "" {
|
|
t.Fatalf("report = %v, want empty string", got)
|
|
}
|
|
if got := out["_ERROR"]; got != "upstream failed" {
|
|
t.Fatalf("_ERROR = %v, want upstream failed", got)
|
|
}
|
|
}
|
|
|
|
func TestWencai_InvokePreservesErrorEnvelopeWithoutGoError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fake := &fakeWencaiInvoker{
|
|
out: `{"report":"","_ERROR":"business error"}`,
|
|
}
|
|
c := newWencaiComponentWithInvoker(fake)
|
|
out, err := c.Invoke(context.Background(), map[string]any{"query": "商业航天"})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if got := out["report"]; got != "" {
|
|
t.Fatalf("report = %v, want empty string", got)
|
|
}
|
|
if got := out["_ERROR"]; got != "business error" {
|
|
t.Fatalf("_ERROR = %v, want business error", got)
|
|
}
|
|
}
|
|
|
|
func TestWencai_BuildWorkflowUsesPythonComponentName(t *testing.T) {
|
|
c := &agentcanvas.Canvas{
|
|
Components: map[string]agentcanvas.CanvasComponent{
|
|
"begin_0": {
|
|
Obj: agentcanvas.CanvasComponentObj{ComponentName: "Begin", Params: map[string]any{}},
|
|
Downstream: []string{"wencai_0"},
|
|
},
|
|
"wencai_0": {
|
|
Obj: agentcanvas.CanvasComponentObj{ComponentName: "WenCai", Params: map[string]any{
|
|
"top_n": float64(20),
|
|
"query_type": "stock",
|
|
}},
|
|
Upstream: []string{"begin_0"},
|
|
Downstream: []string{"message_0"},
|
|
},
|
|
"message_0": {
|
|
Obj: agentcanvas.CanvasComponentObj{ComponentName: "Message", Params: map[string]any{}},
|
|
Upstream: []string{"wencai_0"},
|
|
},
|
|
},
|
|
Path: []string{"begin_0", "wencai_0", "message_0"},
|
|
}
|
|
if _, err := agentcanvas.BuildWorkflow(context.Background(), c); err != nil {
|
|
t.Fatalf("BuildWorkflow with WenCai: %v", err)
|
|
}
|
|
}
|