mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-04 23:00:30 +08:00
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
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
This commit is contained in:
181
internal/agent/component/message.go
Normal file
181
internal/agent/component/message.go
Normal file
@@ -0,0 +1,181 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user