mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-26 02:13:29 +08:00
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>
This commit is contained in:
@@ -21,20 +21,20 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// ErrCodeExecSandboxMissing is returned when the Phase 5 gRPC client to the
|
||||
// Python sandbox (agent.sandbox.client.execute_code) is not yet wired. The
|
||||
// Python sandbox itself is kept as-is per plan §2.11.4 (do NOT rewrite the
|
||||
// sandbox); Phase 3 batch 1 only ships the tool shell, and Phase 5 adds
|
||||
// the gRPC client.
|
||||
// ErrCodeExecSandboxMissing is returned when no sandbox client is
|
||||
// registered. The Python sandbox itself is kept as-is (the Go
|
||||
// side never reimplemented the sandbox). When a client is
|
||||
// registered via SetSandboxClient at boot, the tool dispatches
|
||||
// the execution.
|
||||
var ErrCodeExecSandboxMissing = errors.New(
|
||||
"CodeExec sandbox gRPC client not yet implemented in Go — " +
|
||||
"defer to Python Canvas",
|
||||
"CodeExec sandbox client not registered — call SetSandboxClient at boot",
|
||||
)
|
||||
|
||||
const codeExecToolName = "execute_code"
|
||||
@@ -42,30 +42,44 @@ const codeExecToolName = "execute_code"
|
||||
const codeExecToolDescription = "This tool has a sandbox that can execute code written in 'Python'/'Javascript'. " +
|
||||
"It receives a piece of code and returns a JSON string."
|
||||
|
||||
// codeExecArgs is the JSON shape the model sends in. The Python tool
|
||||
// accepts "lang" + "script" (see agent/tools/code_exec.py:303-310); we
|
||||
// also accept "code" as a synonym since some DSLs and Phase 3 batch 1
|
||||
// tests use that spelling.
|
||||
// codeExecArgs is the JSON shape the model sends in. The Python
|
||||
// tool accepts "lang" + "script"; we also accept "code" as a
|
||||
// synonym since some DSLs and tests use that spelling.
|
||||
type codeExecArgs struct {
|
||||
Language string `json:"language,omitempty"`
|
||||
Lang string `json:"lang,omitempty"`
|
||||
Script string `json:"script,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Args map[string]any `json:"arguments,omitempty"`
|
||||
// Timeout is the per-execution wall-clock budget in seconds. 0
|
||||
// (the default) defers to the sandbox provider's own default
|
||||
// (typically 30s). Mirrors Python's
|
||||
// `code_exec.py:358 timeout_seconds = int(os.environ.get(...))`
|
||||
// but the value flows per-call rather than per-process, which
|
||||
// lets the model dial up/down for known-fast vs. known-slow
|
||||
// scripts.
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// codeExecResult is the JSON envelope returned to the model. The output
|
||||
// shape mirrors the Python tool's `content` / `_ERROR` / `actual_type`
|
||||
// fields so downstream nodes can pattern-match unchanged.
|
||||
// fields so downstream nodes can pattern-match unchanged. Artifacts and
|
||||
// Attachments are surfaced for the model and downstream component
|
||||
// consumption (e.g. Message component's artifact markdown formatter).
|
||||
type codeExecResult struct {
|
||||
Content string `json:"content,omitempty"`
|
||||
ActualType string `json:"actual_type,omitempty"`
|
||||
Stub bool `json:"stub,omitempty"`
|
||||
Error string `json:"_ERROR,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ActualType string `json:"actual_type,omitempty"`
|
||||
Stub bool `json:"stub,omitempty"`
|
||||
Error string `json:"_ERROR,omitempty"`
|
||||
ExitCode int `json:"exit_code,omitempty"`
|
||||
Stdout string `json:"stdout,omitempty"`
|
||||
Stderr string `json:"stderr,omitempty"`
|
||||
Artifacts []map[string]any `json:"_ARTIFACTS,omitempty"`
|
||||
Attachments []map[string]any `json:"attachments,omitempty"`
|
||||
}
|
||||
|
||||
// CodeExecTool is the Phase 3 batch 1 shell for the CodeExec tool
|
||||
// (plan §2.11.4 row 3, §5 Phase 3 第 1 批). It validates language +
|
||||
// CodeExecTool is the for the CodeExec tool
|
||||
// ( . It validates language +
|
||||
// non-empty code and returns a structured "not-yet-wired" error.
|
||||
type CodeExecTool struct{}
|
||||
|
||||
@@ -76,7 +90,7 @@ func NewCodeExecTool() *CodeExecTool {
|
||||
}
|
||||
|
||||
// Info returns the tool's metadata for the chat model. The schema mirrors
|
||||
// the Python CodeExecParam ToolMeta (plan §5 Phase 3, 字段对齐).
|
||||
// the Python CodeExecParam ToolMeta (plan , 字段对齐).
|
||||
func (c *CodeExecTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: codeExecToolName,
|
||||
@@ -97,9 +111,8 @@ func (c *CodeExecTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InvokableRun validates the inputs and returns the structured stub error.
|
||||
// Phase 5 will replace the stub body with a gRPC call into the Python
|
||||
// sandbox (per plan §2.11.4 "不重写沙箱" 决策).
|
||||
// InvokableRun validates the inputs and dispatches to the
|
||||
// registered sandbox client via SetSandboxClient.
|
||||
func (c *CodeExecTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
|
||||
var args codeExecArgs
|
||||
if argumentsInJSON == "" {
|
||||
@@ -124,9 +137,96 @@ func (c *CodeExecTool) InvokableRun(ctx context.Context, argumentsInJSON string,
|
||||
return codeExecStubResult("code is required"), errors.New("code_exec: empty code")
|
||||
}
|
||||
|
||||
// Phase 3 batch 1: gRPC client wires in Phase 5.
|
||||
return codeExecStubResult(ErrCodeExecSandboxMissing.Error()),
|
||||
ErrCodeExecSandboxMissing
|
||||
// Dispatch to the registered SandboxClient. When the default
|
||||
// stub is in place, the call surfaces
|
||||
// ErrCodeExecSandboxMissing; once a real client is
|
||||
// installed via SetSandboxClient at boot, the script runs.
|
||||
client := GetSandboxClient()
|
||||
resp, err := client.ExecuteCode(ctx, SandboxRequest{
|
||||
Lang: lang,
|
||||
Script: script,
|
||||
Arguments: args.Args,
|
||||
Timeout: args.Timeout,
|
||||
})
|
||||
if err != nil {
|
||||
return codeExecStubResult(err.Error()), err
|
||||
}
|
||||
out, mErr := codeExecResultJSON(resp)
|
||||
if mErr != nil {
|
||||
return codeExecStubResult(mErr.Error()), mErr
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// codeExecResultJSON serializes a SandboxResponse into the envelope
|
||||
// the eino tool contract returns. Field mapping mirrors the Python
|
||||
// tool's `code_exec.py:385-490` `_process_execution_result`:
|
||||
//
|
||||
// - Stdout / Stderr / ExitCode: stream directly through.
|
||||
// - Returned → Content (the model's natural "what did main() give
|
||||
// us back" field).
|
||||
// - StructuredResult["actual_type"] → ActualType (Python
|
||||
// `infer_actual_type` surface for downstream Message component).
|
||||
// - Metadata["artifacts"] → Artifacts (the model AND the Message
|
||||
// component's `_ARTIFACTS` collector both consume this; we
|
||||
// surface it as `_ARTIFACTS` to match the Python envelope).
|
||||
// - Metadata["attachments"] → Attachments (rendered into
|
||||
// downstream Markdown by Message via the same path the Agent
|
||||
// tool artifact markdown uses).
|
||||
//
|
||||
// Artifacts / Attachments with the wrong element type (anything
|
||||
// other than map[string]any) are silently dropped with a log
|
||||
// warning. This matches the Python tool's "skip on shape mismatch"
|
||||
// semantics — better to lose one artifact than to abort the run.
|
||||
func codeExecResultJSON(r *SandboxResponse) (string, error) {
|
||||
if r == nil {
|
||||
return codeExecStubResult("empty response"), nil
|
||||
}
|
||||
out := codeExecResult{
|
||||
Content: r.Returned,
|
||||
ExitCode: r.ExitCode,
|
||||
Stdout: r.Stdout,
|
||||
Stderr: r.Stderr,
|
||||
}
|
||||
if r.StructuredResult != nil {
|
||||
if v, ok := r.StructuredResult["actual_type"].(string); ok {
|
||||
out.ActualType = v
|
||||
}
|
||||
}
|
||||
if r.Metadata != nil {
|
||||
out.Artifacts = extractArtifactList(r.Metadata, "artifacts")
|
||||
out.Attachments = extractArtifactList(r.Metadata, "attachments")
|
||||
}
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("code_exec: marshal result: %w", err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// extractArtifactList pulls a list of dict-shaped entries out of
|
||||
// Metadata[key]. Items that aren't map[string]any are dropped with
|
||||
// a stderr log line so the operator can see the data loss without
|
||||
// the run aborting.
|
||||
func extractArtifactList(meta map[string]any, key string) []map[string]any {
|
||||
raw, ok := meta[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
arr, ok := raw.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]map[string]any, 0, len(arr))
|
||||
for i, item := range arr {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "code_exec: %s[%d] is %T, expected map[string]any; dropping\n", key, i, item)
|
||||
continue
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func codeExecStubResult(msg string) string {
|
||||
|
||||
Reference in New Issue
Block a user