diff --git a/internal/agent/canvas/interrupt_resume.go b/internal/agent/canvas/interrupt_resume.go index d1629ca9a3..f5d91dcc25 100644 --- a/internal/agent/canvas/interrupt_resume.go +++ b/internal/agent/canvas/interrupt_resume.go @@ -53,6 +53,8 @@ import ( "strings" "github.com/cloudwego/eino/compose" + + "ragflow/internal/agent/runtime" ) // BuildInputSpec turns the DSL's UserFillUp params into the user-visible @@ -84,6 +86,27 @@ func BuildInputSpec(params map[string]any) map[string]any { return spec } +// buildUserFillUpInterruptInfo renders a fresh wait prompt from the current +// canvas state so repeated pauses never reuse an earlier iteration's tips. +func buildUserFillUpInterruptInfo(ctx context.Context, params map[string]any) map[string]any { + info := BuildInputSpec(params) + if enabled, ok := info["enable_tips"].(bool); ok && !enabled { + delete(info, "tips") + return info + } + + tips, _ := info["tips"].(string) + if tips == "" { + return info + } + state, _, err := GetStateFromContext[*CanvasState](ctx) + if err != nil || state == nil { + return info + } + info["tips"] = runtime.ResolveTemplateForDisplay(tips, state) + return info +} + // UserFillUpNodeBody returns an eino node function implementing // "wait for user input" semantics. // @@ -121,7 +144,7 @@ func UserFillUpNodeBody(cpnID string, params map[string]any) func(ctx context.Co // First-call branch: emit the interrupt signal. The returned // error implements error; eino's runner catches it, persists a // checkpoint, and bubbles it up. - if err := compose.Interrupt(ctx, inputSpec); err != nil { + if err := compose.Interrupt(ctx, buildUserFillUpInterruptInfo(ctx, params)); err != nil { return nil, err } diff --git a/internal/agent/canvas/interrupt_resume_test.go b/internal/agent/canvas/interrupt_resume_test.go index 93ede95e8b..aec07b9401 100644 --- a/internal/agent/canvas/interrupt_resume_test.go +++ b/internal/agent/canvas/interrupt_resume_test.go @@ -53,6 +53,56 @@ func TestBuildInputSpec_BasicFields(t *testing.T) { } } +func TestUserFillUpNodeBody_ResolvesTipsFromCanvasState(t *testing.T) { + state := NewCanvasState("run-1", "task-1") + state.SetVar("Agent:MoodyIdeasMarry", "content", "How old are you?") + ctx := WithState(context.Background(), state) + + body := UserFillUpNodeBody("UserFillUp:TwelveBadgersRescue", map[string]any{ + "enable_tips": true, + "tips": "{Agent:MoodyIdeasMarry@content}", + }) + _, err := body(ctx, nil) + if err == nil { + t.Fatal("UserFillUp should interrupt while waiting for input") + } + rawInfo, ok := compose.IsInterruptRerunError(err) + if !ok { + t.Fatalf("UserFillUp returned non-interrupt error: %v", err) + } + got, ok := rawInfo.(map[string]any) + if !ok { + t.Fatalf("interrupt info type = %T, want map[string]any", rawInfo) + } + + if got["tips"] != "How old are you?" { + t.Fatalf("tips = %q, want resolved upstream Agent content", got["tips"]) + } +} + +func TestUserFillUpNodeBody_OmitsDisabledTips(t *testing.T) { + body := UserFillUpNodeBody("UserFillUp:TwelveBadgersRescue", map[string]any{ + "enable_tips": false, + "tips": "should not be shown", + }) + _, err := body(context.Background(), nil) + if err == nil { + t.Fatal("UserFillUp should interrupt while waiting for input") + } + rawInfo, ok := compose.IsInterruptRerunError(err) + if !ok { + t.Fatalf("UserFillUp returned non-interrupt error: %v", err) + } + got, ok := rawInfo.(map[string]any) + if !ok { + t.Fatalf("interrupt info type = %T, want map[string]any", rawInfo) + } + + if _, ok := got["tips"]; ok { + t.Fatalf("disabled tips should be omitted: %+v", got) + } +} + // TestBuildInputSpec_NilSafe covers the nil params path (defensive). func TestBuildInputSpec_NilSafe(t *testing.T) { got := BuildInputSpec(nil) diff --git a/internal/agent/component/agent.go b/internal/agent/component/agent.go index 357e69ddee..86f7a65c7f 100644 --- a/internal/agent/component/agent.go +++ b/internal/agent/component/agent.go @@ -549,7 +549,7 @@ func (c *AgentComponent) Invoke(ctx context.Context, inputs map[string]any) (map } if hasRuntimeUserPrompt { p.UserPrompt = formatAgentRuntimePrompt(inputs, p.UserPrompt) - } else if shouldFallbackToSysQuery(p.UserPrompt) && state != nil { + } else if shouldFallbackToSysQuery(p.UserPrompt) && strings.TrimSpace(p.SystemPrompt) == "" && state != nil { if query, ok := stringFromState(state, "query"); ok { p.UserPrompt = query } diff --git a/internal/agent/component/agent_test.go b/internal/agent/component/agent_test.go index a21c472c75..cdde193e36 100644 --- a/internal/agent/component/agent_test.go +++ b/internal/agent/component/agent_test.go @@ -212,6 +212,45 @@ func TestAgent_UsesPromptsListForSysQuery(t *testing.T) { } } +func TestAgent_EmptyConfiguredUserPromptDoesNotFallbackToSysQuery(t *testing.T) { + var gotSystemPrompt, gotUserPrompt string + withAgentRunner(t, func(_ context.Context, p AgentParam) (*schema.Message, error) { + gotSystemPrompt = p.SystemPrompt + gotUserPrompt = p.UserPrompt + return &schema.Message{Role: schema.Assistant, Content: "ok"}, nil + }) + + cmp, err := New("Agent", map[string]any{ + "model_id": "stub", + "api_key": "test-key", + "sys_prompt": "User answer: {UserFillUp:TwelveBadgersRescue@key}", + "prompts": []any{ + map[string]any{"role": "user", "content": ""}, + }, + }) + if err != nil { + t.Fatalf("New(Agent): %v", err) + } + + state := runtime.NewCanvasState("run-1", "task-1") + state.Sys["query"] = "1" + state.SetVar("UserFillUp:TwelveBadgersRescue", "key", "21") + ctx := runtime.WithState(context.Background(), state) + + if _, err := cmp.Invoke(ctx, nil); err != nil { + t.Fatalf("Invoke: %v", err) + } + if gotSystemPrompt != "User answer: 21" { + t.Fatalf("runner system prompt = %q, want resolved UserFillUp answer", gotSystemPrompt) + } + if gotUserPrompt == "1" { + t.Fatalf("runner user prompt reused sys.query: %q", gotUserPrompt) + } + if gotUserPrompt != gotSystemPrompt { + t.Fatalf("runner user prompt = %q, want system-only fallback %q", gotUserPrompt, gotSystemPrompt) + } +} + func TestAgent_FormatsRuntimePromptLikePython(t *testing.T) { var gotPrompt string withAgentRunner(t, func(_ context.Context, p AgentParam) (*schema.Message, error) {