mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-04 23:00:30 +08:00
Port Python rag/advanced_rag agentic search to Go: ES-backed dataset-nav service, agentic-search harness, and agent tools. Includes agentic-search port plan and self-review docs.
141 lines
5.3 KiB
Go
141 lines
5.3 KiB
Go
package harness
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"ragflow/internal/agent/component"
|
|
)
|
|
|
|
// fakeChatInvoker returns a fixed content for the chat call, so tests can drive
|
|
// the route/planner LLM output deterministically.
|
|
type fakeChatInvoker struct{ content string }
|
|
|
|
func (f *fakeChatInvoker) Invoke(_ context.Context, _ *gorm.DB, _ component.ChatInvokeRequest) (*component.ChatInvokeResponse, error) {
|
|
return &component.ChatInvokeResponse{Content: f.content}, nil
|
|
}
|
|
|
|
func installChat(t *testing.T, content string) {
|
|
t.Helper()
|
|
component.SetDefaultChatInvoker(&fakeChatInvoker{content: content})
|
|
t.Cleanup(func() { component.SetDefaultChatInvoker(nil) })
|
|
}
|
|
|
|
// TestRouteNode_Classifies asserts route classification drives the execution
|
|
// strategy from the mode and the LLM's question_type.
|
|
func TestRouteNode_Classifies(t *testing.T) {
|
|
installChat(t, `{"question_type":"comparative","requires_decomposition":true,"reasoning":"cmp"}`)
|
|
r := RouteNode(context.Background(), nil, "Compare A and B", "medium")
|
|
if r.QuestionType != "comparative" {
|
|
t.Errorf("question_type = %q, want comparative", r.QuestionType)
|
|
}
|
|
// medium mode requires decomposition AND LLM says true -> true.
|
|
if !r.RequiresDecomposition {
|
|
t.Errorf("requires_decomposition = false, want true")
|
|
}
|
|
if r.ExecutionStrategy != "decompose_and_search" {
|
|
t.Errorf("execution_strategy = %q, want decompose_and_search", r.ExecutionStrategy)
|
|
}
|
|
}
|
|
|
|
// TestRouteNode_LowModeDisablesDecomposition asserts low mode never decomposes
|
|
// even if the LLM requests it.
|
|
func TestRouteNode_LowModeDisablesDecomposition(t *testing.T) {
|
|
installChat(t, `{"question_type":"analytical","requires_decomposition":true}`)
|
|
r := RouteNode(context.Background(), nil, "Analyze X", "low")
|
|
if r.RequiresDecomposition {
|
|
t.Errorf("low mode must disable decomposition, got true")
|
|
}
|
|
if r.ExecutionStrategy != "direct_search" {
|
|
t.Errorf("low mode execution_strategy = %q, want direct_search", r.ExecutionStrategy)
|
|
}
|
|
}
|
|
|
|
// TestRouteNode_FencedJSON asserts think-tag/fence stripping works.
|
|
func TestRouteNode_FencedJSON(t *testing.T) {
|
|
installChat(t, "Sure!\n```json\n{\"question_type\":\"factual\",\"requires_decomposition\":false}\n```")
|
|
r := RouteNode(context.Background(), nil, "What is X?", "medium")
|
|
if r.QuestionType != "factual" {
|
|
t.Errorf("question_type = %q, want factual", r.QuestionType)
|
|
}
|
|
if r.RequiresDecomposition {
|
|
t.Errorf("requires_decomposition = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestRouteNode_EmptyQuestionFallsBack asserts an empty question yields a
|
|
// direct factual decision without calling the LLM.
|
|
func TestRouteNode_EmptyQuestionFallsBack(t *testing.T) {
|
|
r := RouteNode(context.Background(), nil, "", "medium")
|
|
if r.QuestionType != "factual" || r.RequiresDecomposition {
|
|
t.Errorf("empty question must fall back to direct factual, got %+v", r)
|
|
}
|
|
}
|
|
|
|
// TestPlannerNode_DirectMode asserts a non-decomposed route yields one coarse
|
|
// claim without calling the LLM.
|
|
func TestPlannerNode_DirectMode(t *testing.T) {
|
|
plan := PlannerNode(context.Background(), nil, RouteDecision{
|
|
Question: "What is X?", RequiresDecomposition: false,
|
|
}, nil)
|
|
if plan.PlanType != "direct" || len(plan.Claims) != 1 {
|
|
t.Fatalf("direct plan = %+v, want 1 direct claim", plan)
|
|
}
|
|
}
|
|
|
|
// TestPlannerNode_Decomposes asserts the planner builds claims from the LLM
|
|
// output and applies the mode's max iterations.
|
|
func TestPlannerNode_Decomposes(t *testing.T) {
|
|
installChat(t, `{"claims":[
|
|
{"claim_id":"c0","description":"fact one","priority":0},
|
|
{"claim_id":"c1","description":"fact two","priority":1}
|
|
]}`)
|
|
plan := PlannerNode(context.Background(), nil, RouteDecision{
|
|
Question: "Compare A and B", QuestionType: "comparative", RequiresDecomposition: true, ThinkingMode: "medium",
|
|
}, nil)
|
|
if plan.PlanType != "comparative_decomposition" {
|
|
t.Errorf("plan_type = %q, want comparative_decomposition", plan.PlanType)
|
|
}
|
|
if len(plan.Claims) != 2 {
|
|
t.Fatalf("claims = %d, want 2", len(plan.Claims))
|
|
}
|
|
if plan.Claims[1].Priority != 1 {
|
|
t.Errorf("claim priority = %d, want 1", plan.Claims[1].Priority)
|
|
}
|
|
// medium maxOrchestratorCycles = 3
|
|
if plan.MaxIterations != 3 {
|
|
t.Errorf("max_iterations = %d, want 3", plan.MaxIterations)
|
|
}
|
|
}
|
|
|
|
// TestPlannerNode_UnknownModeFallsBack asserts an unknown (non-empty) mode label
|
|
// falls back to medium so the planner is not driven by a zero-valued mode
|
|
// (which would produce a degenerate plan with max_claims=0).
|
|
func TestPlannerNode_UnknownModeFallsBack(t *testing.T) {
|
|
installChat(t, `{"claims":[{"claim_id":"c0","description":"fact one","priority":0}]}`)
|
|
plan := PlannerNode(context.Background(), nil, RouteDecision{
|
|
Question: "Q", RequiresDecomposition: true, ThinkingMode: "turbo-unknown",
|
|
}, nil)
|
|
// medium maxOrchestratorCycles = 3, and claims must still be built.
|
|
if plan.MaxIterations != 3 {
|
|
t.Errorf("max_iterations = %d, want 3 (medium fallback)", plan.MaxIterations)
|
|
}
|
|
if len(plan.Claims) != 1 {
|
|
t.Errorf("claims = %d, want 1", len(plan.Claims))
|
|
}
|
|
}
|
|
|
|
// TestPlannerNode_BadJSONFallsBack asserts unparseable planner output falls back
|
|
// to the direct plan.
|
|
func TestPlannerNode_BadJSONFallsBack(t *testing.T) {
|
|
installChat(t, "not json at all")
|
|
plan := PlannerNode(context.Background(), nil, RouteDecision{
|
|
Question: "Q", RequiresDecomposition: true, ThinkingMode: "medium",
|
|
}, nil)
|
|
if plan.PlanType != "direct" || len(plan.Claims) != 1 {
|
|
t.Fatalf("fallback plan = %+v, want direct", plan)
|
|
}
|
|
}
|