mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-19 14:11:04 +08:00
fix(go-agent): handle await response prompts correctly (#17044)
## Summary - Prevent downstream agents from reusing stale `sys.query` values. - Resolve Await Response tips from the current canvas state and honor `enable_tips`. ## Testing - `bash build.sh --test ./internal/agent/component/...` - `bash build.sh --test ./internal/agent/canvas/...`
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user