mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
182 lines
6.3 KiB
Go
182 lines
6.3 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
// Package component — Message component (T3, plan §2.11.3 row 4).
|
|
//
|
|
// Message is the canvas terminal output node. It resolves a Jinja2-style
|
|
// {{...}} template against the current *CanvasState and (optionally) emits
|
|
// the result as a single SSE chunk. Memory persistence and chunked
|
|
// streaming are deferred (plan §2.11.6 P0 scope): the P0 implementation
|
|
// resolves the template once, returns it as outputs["content"], and
|
|
// exposes Stream() that yields one chunk + closes.
|
|
package component
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"maps"
|
|
|
|
"ragflow/internal/agent/runtime"
|
|
)
|
|
|
|
const componentNameMessage = "Message"
|
|
|
|
// MessageComponent is the canvas terminal output node. It owns the
|
|
// resolved text template as a per-instance field — the factory sets
|
|
// it from the DSL params at build time, and Invoke falls back to it
|
|
// when the input map does not carry a fresh "text" override.
|
|
type MessageComponent struct {
|
|
name string
|
|
text string
|
|
}
|
|
|
|
// NewMessageComponent constructs a Message component. The params map
|
|
// may carry:
|
|
//
|
|
// - "text" (string) — the canonical v2 name
|
|
// - "content" (string | []string | []any) — the v1 name; when a
|
|
// list, the first element is taken as the template (matches the
|
|
// Python v1 message surface where content is a list of paragraphs)
|
|
//
|
|
// At least one must produce a non-empty string; otherwise the node
|
|
// emits an empty content (it is the canvas terminal, so a runtime
|
|
// error would be louder than a missing template).
|
|
func NewMessageComponent(params map[string]any) (Component, error) {
|
|
tpl := extractMessageText(params)
|
|
return &MessageComponent{name: componentNameMessage, text: tpl}, nil
|
|
}
|
|
|
|
// extractMessageText reads text / content from params in the v1 / v2
|
|
// order documented on NewMessageComponent. Returns the empty string
|
|
// when neither key is present or the value is not a string-shaped
|
|
// scalar.
|
|
func extractMessageText(params map[string]any) string {
|
|
if v, ok := params["text"].(string); ok {
|
|
return v
|
|
}
|
|
if v, ok := params["content"]; ok {
|
|
switch x := v.(type) {
|
|
case string:
|
|
return x
|
|
case []string:
|
|
if len(x) > 0 {
|
|
return x[0]
|
|
}
|
|
case []any:
|
|
if len(x) > 0 {
|
|
if s, ok := x[0].(string); ok {
|
|
return s
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Name returns the registered component name.
|
|
func (m *MessageComponent) Name() string { return m.name }
|
|
|
|
// Invoke resolves inputs["text"] (or the per-instance text seeded
|
|
// from params at build time) as a template against the current
|
|
// *CanvasState, returns the resolved string at outputs["content"], and
|
|
// (if inputs["stream"] == true) records the number of chunks in
|
|
// outputs["streamed_chunks"]. Memory persistence (memory_save) is
|
|
// logged as deferred to a later phase per the P0 plan.
|
|
//
|
|
// inputs["text"] takes precedence over the per-instance text so the
|
|
// same node can be reused with different templates at run time when
|
|
// the orchestrator wants to override the DSL-declared value.
|
|
func (m *MessageComponent) Invoke(ctx context.Context, inputs map[string]any) (map[string]any, error) {
|
|
state, _, err := runtime.GetStateFromContext[*runtime.CanvasState](ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Message: %w", err)
|
|
}
|
|
if state == nil {
|
|
return nil, fmt.Errorf("Message: nil canvas state")
|
|
}
|
|
|
|
text, _ := inputs["text"].(string)
|
|
if text == "" {
|
|
text = m.text
|
|
}
|
|
resolved, err := runtime.ResolveTemplate(text, state)
|
|
if err != nil {
|
|
// ResolveTemplate surfaces unresolved references as errors, but
|
|
// the partial output (with empty-string substitutions) is still
|
|
// returned so the SSE consumer can choose to log it. Match
|
|
// the existing canvas package's contract here.
|
|
return nil, fmt.Errorf("Message: template resolve: %w", err)
|
|
}
|
|
|
|
if memSave, _ := inputs["memory_save"].(bool); memSave {
|
|
log.Printf("Message: memory_save=true (memory persistence deferred to Phase 2.5)")
|
|
}
|
|
|
|
out := map[string]any{"content": resolved}
|
|
if streamOn, _ := inputs["stream"].(bool); streamOn {
|
|
// P0: one chunk for the whole resolved content. A later phase
|
|
// can split on token / sentence boundaries.
|
|
out["streamed_chunks"] = 1
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Stream is the SSE variant. The P0 implementation produces a single
|
|
// chunk containing the resolved content (key "content") and closes the
|
|
// channel. A future phase can split the resolved string into multiple
|
|
// chunks; for now the contract is "one chunk, then close".
|
|
func (m *MessageComponent) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {
|
|
out, err := m.Invoke(ctx, inputs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ch := make(chan map[string]any, 1)
|
|
ch <- out
|
|
close(ch)
|
|
return ch, nil
|
|
}
|
|
|
|
// Inputs returns the public parameter surface. Field types match the
|
|
// Python DSL contract (text template, stream toggle, memory_save toggle).
|
|
func (m *MessageComponent) Inputs() map[string]string {
|
|
return map[string]string{
|
|
"text": "Template string with {{...}} references; resolved against the canvas state.",
|
|
"stream": "When true, the resolved content is delivered as an SSE stream (P0: one chunk).",
|
|
"memory_save": "When true, persist the message to API4Conversation (deferred to Phase 2.5).",
|
|
}
|
|
}
|
|
|
|
// Outputs returns the resolved template plus the streamed-chunk counter.
|
|
func (m *MessageComponent) Outputs() map[string]string {
|
|
return map[string]string{
|
|
"content": "Resolved template string (the message text).",
|
|
"streamed_chunks": "Number of SSE chunks emitted (present when stream=true).",
|
|
}
|
|
}
|
|
|
|
// mapCopy shallow-copies src into a fresh map. Used to keep Message's
|
|
// passthrough outputs un-aliased from the caller's inputs map.
|
|
func mapCopy(src map[string]any) map[string]any {
|
|
out := make(map[string]any, len(src))
|
|
maps.Copy(out, src)
|
|
return out
|
|
}
|
|
|
|
func init() {
|
|
Register(componentNameMessage, NewMessageComponent)
|
|
}
|