mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-01 13:33:48 +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:
@@ -39,6 +39,8 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"ragflow/internal/agent/audio"
|
||||
"ragflow/internal/agent/canvas"
|
||||
"ragflow/internal/agent/runtime"
|
||||
"ragflow/internal/dao"
|
||||
"ragflow/internal/engine"
|
||||
@@ -240,7 +242,29 @@ func startServer(config *server.Config) {
|
||||
mcpHandler := handler.NewMCPHandler(mcpService)
|
||||
skillSearchHandler := handler.NewSkillSearchHandler(docEngine)
|
||||
providerHandler := handler.NewProviderHandler(userService, modelProviderService)
|
||||
agentHandler := handler.NewAgentHandler(service.NewAgentService(), fileService)
|
||||
// Install the agent service's Redis-backed run infrastructure
|
||||
// (CheckPointStore / StateSerializer / RunTracker). When Redis
|
||||
// is unreachable (degraded boot, stand-alone mode, no-redis CI)
|
||||
// the constructors return errors and we fall through to the
|
||||
// in-memory / no-tracking path: the agent service treats nil
|
||||
// options as the in-memory test path, so graceful degradation
|
||||
// is a 1-line if-not-nil pass-through — no separate "boot" mode
|
||||
// required.
|
||||
agentOpts := buildAgentRunOptions()
|
||||
agentHandler := handler.NewAgentHandler(service.NewAgentServiceWithOptions(
|
||||
agentOpts.checkpointStore,
|
||||
agentOpts.stateSerializer,
|
||||
agentOpts.runTracker,
|
||||
), fileService)
|
||||
|
||||
// Wire the TTS synthesizer to the per-tenant model-provider
|
||||
// dispatch. SynthesizeRequest is routed through
|
||||
// ModelProviderService.AudioSpeech, which fans out to the
|
||||
// tenant's configured TTS model driver. When the model
|
||||
// provider is unconfigured, the synthesizer falls back to a
|
||||
// no-op echo (the audio package contract), so this is always
|
||||
// safe to call.
|
||||
configureTTSSynthesizer(modelProviderService)
|
||||
searchBotLLM := &handler.SearchBotRealLLM{Svc: modelProviderService}
|
||||
searchBotHandler := handler.NewSearchBotHandler(
|
||||
searchService,
|
||||
@@ -266,16 +290,16 @@ func startServer(config *server.Config) {
|
||||
docEngine,
|
||||
)
|
||||
|
||||
// Phase 6 per-tenant canvas-runtime override. The selector is backed by
|
||||
// the existing Redis client and the global logger. The handler is
|
||||
// ALWAYS constructed, even when Redis is briefly unavailable at startup,
|
||||
// so the POST /api/v1/admin/canvas-runtime/:tenant_id endpoint stays
|
||||
// registered and returns the explicit ErrSelectorNotConfigured (HTTP 500)
|
||||
// path until Redis recovers. The previous behaviour — skipping handler
|
||||
// construction when rdb == nil — silently removed the route until the
|
||||
// next process restart, so a transient Redis blip at boot stranded
|
||||
// canary operators with a 404 they could not diagnose from the client
|
||||
// side. Review follow-up: keep the route hot.
|
||||
// Per-tenant canvas-runtime override selector, backed by the
|
||||
// existing Redis client and the global logger. The handler is
|
||||
// ALWAYS constructed, even when Redis is briefly unavailable at
|
||||
// startup, so the POST /api/v1/admin/canvas-runtime/:tenant_id
|
||||
// endpoint stays registered and returns the explicit
|
||||
// ErrSelectorNotConfigured (HTTP 500) path until Redis recovers.
|
||||
// Skipping handler construction when rdb == nil silently removed
|
||||
// the route until the next process restart, so a transient
|
||||
// Redis blip at boot stranded canary operators with a 404 they
|
||||
// could not diagnose from the client side. Keep the route hot.
|
||||
var adminRuntimeSelector *runtime.Selector
|
||||
if rdb := redis.Get().GetClient(); rdb != nil {
|
||||
adminRuntimeSelector = runtime.NewSelector(rdb, common.Logger)
|
||||
@@ -371,3 +395,72 @@ func startServer(config *server.Config) {
|
||||
common.Fatal("Server forced to shutdown", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// agentRunOptions bundles the three optional injection slots the
|
||||
// agent service accepts via NewAgentServiceWithOptions: the Redis-
|
||||
// backed CheckPointStore, StateSerializer, and RunTracker. The
|
||||
// fields stay nil when the underlying constructors fail (Redis
|
||||
// unreachable, etc.); the agent service treats nil as "in-memory
|
||||
// / no-tracking" so the server continues to serve traffic without
|
||||
// requiring Redis to be up.
|
||||
type agentRunOptions struct {
|
||||
checkpointStore canvas.CheckPointStore
|
||||
stateSerializer canvas.StateSerializer
|
||||
runTracker *canvas.RunTracker
|
||||
}
|
||||
|
||||
// buildAgentRunOptions installs the Redis-backed run infrastructure
|
||||
// when Redis is available. The Redis client is the one already
|
||||
// initialised at the top of main; the TTL is a conservative 24h for
|
||||
// both the checkpoint store and the run tracker. On any error
|
||||
// (Redis down at boot, constructor panic, nil-Redis fallback) we
|
||||
// log and return a zero-value struct — the agent service falls back
|
||||
// to the in-memory path transparently.
|
||||
func buildAgentRunOptions() agentRunOptions {
|
||||
var out agentRunOptions
|
||||
if !redis.IsEnabled() || redis.Get() == nil {
|
||||
common.Info("agent: redis client not initialised; agent run infra in in-memory mode (no checkpoints, no run tracker)")
|
||||
return out
|
||||
}
|
||||
cp := canvas.NewRedisCheckPointStore(24 * time.Hour)
|
||||
out.checkpointStore = cp
|
||||
// stateSerializer is intentionally left nil. eino's default
|
||||
// InternalSerializer (used when no compose.WithSerializer is
|
||||
// passed at compile time) already knows how to round-trip
|
||||
// runtime.CanvasState because the runtime package registers
|
||||
// it via compose.RegisterSerializableType[CanvasState] in
|
||||
// init(). Overriding with RAGFlow's plain-JSON
|
||||
// CanvasStateSerializer (json.Marshal/Unmarshal) produces
|
||||
// bytes the InternalSerializer cannot decode on the resume
|
||||
// pass — the UserFillUp two-node pattern surfaces this as
|
||||
// "load checkpoint from store fail: cannot unmarshal object
|
||||
// into Go struct field checkpoint.Channels of type
|
||||
// compose.channel". Rely on eino's default instead.
|
||||
rt := canvas.NewRunTracker(24 * time.Hour)
|
||||
out.runTracker = rt
|
||||
common.Info("agent: redis-backed run infra installed (24h TTL on checkpoint store + run tracker; eino default serializer)")
|
||||
return out
|
||||
}
|
||||
|
||||
// configureTTSSynthesizer installs the audio.ModelProviderFunc
|
||||
// that dispatches Synthesize requests through the project's
|
||||
// ModelProviderService. The model provider's AudioSpeech method
|
||||
// (internal/service/model_service.go) resolves the per-tenant TTS
|
||||
// model driver, sends the request upstream, and returns
|
||||
// synthesized audio bytes.
|
||||
//
|
||||
// The audio package's NewTTSDispatchFunc helper converts the
|
||||
// audio.SynthesizeRequest shape into the model's dispatch shape
|
||||
// (audioContent = req.Text, voice/lang → TTSConfig.Params,
|
||||
// ModelName from req.Engine). When the model provider is
|
||||
// unconfigured (nil dispatcher) the helper returns nil, which
|
||||
// reverts the audio package to its default stub.
|
||||
func configureTTSSynthesizer(modelProviderService *service.ModelProviderService) {
|
||||
if modelProviderService == nil {
|
||||
common.Info("agent: model provider service not initialised; TTS in no-op echo mode")
|
||||
audio.SetModelProviderSynthesizer(nil)
|
||||
return
|
||||
}
|
||||
audio.SetModelProviderSynthesizer(audio.NewTTSDispatchFunc(modelProviderService))
|
||||
common.Info("agent: TTS model-provider dispatch installed (audio.Synthesize → ModelProviderService.AudioSpeech)")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user