Files
ragflow/internal/agent/dsl/v2.go
Zhichang Yu 3fa15c0e2f 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
2026-06-12 22:58:28 +08:00

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
}