mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-07 03:48:44 +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
147 lines
4.7 KiB
Go
147 lines
4.7 KiB
Go
// Package dsl provides the Go-native v2 schema, version auto-detection loader,
|
|
// and v1->v2 converter for the RAGFlow agent canvas.
|
|
//
|
|
// Phase 2.5 introduces the v2 schema. The Python-era v1 DSL (with the `obj`
|
|
// wrapper and `_feeded_deprecated_params` / `_deprecated_params` /
|
|
// `_user_feeded_params` legacy fields) is still loadable via the converter
|
|
// (see converter_v1_to_v2.go). v2 -> v1 round-trip is deferred to Phase 5.5.
|
|
//
|
|
// See plan: .claude/plans/agent-go-port.md
|
|
// - §3.1 (In-scope)
|
|
// - §2.11.7 (Python-era fields removed in v2)
|
|
// - §4.6 (DSL v2 schema)
|
|
// - §4.7 (v1 <-> v2 converter)
|
|
// - §5 Phase 2.5
|
|
package dsl
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// CurrentVersion is the schema version emitted by the v2 loader and converter.
|
|
const CurrentVersion = 2
|
|
|
|
// Canvas is the v2 in-memory model of a RAGFlow agent canvas.
|
|
//
|
|
// A Canvas is a directed graph keyed by Component.ID. v2 removes the v1
|
|
// `obj` wrapper and the three deprecated param sets; the schema is flat.
|
|
type Canvas struct {
|
|
// Version is always 2 for a Canvas produced by this package.
|
|
Version int `json:"version"`
|
|
|
|
// Components is keyed by Component.ID (v2 uses simple `name_id` keys,
|
|
// e.g. "begin_0", "llm_3" — no `Name:UUID` colon-prefixed keys).
|
|
Components map[string]Component `json:"components"`
|
|
}
|
|
|
|
// Component is a single node in a v2 canvas.
|
|
//
|
|
// Required fields are ID, Name, Downstream, Params. Outputs is optional and
|
|
// reserved for runtime materialization; it is not present in a freshly
|
|
// loaded DSL.
|
|
type Component struct {
|
|
// ID is the canvas-local identifier. For v1-converted components, it is
|
|
// the lowercased name + "_" + uuid, e.g. "agent_abc123".
|
|
ID string `json:"id"`
|
|
|
|
// Name is the human-readable component class name, e.g. "Retrieval",
|
|
// "LLM", "Agent". Matches v1 `obj.component_name`.
|
|
Name string `json:"name"`
|
|
|
|
// Downstream lists canvas-local IDs of components that follow this one.
|
|
// Empty for terminal nodes.
|
|
Downstream []string `json:"downstream"`
|
|
|
|
// Params is the v2 parameter bag. v1 legacy fields
|
|
// (_feeded_deprecated_params / _deprecated_params / _user_feeded_params)
|
|
// are dropped on conversion.
|
|
Params map[string]any `json:"params"`
|
|
|
|
// Outputs is the runtime materialization slot; not present in DSL on
|
|
// load. It is reserved for future Phase 5 execution wiring.
|
|
Outputs map[string]any `json:"outputs,omitempty"`
|
|
}
|
|
|
|
// Marshal serializes a Canvas to compact JSON bytes.
|
|
func (c *Canvas) Marshal() ([]byte, error) {
|
|
if c == nil {
|
|
return nil, fmt.Errorf("dsl: nil Canvas")
|
|
}
|
|
if c.Version == 0 {
|
|
c.Version = CurrentVersion
|
|
}
|
|
return json.Marshal(c)
|
|
}
|
|
|
|
// MarshalIndent serializes a Canvas to indented JSON bytes.
|
|
func (c *Canvas) MarshalIndent() ([]byte, error) {
|
|
if c == nil {
|
|
return nil, fmt.Errorf("dsl: nil Canvas")
|
|
}
|
|
if c.Version == 0 {
|
|
c.Version = CurrentVersion
|
|
}
|
|
return json.MarshalIndent(c, "", " ")
|
|
}
|
|
|
|
// UnmarshalV2 parses a v2 JSON byte slice into a Canvas. It rejects v1
|
|
// payloads (no `version` field, contains `obj` wrappers) — use Load for
|
|
// auto-detect, or LoadV1 for v1-only callers.
|
|
func UnmarshalV2(raw []byte) (*Canvas, error) {
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
var c Canvas
|
|
if err := dec.Decode(&c); err != nil {
|
|
return nil, fmt.Errorf("dsl: parse v2: %w", err)
|
|
}
|
|
if c.Version != CurrentVersion {
|
|
return nil, fmt.Errorf("dsl: expected version %d, got %d", CurrentVersion, c.Version)
|
|
}
|
|
if err := c.Validate(); err != nil {
|
|
return nil, fmt.Errorf("dsl: validate v2: %w", err)
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// Validate checks the structural well-formedness of a v2 Canvas.
|
|
//
|
|
// Rules:
|
|
// - Version must be 2.
|
|
// - Components map must be non-empty.
|
|
// - Each Component.Name must be non-empty.
|
|
// - Each Downstream entry must reference an existing component ID in
|
|
// Components (no dangling refs).
|
|
// - No `:` in component IDs (v2 forbids the v1 `Name:UUID` convention).
|
|
func (c *Canvas) Validate() error {
|
|
if c == nil {
|
|
return fmt.Errorf("nil Canvas")
|
|
}
|
|
if c.Version != CurrentVersion {
|
|
return fmt.Errorf("version=%d, expected %d", c.Version, CurrentVersion)
|
|
}
|
|
if len(c.Components) == 0 {
|
|
return fmt.Errorf("components map is empty")
|
|
}
|
|
for id, comp := range c.Components {
|
|
if strings.Contains(id, ":") {
|
|
return fmt.Errorf("component id %q contains ':' (v1 legacy)", id)
|
|
}
|
|
if comp.Name == "" {
|
|
return fmt.Errorf("component %q has empty Name", id)
|
|
}
|
|
if id != comp.ID && comp.ID != "" {
|
|
// comp.ID is set explicitly; allow if it matches the key.
|
|
return fmt.Errorf("component key %q does not match Component.ID %q", id, comp.ID)
|
|
}
|
|
for _, ds := range comp.Downstream {
|
|
if _, ok := c.Components[ds]; !ok {
|
|
return fmt.Errorf("component %q downstream %q does not exist", id, ds)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|