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:
Hz_
2026-07-18 18:44:00 +08:00
committed by GitHub
parent 4a134e6035
commit 8945f66814
4 changed files with 114 additions and 2 deletions

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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) {