mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 23:41:12 +08:00
Replaces the Python agent canvas runtime with a Go implementation that runs inside `cmd/server_main`. The canvas compiles into an eino Workflow that pauses on wait-for-user via native Interrupt/Resume (no sentinel flag) and resumes from a Redis-backed CheckPointStore. All 21 Python agent components and ~35 tools are ported with functional parity. Sandbox providers now read their JSON config from the admin-panel system_settings table with env fallback. 234 files / +35,413 / -6,111. All Go files are gofmt-clean (CI gate added); drops the v2 DSL E2E step and the gap-analysis plan (both redundant after the port ships). ## Type of change - [x] Refactoring - [x] New feature - [x] Bug fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
186 lines
5.5 KiB
Go
186 lines
5.5 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
|
|
//
|
|
|
|
// migrate-canvas applies Go's dsl.NormalizeForCanvas to one or more
|
|
// JSON files and emits the normalized form. It is the "Go-side" of
|
|
// the parity corpus described in the agent-go-port-design doc §7.
|
|
//
|
|
// Usage:
|
|
//
|
|
// # Pretty-print the normalized form of a single DSL file:
|
|
// go run ./tools/migrate-canvas docs/develop/sample.json
|
|
//
|
|
// # Diff against a "golden" normalized file (CI mode):
|
|
// go run ./tools/migrate-canvas -golden=expected.json docs/develop/sample.json
|
|
//
|
|
// # Walk every testdata fixture and emit the normalized form:
|
|
// go run ./tools/migrate-canvas -walk internal/agent/dsl/testdata
|
|
//
|
|
// Behaviour:
|
|
// - In non-CI mode, the tool writes the normalized JSON to stdout
|
|
// (pretty-printed) and prints a one-line summary to stderr.
|
|
// - In CI mode (-golden=<path>), the tool compares the actual
|
|
// normalized form to the golden file and exits non-zero on drift.
|
|
// - The tool never panics on malformed input: NormalizeForCanvas
|
|
// is best-effort and the tool reports the result as a warning
|
|
// when the input is empty / unparseable.
|
|
//
|
|
// Limitations vs the original plan §7 migrate-canvas spec:
|
|
// - This tool does NOT shell out to Python. The Python side's
|
|
// normalize_chunker_dsl is for the chunker DSL, which is a
|
|
// different domain (chunking pipeline, not agent canvas). The
|
|
// Go-side NormalizeForCanvas covers the agent canvas normalize
|
|
// path; the chunker DSL still goes through the Python path
|
|
// (deferred per plan v3.3.1 user decision).
|
|
// - The "fixture corpus" used in CI is the existing 7 files in
|
|
// internal/agent/dsl/testdata/; the tool can be extended to
|
|
// add a -generate-golden flag that writes the current output
|
|
// as the new golden (the standard update-on-intent pattern).
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
dslpkg "ragflow/internal/agent/dsl"
|
|
)
|
|
|
|
func main() {
|
|
goldenFlag := flag.String("golden", "", "path to golden file for CI drift check (mutually exclusive with -write-golden)")
|
|
writeGolden := flag.Bool("write-golden", false, "write the normalized output to <input>.golden (update pattern)")
|
|
walkDir := flag.String("walk", "", "if set, treat positional args as a directory; every *.json file is normalised")
|
|
flag.Parse()
|
|
|
|
if *goldenFlag != "" && *writeGolden {
|
|
fmt.Fprintln(os.Stderr, "migrate-canvas: -golden and -write-golden are mutually exclusive")
|
|
os.Exit(2)
|
|
}
|
|
|
|
if *walkDir != "" {
|
|
// Walk directory mode: positional args ignored; walk <walkDir>/**/*.json.
|
|
runWalk(*walkDir, *goldenFlag, *writeGolden)
|
|
return
|
|
}
|
|
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
fmt.Fprintln(os.Stderr, "migrate-canvas: expected <file.json> (or -walk <dir>)")
|
|
flag.CommandLine.Usage()
|
|
os.Exit(2)
|
|
}
|
|
|
|
exit := 0
|
|
for _, path := range args {
|
|
if err := runOne(path, *goldenFlag, *writeGolden); err != nil {
|
|
fmt.Fprintf(os.Stderr, "FAIL %s: %v\n", path, err)
|
|
exit = 1
|
|
}
|
|
}
|
|
os.Exit(exit)
|
|
}
|
|
|
|
func runOne(path, goldenPath string, writeGolden bool) error {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("read: %w", err)
|
|
}
|
|
var dsl map[string]any
|
|
if err := json.Unmarshal(raw, &dsl); err != nil {
|
|
return fmt.Errorf("parse: %w", err)
|
|
}
|
|
normalised := dslpkg.NormalizeForCanvas(dsl)
|
|
|
|
if writeGolden {
|
|
goldenPath = path + ".golden"
|
|
out, mErr := marshalPretty(normalised)
|
|
if mErr != nil {
|
|
return fmt.Errorf("marshal: %w", mErr)
|
|
}
|
|
if err := os.WriteFile(goldenPath, out, 0o644); err != nil {
|
|
return fmt.Errorf("write golden: %w", err)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "OK %s -> %s\n", path, goldenPath)
|
|
return nil
|
|
}
|
|
|
|
if goldenPath != "" {
|
|
golden, gErr := os.ReadFile(goldenPath)
|
|
if gErr != nil {
|
|
return fmt.Errorf("read golden: %w", gErr)
|
|
}
|
|
actual, mErr := marshalPretty(normalised)
|
|
if mErr != nil {
|
|
return fmt.Errorf("marshal: %w", mErr)
|
|
}
|
|
if !bytes.Equal(bytes.TrimSpace(golden), bytes.TrimSpace(actual)) {
|
|
return fmt.Errorf("drift: golden != actual (run with -write-golden to update)")
|
|
}
|
|
fmt.Fprintf(os.Stderr, "OK %s (matches %s)\n", path, goldenPath)
|
|
return nil
|
|
}
|
|
|
|
out, mErr := marshalPretty(normalised)
|
|
if mErr != nil {
|
|
return fmt.Errorf("marshal: %w", mErr)
|
|
}
|
|
fmt.Print(string(out))
|
|
fmt.Fprintf(os.Stderr, "OK %s (%d bytes)\n", path, len(out))
|
|
return nil
|
|
}
|
|
|
|
func runWalk(dir, goldenDir string, writeGolden bool) {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "migrate-canvas: walk: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
var files []string
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
name := e.Name()
|
|
if !strings.HasSuffix(name, ".json") {
|
|
continue
|
|
}
|
|
files = append(files, filepath.Join(dir, name))
|
|
}
|
|
sort.Strings(files)
|
|
exit := 0
|
|
for _, p := range files {
|
|
var golden string
|
|
if goldenDir != "" {
|
|
golden = filepath.Join(goldenDir, filepath.Base(p)+".golden")
|
|
}
|
|
if err := runOne(p, golden, writeGolden); err != nil {
|
|
fmt.Fprintf(os.Stderr, "FAIL %s: %v\n", p, err)
|
|
exit = 1
|
|
}
|
|
}
|
|
os.Exit(exit)
|
|
}
|
|
|
|
func marshalPretty(v any) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetIndent("", " ")
|
|
enc.SetEscapeHTML(false)
|
|
if err := enc.Encode(v); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|