fix(go-agent): align agent debug input form with Python (#16654)

## Summary

- derive Go Agent debug input forms from prompt variable references
instead of Agent meta fields
- seed `sys.*` debug params into `CanvasState.Sys` so single-component
debug resolves prompt variables like Python
- restore Agent test-run parity for form rendering and debug execution

## Tests

- `go test ./internal/agent/component -run
'TestAgent_(GetInputForm_UsesPromptReferences|GetInputForm_DeduplicatesPromptReferences|Meta_DefaultsToEmpty|Reset_NoTools)$'`
- `go test ./internal/handler -run
'Test(DebugComponent_SeedsSysInputsIntoCanvasState|DebugComponent_HappyPath_Begin|GetComponentInputForm_HappyPath)$'`

AFTER:
<img width="669" height="456" alt="image"
src="https://github.com/user-attachments/assets/4fd86559-aafc-4027-91ae-6e666137ee1b"
/>
This commit is contained in:
Hz_
2026-07-06 13:29:10 +08:00
committed by GitHub
parent c9f064d5fd
commit c4166a91e0
4 changed files with 203 additions and 31 deletions

View File

@@ -15,6 +15,7 @@ import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
einotool "github.com/cloudwego/eino/components/tool"
@@ -282,17 +283,16 @@ func chunksFromState(ctx context.Context) []prompts.CitationSource {
return out
}
// GetInputForm aggregates the Agent's own meta-schema with each
// sub-tool's input form. Mirrors Python's `Agent.get_input_form`.
// GetInputForm aggregates the Agent's own user-callable parameters with each
// sub-tool's input form. Mirrors Python's `Agent.get_input_form`, which
// returns a flat field-definition map keyed by input name.
//
// Today the sub-tool input forms are aggregated via an optional
// `InputForm() map[string]any` method on the eino tool (when tools
// implement it); tools that don't expose a structured input form
// are skipped silently.
func (c *AgentComponent) GetInputForm() map[string]any {
out := map[string]any{
"self": c.param.Meta,
}
out := extractAgentPromptInputForm(c.param.SystemPrompt, c.param.UserPrompt)
tools, err := buildAgentTools(c.param)
if err != nil {
return out
@@ -314,6 +314,41 @@ func (c *AgentComponent) GetInputForm() map[string]any {
return out
}
func extractAgentPromptInputForm(systemPrompt, userPrompt string) map[string]any {
out := map[string]any{}
seen := map[string]struct{}{}
matches := append(runtime.VarRefPattern.FindAllStringSubmatch(systemPrompt, -1), runtime.VarRefPattern.FindAllStringSubmatch(userPrompt, -1)...)
for _, match := range matches {
if len(match) < 2 {
continue
}
key := strings.TrimSpace(match[1])
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out[key] = map[string]any{
"type": "line",
"name": key,
"optional": false,
}
}
return out
}
func sortedAgentPromptInputKeys(systemPrompt, userPrompt string) []string {
form := extractAgentPromptInputForm(systemPrompt, userPrompt)
keys := make([]string, 0, len(form))
for key := range form {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
// Reset calls Reset on every sub-tool that implements the Resetter
// interface. Mirrors Python's per-tool reset() — useful for clearing
// per-invocation state (caches, scratch buffers) between calls.

View File

@@ -22,33 +22,43 @@ import (
"github.com/cloudwego/eino/schema"
)
// TestAgent_GetInputForm_IncludesSelfMeta: GetInputForm returns the
// Agent's own meta-schema under the "self" key, even when no tools
// are configured.
func TestAgent_GetInputForm_IncludesSelfMeta(t *testing.T) {
// TestAgent_GetInputForm_UsesPromptReferences verifies the Agent input form
// follows Python's behavior: it is derived from prompt template references.
func TestAgent_GetInputForm_UsesPromptReferences(t *testing.T) {
c := NewAgentComponent(AgentParam{
ModelID: "echo",
Meta: AgentMeta{
Name: "research_agent",
Description: "Performs multi-step research",
Parameters: map[string]AgentMetaParam{
"user_prompt": {Type: "string", Description: "The question", Required: true},
},
},
ModelID: "echo",
SystemPrompt: "Use {sys.query} and {{sys.user_id}} to answer.",
UserPrompt: "Question: {sys.query}",
})
form := c.GetInputForm()
if form == nil {
t.Fatal("GetInputForm returned nil")
}
self, ok := form["self"].(AgentMeta)
if _, ok := form["self"]; ok {
t.Fatalf("form unexpectedly contains synthetic self key: %#v", form["self"])
}
queryField, ok := form["sys.query"].(map[string]any)
if !ok {
t.Fatalf("form['self'] type=%T, want AgentMeta", form["self"])
t.Fatalf("form['sys.query'] type=%T, want map[string]any", form["sys.query"])
}
if self.Name != "research_agent" {
t.Errorf("Name=%q, want research_agent", self.Name)
if queryField["type"] != "line" {
t.Errorf("sys.query.type=%v, want line", queryField["type"])
}
if self.Parameters["user_prompt"].Type != "string" {
t.Errorf("user_prompt type=%q, want string", self.Parameters["user_prompt"].Type)
if queryField["name"] != "sys.query" {
t.Errorf("sys.query.name=%v, want 'sys.query'", queryField["name"])
}
if queryField["optional"] != false {
t.Errorf("sys.query.optional=%v, want false", queryField["optional"])
}
userIDField, ok := form["sys.user_id"].(map[string]any)
if !ok {
t.Fatalf("form['sys.user_id'] type=%T, want map[string]any", form["sys.user_id"])
}
if userIDField["name"] != "sys.user_id" {
t.Errorf("sys.user_id.name=%v, want sys.user_id", userIDField["name"])
}
if userIDField["optional"] != false {
t.Errorf("sys.user_id.optional=%v, want false", userIDField["optional"])
}
}
@@ -59,17 +69,27 @@ func TestAgent_Reset_NoTools(t *testing.T) {
c.Reset() // should not panic
}
// TestAgent_Meta_DefaultsToEmpty: zero-value AgentParam.Meta is the
// empty AgentMeta struct (not a nil map dereference).
// TestAgent_GetInputForm_DeduplicatesPromptReferences ensures repeated refs
// across system and user prompts collapse to one input field.
func TestAgent_GetInputForm_DeduplicatesPromptReferences(t *testing.T) {
c := NewAgentComponent(AgentParam{
ModelID: "echo",
SystemPrompt: "A {sys.query}",
UserPrompt: "B {{sys.query}}",
})
keys := sortedAgentPromptInputKeys(c.param.SystemPrompt, c.param.UserPrompt)
if len(keys) != 1 || keys[0] != "sys.query" {
t.Fatalf("keys=%v, want [sys.query]", keys)
}
}
// TestAgent_Meta_DefaultsToEmpty: prompts without variable references yield
// an empty input-form map.
func TestAgent_Meta_DefaultsToEmpty(t *testing.T) {
c := NewAgentComponent(AgentParam{ModelID: "echo"})
form := c.GetInputForm()
self, ok := form["self"].(AgentMeta)
if !ok {
t.Fatalf("form['self'] type=%T, want AgentMeta", form["self"])
}
if self.Name != "" || self.Description != "" {
t.Errorf("expected empty meta, got %+v", self)
if len(form) != 0 {
t.Fatalf("expected empty input form, got %+v", form)
}
}