Files
ragflow/internal/agent/component/begin.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

127 lines
4.4 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.
//
// Package component — Begin component (T3).
//
// Begin is the DSL entry node. It injects the request's `inputs` into
// the shared *CanvasState.Sys namespace and passes the input map
// through to its downstream unchanged. File-input handling
// (FileService.get_files) handles `query`, `user_id`, and file
// inputs alike.
package component
import (
"context"
"fmt"
"maps"
"ragflow/internal/agent/runtime"
)
// mapsCopy is a thin alias for the stdlib maps.Copy to keep the call
// sites uniform with the rest of the package (which uses the same name
// in switch.go and message.go).
func mapsCopy(dst, src map[string]any) {
maps.Copy(dst, src)
}
const componentNameBegin = "Begin"
// BeginComponent is the canvas entry node. The exported fields are
// populated by the factory (registered via init) from the DSL params map.
// ParamBase surface is intentionally omitted for P0 — Begin is trivial
// and needs no validation beyond what the State writes perform.
type BeginComponent struct {
name string
}
// NewBeginComponent constructs a Begin component. It accepts the DSL params
// map but does not retain it (Begin has no per-instance configuration).
func NewBeginComponent(_ map[string]any) (Component, error) {
return &BeginComponent{name: componentNameBegin}, nil
}
// Name returns the registered component name. Used by the registry and
// the eino node-name injection in BuildWorkflow.
func (b *BeginComponent) Name() string { return b.name }
// Invoke writes inputs["query"] and (when present) inputs["user_id"] into
// the shared *CanvasState.Sys namespace, then returns the input map as
// outputs unchanged. The input map is shallow-copied to avoid aliasing
// surprises across concurrent goroutines that share an inputs map.
func (b *BeginComponent) 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("Begin: %w", err)
}
if state == nil {
return nil, fmt.Errorf("Begin: nil canvas state")
}
// Query: required to drive downstream components.
query, _ := inputs["query"].(string)
state.Sys["query"] = query
// Optional user_id — present in interactive chat flows, absent in
// background jobs. Always a string when set; cast failure silently
// drops the value (mirrors Python's getattr fallback).
if uid, ok := inputs["user_id"].(string); ok && uid != "" {
state.Sys["user_id"] = uid
}
// Passthrough: a shallow copy keeps the caller's map un-aliased.
out := make(map[string]any, len(inputs))
mapsCopy(out, inputs)
return out, nil
}
// Stream is a synchronous facade over Invoke for P0. SSE streaming of
// Begin output is not meaningful (Begin has no I/O), so the channel
// receives a single payload and closes — same shape as Invoke's return.
func (b *BeginComponent) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {
out, err := b.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 parameter metadata. Descriptions are short; the doc
// strings live on the struct / method above.
func (b *BeginComponent) Inputs() map[string]string {
return map[string]string{
"query": "User query string (the chat input).",
"user_id": "Optional user/tenant identifier.",
"inputs": "Optional free-form inputs map; passthrough only.",
}
}
// Outputs returns the same keys as Inputs (Begin is a passthrough).
func (b *BeginComponent) Outputs() map[string]string {
return map[string]string{
"query": "Query string (passthrough).",
"user_id": "User id, if provided (passthrough).",
"inputs": "Raw inputs map (passthrough).",
}
}
func init() {
Register(componentNameBegin, NewBeginComponent)
}