fix: agent chat completions can not use (#16570)

### Summary

As title
<img width="2370" height="2039" alt="image"
src="https://github.com/user-attachments/assets/4cccf543-3908-49ee-8101-c5068fbf53ec"
/>
This commit is contained in:
Haruko386
2026-07-03 13:25:14 +08:00
committed by GitHub
parent e65bac238e
commit 383d059969
7 changed files with 353 additions and 71 deletions

View File

@@ -93,6 +93,8 @@ type AgentParam struct {
BaseURL string
}
const agentUserPromptSchemaDefault = "This is the order you need to send to the agent."
// AgentMeta declares the OpenAI-style function-call interface for the
// Agent component. Mirrors ragflow Python's ToolMeta shape.
type AgentMeta struct {
@@ -394,6 +396,10 @@ func (c *AgentComponent) Name() string { return "Agent" }
// the output map.
func (c *AgentComponent) Invoke(ctx context.Context, inputs map[string]any) (map[string]any, error) {
p := mergeAgentParam(c.param, inputs)
hasRuntimeUserPrompt := false
if v, ok := stringFrom(inputs, "user_prompt"); ok {
hasRuntimeUserPrompt = !shouldFallbackToSysQuery(v)
}
// v3.6.1: derive the driver and bare model name from the
// composite llm_id when the Agent DSL didn't set `driver`. The
@@ -418,6 +424,30 @@ func (c *AgentComponent) Invoke(ctx context.Context, inputs map[string]any) (map
}
p.APIKey, p.BaseURL = resolveTenantLLMConfig(ctx, p.Driver, p.ModelID, p.APIKey, p.BaseURL, originalModelID)
var state *runtime.CanvasState
if s, _, err := runtime.GetStateFromContext[*runtime.CanvasState](ctx); err == nil && s != nil {
state = s
if resolved, rerr := runtime.ResolveTemplate(p.SystemPrompt, state); resolved != p.SystemPrompt || rerr == nil {
p.SystemPrompt = resolved
if rerr != nil {
common.Debug("agent: resolve system_prompt", zap.Error(rerr))
}
}
if resolved, rerr := runtime.ResolveTemplate(p.UserPrompt, state); resolved != p.UserPrompt || rerr == nil {
p.UserPrompt = resolved
if rerr != nil {
common.Debug("agent: resolve user_prompt", zap.Error(rerr))
}
}
}
if hasRuntimeUserPrompt {
p.UserPrompt = formatAgentRuntimePrompt(inputs, p.UserPrompt)
} else if shouldFallbackToSysQuery(p.UserPrompt) && state != nil {
if query, ok := stringFromState(state, "query"); ok {
p.UserPrompt = query
}
}
if p.ModelID == "" {
return nil, &ParamError{Field: "model_id", Reason: "required"}
}
@@ -586,10 +616,7 @@ func buildAgentChatModel(p AgentParam) (*models.EinoChatModel, error) {
if driver == "" {
driver = "dummy"
}
var baseURL map[string]string
if p.BaseURL != "" {
baseURL = map[string]string{"default": p.BaseURL}
}
baseURL := baseURLMapForDriver(driver, p.BaseURL)
// urlSuffix: see chatURLSuffixFor in llm.go for the rationale.
// The factory's NewModelDriver stores URLSuffix verbatim; the
// driver then appends URLSuffix.Chat to baseURL to build the
@@ -696,6 +723,95 @@ func extractToolCalls(msg *schema.Message) []map[string]any {
return calls
}
// promptMessagesFromParams extracts the Python DSL `prompts` list into
// the single system/user prompt shape supported by the Go ReAct runner.
func promptMessagesFromParams(params map[string]any) (systemPrompt, userPrompt string, ok bool) {
raw, exists := params["prompts"]
if !exists {
return "", "", false
}
switch v := raw.(type) {
case string:
return "", v, true
case []any:
var systems, users []string
for _, item := range v {
m, ok := item.(map[string]any)
if !ok {
continue
}
content, ok := stringFrom(m, "content")
if !ok {
continue
}
role, _ := stringFrom(m, "role")
switch strings.ToLower(strings.TrimSpace(role)) {
case "system":
systems = append(systems, content)
case "user", "":
users = append(users, content)
}
}
if len(systems) == 0 && len(users) == 0 {
return "", "", false
}
return strings.Join(systems, "\n"), strings.Join(users, "\n"), true
case []map[string]any:
items := make([]any, 0, len(v))
for _, item := range v {
items = append(items, item)
}
return promptMessagesFromParams(map[string]any{"prompts": items})
}
return "", "", false
}
func appendPromptText(base, extra string) string {
if strings.TrimSpace(extra) == "" {
return base
}
if strings.TrimSpace(base) == "" {
return extra
}
return base + "\n" + extra
}
func hasNonEmptyString(inputs map[string]any, name string) bool {
v, ok := stringFrom(inputs, name)
return ok && strings.TrimSpace(v) != ""
}
func shouldFallbackToSysQuery(prompt string) bool {
p := strings.TrimSpace(prompt)
return p == "" || p == agentUserPromptSchemaDefault
}
func stringFromState(state *runtime.CanvasState, name string) (string, bool) {
if state == nil {
return "", false
}
v, ok := state.Sys[name].(string)
if !ok || strings.TrimSpace(v) == "" {
return "", false
}
return v, true
}
func formatAgentRuntimePrompt(inputs map[string]any, userPrompt string) string {
var b strings.Builder
if reasoning, ok := stringFrom(inputs, "reasoning"); ok && reasoning != "" {
fmt.Fprintf(&b, "\nREASONING:\n%s\n", reasoning)
}
if contextText, ok := stringFrom(inputs, "context"); ok && contextText != "" {
fmt.Fprintf(&b, "\nCONTEXT:\n%s\n", contextText)
}
if b.Len() == 0 {
return userPrompt
}
fmt.Fprintf(&b, "\nQUERY:\n%s\n", userPrompt)
return b.String()
}
// mergeAgentParam layers raw inputs over the receiver's default param set.
//
// v1 aliases accepted alongside the v2 names: "llm_id" → "model_id",
@@ -714,7 +830,13 @@ func mergeAgentParam(base AgentParam, inputs map[string]any) AgentParam {
} else if v, ok := stringFrom(inputs, "sys_prompt"); ok {
p.SystemPrompt = v
}
if v, ok := stringFrom(inputs, "user_prompt"); ok {
if promptSystem, promptUser, ok := promptMessagesFromParams(inputs); ok {
p.SystemPrompt = appendPromptText(p.SystemPrompt, promptSystem)
if strings.TrimSpace(promptUser) != "" {
p.UserPrompt = promptUser
}
}
if v, ok := stringFrom(inputs, "user_prompt"); ok && !shouldFallbackToSysQuery(v) {
p.UserPrompt = v
}
if v, ok := floatFrom(inputs, "top_p"); ok {
@@ -807,7 +929,11 @@ func init() {
} else if v, ok := stringFrom(params, "sys_prompt"); ok {
p.SystemPrompt = v
}
if v, ok := stringFrom(params, "user_prompt"); ok {
if promptSystem, promptUser, ok := promptMessagesFromParams(params); ok {
p.SystemPrompt = appendPromptText(p.SystemPrompt, promptSystem)
p.UserPrompt = promptUser
}
if v, ok := stringFrom(params, "user_prompt"); ok && p.UserPrompt == "" {
p.UserPrompt = v
}
if v, ok := floatFrom(params, "top_p"); ok {

View File

@@ -28,6 +28,7 @@ import (
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
"ragflow/internal/agent/runtime"
agenttool "ragflow/internal/agent/tool"
)
@@ -69,6 +70,89 @@ func TestAgent_NoToolsReAct(t *testing.T) {
}
}
func TestAgent_ResolvesUserPromptFromCanvasState(t *testing.T) {
var gotPrompt string
withAgentRunner(t, func(_ context.Context, p AgentParam) (*schema.Message, error) {
gotPrompt = p.UserPrompt
return &schema.Message{Role: schema.Assistant, Content: "ok"}, nil
})
state := runtime.NewCanvasState("run-1", "task-1")
state.Sys["query"] = "what is marigold"
ctx := runtime.WithState(context.Background(), state)
c := NewAgentComponent(AgentParam{
ModelID: "stub",
APIKey: "test-key",
UserPrompt: "Question: {sys.query}",
MaxRounds: 1,
})
if _, err := c.Invoke(ctx, nil); err != nil {
t.Fatalf("Invoke: %v", err)
}
if gotPrompt != "Question: what is marigold" {
t.Fatalf("runner prompt = %q, want resolved sys.query", gotPrompt)
}
}
func TestAgent_UsesPromptsListForSysQuery(t *testing.T) {
var gotPrompt string
withAgentRunner(t, func(_ context.Context, p AgentParam) (*schema.Message, error) {
gotPrompt = 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": "act as assistant",
"user_prompt": "This is the order you need to send to the agent.",
"prompts": []any{
map[string]any{"role": "user", "content": "{sys.query}"},
},
})
if err != nil {
t.Fatalf("New(Agent): %v", err)
}
state := runtime.NewCanvasState("run-1", "task-1")
state.Sys["query"] = "用户真正的问题"
ctx := runtime.WithState(context.Background(), state)
if _, err := cmp.Invoke(ctx, nil); err != nil {
t.Fatalf("Invoke: %v", err)
}
if gotPrompt != "用户真正的问题" {
t.Fatalf("runner prompt = %q, want sys.query from prompts list", gotPrompt)
}
}
func TestAgent_FormatsRuntimePromptLikePython(t *testing.T) {
var gotPrompt string
withAgentRunner(t, func(_ context.Context, p AgentParam) (*schema.Message, error) {
gotPrompt = p.UserPrompt
return &schema.Message{Role: schema.Assistant, Content: "ok"}, nil
})
c := NewAgentComponent(AgentParam{
ModelID: "stub",
APIKey: "test-key",
MaxRounds: 1,
})
if _, err := c.Invoke(context.Background(), map[string]any{
"user_prompt": "write answer",
"reasoning": "selected because it can answer",
"context": "known facts",
}); err != nil {
t.Fatalf("Invoke: %v", err)
}
want := "\nREASONING:\nselected because it can answer\n\nCONTEXT:\nknown facts\n\nQUERY:\nwrite answer\n"
if gotPrompt != want {
t.Fatalf("runner prompt = %q, want %q", gotPrompt, want)
}
}
func TestAgent_ToolCallRound(t *testing.T) {
var calls int
withAgentRunner(t, func(_ context.Context, _ AgentParam) (*schema.Message, error) {

View File

@@ -218,14 +218,7 @@ func (e *einoChatInvoker) Invoke(ctx context.Context, req ChatInvokeRequest) (*C
if driver == "" {
driver = "dummy"
}
// baseURL: drivers consult map["default"] as the canonical endpoint
// (see internal/entity/models/base_model.go:GetBaseURL). When the
// caller did not override, leave the driver default in place by
// passing nil — every driver seeds its own map at construction time.
var baseURL map[string]string
if req.BaseURL != "" {
baseURL = map[string]string{"default": req.BaseURL}
}
baseURL := baseURLMapForDriver(driver, req.BaseURL)
// urlSuffix: each driver appends URLSuffix.Chat to baseURL to form
// the chat-completions endpoint (e.g. "chat/completions" for
// openai-compatible drivers, "v1/messages" for anthropic). The
@@ -333,6 +326,25 @@ func chatURLSuffixFor(driver string) models.URLSuffix {
}
}
func baseURLMapForDriver(driver, override string) map[string]string {
if override != "" {
return map[string]string{"default": override}
}
pm := models.GetProviderManager()
if pm == nil {
return nil
}
provider := pm.FindProvider(driver)
if provider == nil || len(provider.URL) == 0 {
return nil
}
baseURL := make(map[string]string, len(provider.URL))
for region, url := range provider.URL {
baseURL[region] = url
}
return baseURL
}
// NewLLMComponent builds an LLMComponent from raw params.
func NewLLMComponent(p LLMParam) *LLMComponent {
return &LLMComponent{param: p}