mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-13 16:38:26 +08:00
### What problem does this PR solve? - Tools management - Pregel engine wrapper for better usage - UT race - Coding style ### Type of change - [x] Refactoring
233 lines
6.1 KiB
Go
233 lines
6.1 KiB
Go
package prebuilt
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"ragflow/internal/harness/graph/runnable"
|
|
)
|
|
|
|
// testLLM implements the LLM interface for testing
|
|
type testLLM struct {
|
|
responses []string
|
|
index int
|
|
}
|
|
|
|
func (m *testLLM) Generate(ctx context.Context, messages []map[string]interface{}) (string, error) {
|
|
if m.index >= len(m.responses) {
|
|
return "", fmt.Errorf("no more responses")
|
|
}
|
|
response := m.responses[m.index]
|
|
m.index++
|
|
return response, nil
|
|
}
|
|
|
|
func (m *testLLM) GenerateStream(ctx context.Context, messages []map[string]interface{}) (<-chan string, error) {
|
|
ch := make(chan string, 1)
|
|
if m.index >= len(m.responses) {
|
|
close(ch)
|
|
return ch, fmt.Errorf("no more responses")
|
|
}
|
|
response := m.responses[m.index]
|
|
m.index++
|
|
ch <- response
|
|
close(ch)
|
|
return ch, nil
|
|
}
|
|
|
|
func TestPrebuiltAgent_MaxIterations(t *testing.T) {
|
|
t.Run("default_max_iterations", func(t *testing.T) {
|
|
mock := &testLLM{
|
|
responses: []string{
|
|
"THINK: searching\nACTION: search\nQUERY: test",
|
|
"THINK: still searching\nACTION: search\nQUERY: again",
|
|
"ANSWER: done",
|
|
},
|
|
}
|
|
config := ReactAgentConfig{
|
|
Tools: []Tool{
|
|
{Name: "search", Description: "search tool",
|
|
Function: func(ctx context.Context, input map[string]interface{}) (interface{}, error) {
|
|
return "result", nil
|
|
}},
|
|
},
|
|
Model: mock,
|
|
SystemPrompt: "test",
|
|
MaxIterations: 0,
|
|
}
|
|
|
|
agent, err := NewReactAgent(config)
|
|
if err != nil {
|
|
t.Fatalf("NewReactAgent: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
output, err := agent.Invoke(ctx, map[string]interface{}{"input": "test"})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
iterations := output["iterations"].(int)
|
|
if iterations <= 0 {
|
|
t.Errorf("expected positive iterations, got %d", iterations)
|
|
}
|
|
t.Logf("MaxIterations=0: completed %d iterations", iterations)
|
|
})
|
|
|
|
t.Run("stop_condition_terminates_early", func(t *testing.T) {
|
|
mock := &testLLM{
|
|
responses: []string{
|
|
"THINK: first\nACTION: search\nQUERY: q1",
|
|
"THINK: second\nACTION: search\nQUERY: q2",
|
|
"THINK: third\nACTION: search\nQUERY: q3",
|
|
},
|
|
}
|
|
callCount := 0
|
|
config := ReactAgentConfig{
|
|
Tools: []Tool{
|
|
{Name: "search", Description: "search",
|
|
Function: func(ctx context.Context, input map[string]interface{}) (interface{}, error) {
|
|
callCount++
|
|
return "result", nil
|
|
}},
|
|
},
|
|
Model: mock,
|
|
SystemPrompt: "test",
|
|
StopCondition: func(state *ReActState) bool {
|
|
return state.Iteration >= 1
|
|
},
|
|
MaxIterations: 10,
|
|
}
|
|
|
|
agent, err := NewReactAgent(config)
|
|
if err != nil {
|
|
t.Fatalf("NewReactAgent: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
output, err := agent.Invoke(ctx, map[string]interface{}{"input": "test"})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
iterations := output["iterations"].(int)
|
|
if iterations > 2 {
|
|
t.Errorf("expected <=2 iterations due to stop condition, got %d", iterations)
|
|
}
|
|
t.Logf("stop condition: completed %d iterations", iterations)
|
|
})
|
|
|
|
t.Run("no_tools_returns_error", func(t *testing.T) {
|
|
_, err := NewReactAgent(ReactAgentConfig{
|
|
Tools: nil,
|
|
Model: &testLLM{},
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for empty tools")
|
|
}
|
|
})
|
|
|
|
t.Run("no_model_returns_error", func(t *testing.T) {
|
|
_, err := NewReactAgent(ReactAgentConfig{
|
|
Tools: []Tool{{Name: "t", Description: "t",
|
|
Function: func(ctx context.Context, input map[string]interface{}) (interface{}, error) { return nil, nil }}},
|
|
Model: nil,
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for nil model")
|
|
}
|
|
})
|
|
|
|
t.Run("unknown_action_handled", func(t *testing.T) {
|
|
mock := &testLLM{
|
|
responses: []string{
|
|
"THINK: unknown action\nACTION: nonexistent\nQUERY: test",
|
|
"THINK: try again\nACTION: search\nQUERY: retry",
|
|
"ANSWER: done",
|
|
},
|
|
}
|
|
config := ReactAgentConfig{
|
|
Tools: []Tool{
|
|
{Name: "search", Description: "search",
|
|
Function: func(ctx context.Context, input map[string]interface{}) (interface{}, error) {
|
|
return "result", nil
|
|
}},
|
|
},
|
|
Model: mock,
|
|
SystemPrompt: "test",
|
|
MaxIterations: 3,
|
|
}
|
|
|
|
agent, err := NewReactAgent(config)
|
|
if err != nil {
|
|
t.Fatalf("NewReactAgent: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
output, err := agent.Invoke(ctx, map[string]interface{}{"input": "test"})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
t.Logf("unknown action handled: iterations=%d", output["iterations"])
|
|
})
|
|
}
|
|
|
|
func TestPrebuiltAgent_TransformNodeEdgeCases(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
tn := TransformNode(func(input map[string]interface{}) (map[string]interface{}, error) {
|
|
return map[string]interface{}{
|
|
"transformed": fmt.Sprintf("%v", input["data"]),
|
|
}, nil
|
|
})
|
|
|
|
output, err := tn.Invoke(ctx, map[string]interface{}{"data": "test"})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if output["transformed"] != "test" {
|
|
t.Errorf("expected 'test', got %v", output["transformed"])
|
|
}
|
|
}
|
|
|
|
func TestPrebuiltAgent_ConditionalNodeEdgeCases(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
branchA := runnable.NewRunnableFunc(
|
|
func(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
|
|
return map[string]interface{}{"branch": "A"}, nil
|
|
},
|
|
runnable.WithName[map[string]interface{}, map[string]interface{}]("branch_a"),
|
|
)
|
|
|
|
t.Run("no_matching_branch_no_default", func(t *testing.T) {
|
|
cn := ConditionalNode(
|
|
func(input map[string]interface{}) string { return "MISSING" },
|
|
map[string]runnable.Runnable[map[string]interface{}, map[string]interface{}]{
|
|
"A": branchA,
|
|
},
|
|
"",
|
|
)
|
|
_, err := cn.Invoke(ctx, map[string]interface{}{})
|
|
if err == nil {
|
|
t.Error("expected error for missing branch with no default")
|
|
}
|
|
})
|
|
|
|
t.Run("default_branch_used", func(t *testing.T) {
|
|
cn := ConditionalNode(
|
|
func(input map[string]interface{}) string { return "MISSING" },
|
|
map[string]runnable.Runnable[map[string]interface{}, map[string]interface{}]{
|
|
"A": branchA,
|
|
},
|
|
"A",
|
|
)
|
|
output, err := cn.Invoke(ctx, map[string]interface{}{})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if output["branch"] != "A" {
|
|
t.Errorf("expected branch A from default, got %v", output["branch"])
|
|
}
|
|
})
|
|
}
|