mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-19 22:21:04 +08:00
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:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user