Files
ragflow/internal/agent/runtime/template_jinja.go
Zhichang Yu e45659868a feat(agent): ship the Go agent canvas port — eino interrupt/resume + Redis check-pointing (#16035)
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>
2026-06-17 13:24:03 +08:00

162 lines
5.8 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.
//
// template_jinja.go — gonja direct import.
//
// Gonja was already a declared dependency in go.mod (marked
// `// indirect`). This file promotes gonja to a direct import by
// adding a parallel template resolver that handles Jinja2-specific
// syntax ({% ... %}, {{ x | filter }}, {# comment #}).
//
// The regex-based ResolveTemplate in template.go is kept as the
// fast path for the common `{{cpn_id@key}}` form — that path
// compiles to a single regex match + lookup, no parser
// allocation. ResolveTemplateAuto dispatches to one or the other
// based on a one-byte scan for Jinja2 markers; for the common
// case the dispatch cost is just the scan.
package runtime
import (
"bytes"
"fmt"
"strings"
"github.com/nikolalohinski/gonja"
)
// ContainsJinjaSyntax reports whether s contains any Jinja2-only
// construct that the regex fast path can't handle:
//
// - {% ... %} statements (if, for, set, ...)
// - {# ... #} comments
// - | filter pipe inside a {{...}} expression
//
// Pure `{{var}}` references (the common v1 case) return false —
// the fast path handles them. The check is a single byte scan, not
// a parse, so the cost is negligible.
func ContainsJinjaSyntax(s string) bool {
if strings.Contains(s, "{%") {
return true
}
if strings.Contains(s, "{#") {
return true
}
// Filter pipe: a '|' inside a {{...}} block. We don't need to
// track whether we're inside braces — the regex fast path
// would not match `{{x|filter}}` correctly because the '|'
// isn't part of the ref grammar, so any pipe anywhere is
// grounds for falling back to gonja.
if strings.ContainsRune(s, '|') {
return true
}
return false
}
// ResolveTemplateJinja renders s through gonja. The execution
// context is built from the canvas state — every cpn_id@key pair
// GetVar knows about becomes a top-level variable in the template
// scope, alongside the special `sys.*` and `env.*` names.
//
// The conversion from CanvasState to gonja context is a best-effort
// flatten: nested maps become dotted identifiers (matching the v1
// `{{cpn_id@key.subkey}}` grammar that GetVar already understands),
// other types are passed through unchanged. The flattening is
// shallow — gonja templates that walk deeply nested structures
// may need a future revision that builds a proper nested map.
//
// Loud-fail on parse / execution errors: the regex fast path
// degrades gracefully (it returns the partial output plus an
// error); the Jinja2 path either parses cleanly or returns an
// error with the original string untouched. Callers that want the
// "always return a string" semantics can wrap the call themselves.
func ResolveTemplateJinja(s string, state *CanvasState) (string, error) {
if state == nil {
return "", fmt.Errorf("template: nil canvas state")
}
ctx := stateToGonjaContext(state)
tpl, err := gonja.FromString(s)
if err != nil {
return "", fmt.Errorf("template: parse: %w", err)
}
out, err := tpl.Execute(ctx)
if err != nil {
return "", fmt.Errorf("template: execute: %w", err)
}
return out, nil
}
// stateToGonjaContext builds a map[string]any from a CanvasState
// that gonja can resolve. Top-level keys are the cpn ids
// (begin_0, llm_0, ...); values are the per-cpn output buckets
// passed through unchanged so gonja can walk nested structures
// with its native dot syntax ({{ agent_0.user.name }} resolves
// to the bucket's "user" sub-map's "name" key).
func stateToGonjaContext(state *CanvasState) map[string]any {
ctx := make(map[string]any, len(state.Outputs))
for cpnID, bucket := range state.Snapshot() {
ctx[cpnID] = bucket
}
return ctx
}
// flattenMap converts a nested map[string]any into a flat map
// where nested keys are joined by '.'. Lists / scalars are
// passed through. Used by stateToGonjaContext so gonja can walk
// `{{ cpn_0.user.name }}` the same way GetVar walks
// `cpn_0@user.name`.
func flattenMap(m map[string]any) map[string]any {
out := make(map[string]any, len(m))
for k, v := range m {
if nested, ok := v.(map[string]any); ok {
for nk, nv := range flattenMap(nested) {
out[k+"."+nk] = nv
}
continue
}
out[k] = v
}
return out
}
// ResolveTemplateAuto dispatches to the regex fast path
// (ResolveTemplate) or the Jinja2 path (ResolveTemplateJinja)
// based on whether the input contains Jinja2-only syntax. The
// fast path is preferred because:
//
// - It compiles to a single regex match + state lookup — no
// parse allocation, no map flatten.
// - It already handles every `{{ cpn_id@key }}` form that
// RAGFlow v1 fixtures use (per plan §2.11.6 entry for
// Switch, Categorize, etc.).
// - It fails loud on unresolvable refs — useful for catching
// misconfigured canvases at runtime.
//
// Falls back to gonja only when the input has Jinja2 markers
// ({% %}, {# #}, or | inside a {{...}}). Callers that always
// want Jinja2 semantics can call ResolveTemplateJinja directly.
func ResolveTemplateAuto(s string, state *CanvasState) (string, error) {
if !ContainsJinjaSyntax(s) {
return ResolveTemplate(s, state)
}
return ResolveTemplateJinja(s, state)
}
// bytesBuffer is a no-op import-anchor kept for the future
// streaming-resolver path (chunked streaming; not implemented
// in this revision).
var _ = bytes.NewBuffer