2026-05-25 14:00:08 +08:00
|
|
|
//
|
|
|
|
|
// 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.
|
|
|
|
|
//
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-04-28 12:12:58 +08:00
|
|
|
"errors"
|
2026-03-04 19:17:16 +08:00
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
2026-07-02 21:21:10 +08:00
|
|
|
"ragflow/internal/admin"
|
2026-06-17 13:24:03 +08:00
|
|
|
"ragflow/internal/agent/audio"
|
|
|
|
|
"ragflow/internal/agent/canvas"
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
"ragflow/internal/agent/runtime"
|
2026-06-26 22:55:49 +08:00
|
|
|
agenttool "ragflow/internal/agent/tool"
|
2026-03-04 19:17:16 +08:00
|
|
|
"ragflow/internal/handler"
|
2026-07-02 21:21:10 +08:00
|
|
|
"ragflow/internal/ingestion"
|
2026-07-02 09:45:01 +08:00
|
|
|
"ragflow/internal/mcp"
|
2026-03-04 19:17:16 +08:00
|
|
|
"ragflow/internal/router"
|
2026-07-02 21:21:10 +08:00
|
|
|
"ragflow/internal/server/local"
|
2026-03-04 19:17:16 +08:00
|
|
|
"ragflow/internal/service"
|
2026-06-12 14:56:44 +08:00
|
|
|
"ragflow/internal/service/chunk"
|
2026-03-04 19:17:16 +08:00
|
|
|
"ragflow/internal/service/nlp"
|
2026-07-02 21:21:10 +08:00
|
|
|
"ragflow/internal/storage"
|
2026-07-03 11:14:02 +08:00
|
|
|
"ragflow/internal/syncer"
|
2026-03-04 19:17:16 +08:00
|
|
|
"ragflow/internal/tokenizer"
|
2026-07-02 21:21:10 +08:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"syscall"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
|
|
|
|
_ "ragflow/internal/agent/component"
|
|
|
|
|
"ragflow/internal/common"
|
|
|
|
|
"ragflow/internal/dao"
|
|
|
|
|
"ragflow/internal/engine"
|
|
|
|
|
"ragflow/internal/engine/redis"
|
2026-07-05 20:43:52 +08:00
|
|
|
_ "ragflow/internal/ingestion/wire" // single owner for ingestion-component registration (File / Parser / Tokenizer / Extractor + 4 Chunker variants)
|
2026-07-02 21:21:10 +08:00
|
|
|
"ragflow/internal/server"
|
|
|
|
|
"ragflow/internal/utility"
|
2026-03-04 19:17:16 +08:00
|
|
|
)
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
type serverArgs struct {
|
2026-07-03 11:14:02 +08:00
|
|
|
mode *string // admin | api | ingestor | syncer
|
2026-07-02 21:21:10 +08:00
|
|
|
helpFlag bool
|
|
|
|
|
versionFlag bool
|
|
|
|
|
debugLog bool
|
|
|
|
|
configPath *string // Used by admin, api; user defined config path
|
|
|
|
|
initSuperUser bool // Used by admin;
|
|
|
|
|
port *int // Used by admin, api
|
2026-07-03 11:14:02 +08:00
|
|
|
adminHost *string // Used by api, ingestor, syncer for heartbeat
|
|
|
|
|
adminPort *int // Used by api, ingestor, syncer for heartbeat, "ip:port"
|
2026-07-02 21:21:10 +08:00
|
|
|
name *string // server name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseArgs() (*serverArgs, error) {
|
|
|
|
|
args := &serverArgs{}
|
|
|
|
|
|
|
|
|
|
var serverMode string
|
|
|
|
|
var configPath string
|
|
|
|
|
for i := 1; i < len(os.Args); i++ {
|
|
|
|
|
arg := os.Args[i]
|
|
|
|
|
switch arg {
|
|
|
|
|
case "--admin":
|
|
|
|
|
serverMode = "admin"
|
|
|
|
|
args.mode = &serverMode
|
|
|
|
|
case "--ingestor":
|
|
|
|
|
serverMode = "ingestor"
|
|
|
|
|
args.mode = &serverMode
|
|
|
|
|
case "--api":
|
|
|
|
|
serverMode = "api"
|
|
|
|
|
args.mode = &serverMode
|
2026-07-03 11:14:02 +08:00
|
|
|
case "--syncer":
|
|
|
|
|
serverMode = "syncer"
|
|
|
|
|
args.mode = &serverMode
|
2026-07-02 21:21:10 +08:00
|
|
|
case "-h", "--help":
|
|
|
|
|
args.helpFlag = true
|
|
|
|
|
case "-v", "--version":
|
|
|
|
|
args.versionFlag = true
|
|
|
|
|
case "--debug":
|
|
|
|
|
args.debugLog = true
|
|
|
|
|
case "-f", "--config":
|
|
|
|
|
if i+1 >= len(os.Args) {
|
|
|
|
|
return nil, fmt.Errorf("%s requires a value", arg)
|
|
|
|
|
}
|
|
|
|
|
i++
|
|
|
|
|
configPath = os.Args[i]
|
|
|
|
|
args.configPath = &configPath
|
|
|
|
|
case "--init-superuser":
|
|
|
|
|
args.initSuperUser = true
|
|
|
|
|
case "-p", "--port":
|
|
|
|
|
if i+1 >= len(os.Args) {
|
|
|
|
|
return nil, errors.New("--port requires a value")
|
|
|
|
|
}
|
|
|
|
|
i++
|
|
|
|
|
port, convErr := strconv.Atoi(os.Args[i])
|
|
|
|
|
if convErr != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid port: %w", convErr)
|
|
|
|
|
}
|
|
|
|
|
args.port = &port
|
|
|
|
|
if port <= 0 || port > 65535 {
|
|
|
|
|
return nil, fmt.Errorf("invalid port: %d", port)
|
|
|
|
|
}
|
|
|
|
|
case "--admin-host":
|
|
|
|
|
if i+1 >= len(os.Args) {
|
|
|
|
|
return nil, errors.New("--admin-host requires a value")
|
|
|
|
|
}
|
|
|
|
|
i++
|
|
|
|
|
parts := strings.SplitN(os.Args[i], ":", 2)
|
|
|
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
|
|
|
return nil, errors.New("--admin-host must be in the form 'ip:port'")
|
|
|
|
|
}
|
|
|
|
|
ip, portStr := parts[0], parts[1]
|
|
|
|
|
port, convErr := strconv.Atoi(portStr)
|
|
|
|
|
if convErr != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to parse admin port: %w", convErr)
|
|
|
|
|
}
|
|
|
|
|
args.adminHost = &ip
|
|
|
|
|
args.adminPort = &port
|
|
|
|
|
case "--name":
|
|
|
|
|
if i+1 >= len(os.Args) {
|
|
|
|
|
return nil, errors.New("--name requires a value")
|
|
|
|
|
}
|
|
|
|
|
i++
|
|
|
|
|
args.name = &os.Args[i]
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown parameter: %s", arg)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return args, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func printHelp(args *serverArgs) {
|
|
|
|
|
switch {
|
|
|
|
|
case args.mode == nil:
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: %s --api|--admin|--ingestor [OPTIONS]\n\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, "RAGFlow Server - Open-source RAG engine based on deep document understanding\n\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Mode selection (default: --api):\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --api \tRun as API server\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --admin \tRun as admin server\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --ingestor \tRun as ingestion worker\n\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Common options:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --config string\tPath to configuration file\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -v, --version \tPrint version information and exit\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --debug \tEnable debug-level logging\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -h, --help \tShow this help message and exit\n\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Run '%s --api --help' for API server options.\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Run '%s --admin --help' for admin server options.\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Run '%s --ingestor --help' for ingester options.\n", os.Args[0])
|
|
|
|
|
case *args.mode == "api":
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: %s --api [OPTIONS]\n\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, "RAGFlow API Server\n\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Options:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --port int \tServer port (overrides config file)\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -f --config string\tPath to configuration file\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -v, --version \tPrint version information and exit\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --debug \tEnable debug-level logging\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -h, --help \tShow this help message and exit\n")
|
|
|
|
|
case *args.mode == "admin":
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: %s --admin [OPTIONS]\n\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, "RAGFlow Admin Server\n\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Options:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -f --config string\t\tPath to configuration file\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --port int \t\t\tServer port (overrides config file)\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --init-superuser\t\t\tInitialize superuser account\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -v, --version \t\t\tPrint version information and exit\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --debug \t\t\tEnable debug-level logging\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -h, --help \t\t\tShow this help message and exit\n")
|
|
|
|
|
case *args.mode == "ingestor":
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: %s --ingestor [OPTIONS]\n\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, "RAGFlow Ingestion Worker - Document ingestion processing\n\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Options:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -f --config string\tPath to config file\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --name string\t\t\tIngestion server name (default: \"default_ingestion\")\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --admin-host string\tAdmin server host:port (overrides config file)\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -v, --version \t\tPrint version information and exit\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --debug \t\tEnable debug-level logging\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -h, --help \t\tShow this help message and exit\n")
|
2026-07-03 11:14:02 +08:00
|
|
|
case *args.mode == "syncer":
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: %s --syncer [OPTIONS]\n\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, "RAGFlow Sync Service - Sync files from source to RAGFlow\n\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Options:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -f --config string\tPath to config file\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --name string\t\t\tSync service server name (default: \"default_syncer\")\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --admin-host string\tAdmin server host:port (overrides config file)\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -v, --version \t\tPrint version information and exit\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " --debug \t\tEnable debug-level logging\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -h, --help \t\tShow this help message and exit\n")
|
2026-07-02 21:21:10 +08:00
|
|
|
}
|
2026-03-12 09:50:57 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
func main() {
|
2026-07-02 21:21:10 +08:00
|
|
|
arguments, err := parseArgs()
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("Failed to parse arguments: %v\n", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if arguments.helpFlag || arguments.mode == nil {
|
|
|
|
|
printHelp(arguments)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if arguments.versionFlag {
|
2026-06-16 20:27:37 +08:00
|
|
|
fmt.Printf("RAGFlow version: %s\n", utility.GetRAGFlowVersion())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
// Initialize local variables (runtime variables from Redis)
|
|
|
|
|
err = server.InitLocalVariables()
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Failed to start %s server: %v\n", *arguments.mode, err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Temporary logger initialization
|
|
|
|
|
var logFile string
|
|
|
|
|
var serverName string
|
|
|
|
|
if arguments.name != nil {
|
|
|
|
|
serverName = *arguments.name
|
|
|
|
|
} else {
|
|
|
|
|
serverName = fmt.Sprintf("%s_server", *arguments.mode)
|
|
|
|
|
}
|
|
|
|
|
logFile = fmt.Sprintf("%s.log", serverName)
|
|
|
|
|
|
|
|
|
|
logLevel := "info"
|
|
|
|
|
if arguments.debugLog {
|
|
|
|
|
logLevel = "debug"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = common.Init(logLevel, common.FileOutput{Path: logFile}); err != nil {
|
|
|
|
|
panic("failed to initialize logger: " + err.Error())
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize configuration
|
2026-07-02 21:21:10 +08:00
|
|
|
var configPath string
|
|
|
|
|
if arguments.configPath != nil {
|
|
|
|
|
configPath = *arguments.configPath
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
if err = server.Init(configPath); err != nil {
|
|
|
|
|
common.Error("Failed to initialize configuration", err)
|
|
|
|
|
os.Exit(1)
|
2026-03-12 09:50:57 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
config := server.GetConfig()
|
|
|
|
|
|
|
|
|
|
// override default port if provided
|
|
|
|
|
switch *arguments.mode {
|
|
|
|
|
case "api":
|
|
|
|
|
port := config.Server.Port
|
|
|
|
|
if arguments.port != nil {
|
|
|
|
|
port = *arguments.port
|
|
|
|
|
config.Server.Port = port
|
|
|
|
|
}
|
|
|
|
|
if arguments.name == nil {
|
|
|
|
|
serverName = fmt.Sprintf("api_server_%d", port)
|
|
|
|
|
}
|
|
|
|
|
case "admin":
|
|
|
|
|
port := config.Admin.Port
|
|
|
|
|
if arguments.port != nil {
|
|
|
|
|
port = *arguments.port
|
|
|
|
|
config.Admin.Port = port
|
|
|
|
|
}
|
|
|
|
|
if arguments.name == nil {
|
|
|
|
|
serverName = fmt.Sprintf("admin_server_%d", port)
|
|
|
|
|
}
|
|
|
|
|
case "ingestor":
|
|
|
|
|
if serverName == "" {
|
|
|
|
|
uuid := common.GenerateUUID()
|
|
|
|
|
serverName = fmt.Sprintf("ingestor_server_%s", uuid)
|
|
|
|
|
}
|
2026-07-03 11:14:02 +08:00
|
|
|
case "syncer":
|
|
|
|
|
if serverName == "" {
|
|
|
|
|
uuid := common.GenerateUUID()
|
|
|
|
|
serverName = fmt.Sprintf("syncer_server_%s", uuid)
|
|
|
|
|
}
|
2026-07-02 21:21:10 +08:00
|
|
|
default:
|
|
|
|
|
common.Error("invalid server mode", errors.New(*arguments.mode))
|
|
|
|
|
os.Exit(1)
|
2026-04-28 12:12:58 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
// set server name and log file path
|
|
|
|
|
server.SetServerName(serverName)
|
|
|
|
|
logFile = fmt.Sprintf("%s.log", serverName)
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
// Reinitialize logger with configured level if different
|
2026-07-02 21:21:10 +08:00
|
|
|
logLevel = config.Log.Level
|
|
|
|
|
if logLevel == "" {
|
|
|
|
|
logLevel = "info"
|
2026-06-08 11:49:37 +08:00
|
|
|
}
|
2026-06-16 20:27:37 +08:00
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
if arguments.debugLog {
|
|
|
|
|
logLevel = "debug"
|
2026-06-16 20:27:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
config.Log.Level = logLevel
|
|
|
|
|
|
2026-06-23 16:21:46 +08:00
|
|
|
fileOut := common.FileOutput{
|
2026-07-02 21:21:10 +08:00
|
|
|
Path: logFile,
|
2026-06-23 16:21:46 +08:00
|
|
|
MaxSize: config.Log.MaxSize,
|
|
|
|
|
MaxBackups: config.Log.MaxBackups,
|
|
|
|
|
MaxAge: config.Log.MaxAge,
|
|
|
|
|
Compress: common.ResolveCompress(config.Log.Compress),
|
|
|
|
|
}
|
|
|
|
|
if config.Log.Path != "" {
|
|
|
|
|
fileOut.Path = config.Log.Path
|
|
|
|
|
}
|
2026-07-02 21:21:10 +08:00
|
|
|
if err = common.Init(logLevel, fileOut); err != nil {
|
|
|
|
|
common.Error("Failed to reinitialize logger with configured level", err)
|
2026-04-08 19:32:53 +08:00
|
|
|
}
|
2026-03-04 19:17:16 +08:00
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
server.SetLogger(common.Logger)
|
2026-03-04 19:17:16 +08:00
|
|
|
|
|
|
|
|
// Print all configuration settings
|
2026-07-02 21:21:10 +08:00
|
|
|
common.Info(fmt.Sprintf("Starting %s server: %s, mode: %s", *arguments.mode, serverName, config.Server.Mode))
|
2026-03-04 19:17:16 +08:00
|
|
|
server.PrintAll()
|
|
|
|
|
|
|
|
|
|
// Initialize database
|
2026-07-02 21:21:10 +08:00
|
|
|
if err = dao.InitDB(); err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Fatal("Failed to initialize database", zap.Error(err))
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize doc engine
|
2026-07-02 21:21:10 +08:00
|
|
|
if err = engine.Init(&config.DocEngine); err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Fatal("Failed to initialize doc engine", zap.Error(err))
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
defer engine.Close()
|
|
|
|
|
|
|
|
|
|
// Initialize Redis cache
|
2026-07-02 21:21:10 +08:00
|
|
|
if err = redis.Init(&config.Redis); err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Fatal("Failed to initialize Redis", zap.Error(err))
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
2026-06-15 14:44:16 +08:00
|
|
|
defer redis.Close()
|
2026-03-04 19:17:16 +08:00
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
if err = storage.InitStorageFactory(); err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Fatal("Failed to initialize storage factory", zap.Error(err))
|
2026-03-20 13:15:41 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
if err = engine.InitMessageQueueEngine(config.TaskExecutor.MessageQueueType); err != nil {
|
|
|
|
|
common.Fatal("Failed to initialize message queue engine", zap.Error(err))
|
2026-06-12 14:56:44 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
// Initialize server variables (runtime variables that can change during operation)
|
|
|
|
|
// This must be done after Cache is initialized
|
2026-07-02 21:21:10 +08:00
|
|
|
if err = server.InitVariables(redis.Get()); err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Warn("Failed to initialize server variables from Redis, using defaults", zap.String("error", err.Error()))
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
if arguments.name == nil {
|
|
|
|
|
arguments.name = &serverName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch *arguments.mode {
|
|
|
|
|
case "api":
|
|
|
|
|
if err = runAPI(arguments); err != nil {
|
|
|
|
|
fmt.Printf("Failed to start API server: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
case "admin":
|
|
|
|
|
if err = runAdmin(arguments); err != nil {
|
|
|
|
|
fmt.Printf("Failed to start admin server: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
case "ingestor":
|
|
|
|
|
if err = runIngestor(arguments); err != nil {
|
|
|
|
|
fmt.Printf("Failed to start ingestion worker: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
2026-07-03 11:14:02 +08:00
|
|
|
case "syncer":
|
|
|
|
|
if err = runSyncer(arguments); err != nil {
|
|
|
|
|
fmt.Printf("Failed to start syncer: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
2026-07-02 21:21:10 +08:00
|
|
|
default:
|
|
|
|
|
fmt.Printf("Invalid server mode: %s\n", *arguments.mode)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runAdmin(args *serverArgs) error {
|
|
|
|
|
adminService := admin.NewService()
|
|
|
|
|
adminHandler := admin.NewHandler(adminService)
|
|
|
|
|
|
|
|
|
|
if args.initSuperUser {
|
|
|
|
|
// Initialize default admin user
|
|
|
|
|
if err := adminService.InitDefaultAdmin(); err != nil {
|
|
|
|
|
common.Error("Failed to initialize default admin user", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize router
|
|
|
|
|
r := admin.NewRouter(adminHandler)
|
|
|
|
|
|
|
|
|
|
// Create Gin engine
|
|
|
|
|
ginEngine := gin.New()
|
|
|
|
|
|
|
|
|
|
// Middleware
|
|
|
|
|
ginEngine.Use(common.GinLogger())
|
|
|
|
|
ginEngine.Use(gin.Recovery())
|
|
|
|
|
|
|
|
|
|
// Setup routes
|
|
|
|
|
r.Setup(ginEngine)
|
|
|
|
|
|
|
|
|
|
// Create HTTP server
|
|
|
|
|
config := server.GetConfig()
|
|
|
|
|
addr := fmt.Sprintf(":%d", config.Admin.Port)
|
|
|
|
|
srv := &http.Server{
|
|
|
|
|
Addr: addr,
|
|
|
|
|
Handler: ginEngine,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Print RAGFlow Admin logo
|
|
|
|
|
common.Info("" +
|
|
|
|
|
"\n ____ ___ ______________ ___ __ _ \n" +
|
|
|
|
|
" / __ \\/ | / ____/ ____/ /___ _ __ / | ____/ /___ ___ (_)___ \n" +
|
|
|
|
|
" / /_/ / /| |/ / __/ /_ / / __ \\ | /| / / / /| |/ __ / __ `__ \\/ / __ \\ \n" +
|
|
|
|
|
" / _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / ___ / /_/ / / / / / / / / / /\n" +
|
|
|
|
|
" /_/ |_/_/ |_\\____/_/ /_/\\____/|__/|__/ /_/ |_\\__,_/_/ /_/ /_/_/_/ /_/ \n")
|
|
|
|
|
|
|
|
|
|
// Print RAGFlow version
|
|
|
|
|
common.Info(fmt.Sprintf("RAGFlow admin version: %s", utility.GetRAGFlowVersion()))
|
|
|
|
|
|
|
|
|
|
// Start HTTP server in a goroutine
|
|
|
|
|
go func() {
|
|
|
|
|
common.Info(fmt.Sprintf("Starting RAGFlow admin HTTP server on port: %d", config.Admin.Port))
|
|
|
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
|
|
|
common.Fatal("Failed to start server", zap.Error(err))
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Wait for interrupt signal to gracefully shutdown
|
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR2)
|
|
|
|
|
sig := <-quit
|
|
|
|
|
|
|
|
|
|
common.Info("Received signal", zap.String("signal", sig.String()))
|
|
|
|
|
common.Info("Shutting down RAGFlow HTTP server...")
|
|
|
|
|
|
|
|
|
|
// Create context with timeout for graceful shutdown
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
// Shutdown HTTP server
|
|
|
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
|
|
|
common.Fatal("Server forced to shutdown", zap.Error(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
common.Info("Admin HTTP server exited")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runIngestor(args *serverArgs) error {
|
|
|
|
|
|
|
|
|
|
ingestor := ingestion.NewIngestor(*args.name, 2, []string{"pdf", "docx", "txt"})
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
err := ingestor.Start()
|
|
|
|
|
if err != nil {
|
|
|
|
|
common.Error("Failed to initialize ingestor", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR2)
|
|
|
|
|
|
|
|
|
|
common.Info("\n ____ __ _\n" +
|
|
|
|
|
" / _/___ ____ ____ _____/ /_(_)___ ____ ________ ______ _____ _____\n" +
|
|
|
|
|
" / // __ \\/ __ `/ _ \\/ ___/ __/ / __ \\/ __ \\ / ___/ _ \\/ ___/ | / / _ \\/ ___/\n" +
|
|
|
|
|
" _/ // / / / /_/ / __(__ ) /_/ / /_/ / / / / (__ ) __/ / | |/ / __/ /\n" +
|
|
|
|
|
"/___/_/ /_/\\__, /\\___/____/\\__/_/\\____/_/ /_/ /____/\\___/_/ |___/\\___/_/\n" +
|
|
|
|
|
" /____/\n")
|
|
|
|
|
|
|
|
|
|
// Print RAGFlow version
|
|
|
|
|
common.Info(fmt.Sprintf("RAGFlow ingestion service version: %s", utility.GetRAGFlowVersion()))
|
|
|
|
|
|
|
|
|
|
// Get local IP address for heartbeat reporting
|
|
|
|
|
localIP, err := utility.GetLocalIP()
|
|
|
|
|
if err != nil {
|
|
|
|
|
common.Fatal("fail to get local ip address")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize and start heartbeat reporter to admin server
|
|
|
|
|
service.AdminServiceClient = service.NewAdminClient(
|
|
|
|
|
common.Logger,
|
|
|
|
|
common.ServerTypeIngestion,
|
|
|
|
|
fmt.Sprintf("ingestor-%s", ingestor.ID()),
|
|
|
|
|
localIP,
|
|
|
|
|
-1,
|
|
|
|
|
)
|
|
|
|
|
if err = service.AdminServiceClient.InitHTTPClient(); err != nil {
|
|
|
|
|
common.Warn("Failed to initialize heartbeat service", zap.Error(err))
|
|
|
|
|
} else {
|
|
|
|
|
// Start heartbeat reporter with 30 seconds interval
|
|
|
|
|
heartbeatReporter := utility.NewScheduledTask("Heartbeat reporter", 3*time.Second, func() {
|
|
|
|
|
if err = service.AdminServiceClient.SendHeartbeat(); err == nil {
|
|
|
|
|
local.SetAdminStatus(0, "")
|
|
|
|
|
} else {
|
|
|
|
|
local.SetAdminStatus(1, err.Error())
|
|
|
|
|
//logger.Warn(fmt.Sprintf(err.Error()))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
heartbeatReporter.Start()
|
|
|
|
|
defer heartbeatReporter.Stop()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait for either an OS signal or a shutdown command from the admin
|
|
|
|
|
select {
|
|
|
|
|
case sig := <-quit:
|
|
|
|
|
common.Info("Received signal", zap.String("signal", sig.String()))
|
|
|
|
|
common.Info(fmt.Sprintf("Shutting down RAGFlow ingestor %s ...", *args.name))
|
|
|
|
|
case <-ingestor.ShutdownCh:
|
|
|
|
|
common.Info(fmt.Sprintf("Received shutdown command from admin, stopping ingestor %s ...", *args.name))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create context with timeout for graceful shutdown
|
|
|
|
|
_, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
ingestor.Stop()
|
|
|
|
|
|
|
|
|
|
common.Info(fmt.Sprintf("Ingestor %s shutdown complete", *args.name))
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 11:14:02 +08:00
|
|
|
func runSyncer(args *serverArgs) error {
|
|
|
|
|
config := server.GetConfig()
|
|
|
|
|
fileSyncer := syncer.NewSyncer(config.FileSyncer.MaxConcurrentSyncs, time.Duration(config.FileSyncer.SyncInterval)*time.Second)
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
err := fileSyncer.Start()
|
|
|
|
|
if err != nil {
|
|
|
|
|
common.Error("Failed to initialize file syncer", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR2)
|
|
|
|
|
|
|
|
|
|
common.Info("\n _______ __ _____\n" +
|
|
|
|
|
" / ____(_) /__ / ___/__ ______ ________ _____\n" +
|
|
|
|
|
" / /_ / / / _ \\ \\__ \\/ / / / __ \\/ ___/ _ \\/ ___/\n" +
|
|
|
|
|
" / __/ / / / __/ ___/ / /_/ / / / / /__/ __/ /\n" +
|
|
|
|
|
" /_/ /_/_/\\___/ /____/\\__, /_/ /_/\\___/\\___/_/\n" +
|
|
|
|
|
" /____/ \n")
|
|
|
|
|
|
|
|
|
|
// Print RAGFlow version
|
|
|
|
|
common.Info(fmt.Sprintf("RAGFlow file syncer service version: %s", utility.GetRAGFlowVersion()))
|
|
|
|
|
|
|
|
|
|
// Get local IP address for heartbeat reporting
|
|
|
|
|
localIP, err := utility.GetLocalIP()
|
|
|
|
|
if err != nil {
|
|
|
|
|
common.Fatal("fail to get local ip address")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize and start heartbeat reporter to admin server
|
|
|
|
|
service.AdminServiceClient = service.NewAdminClient(
|
|
|
|
|
common.Logger,
|
|
|
|
|
common.ServerTypeFileSyncer,
|
|
|
|
|
fmt.Sprintf("syncer-%s", fileSyncer.ID()),
|
|
|
|
|
localIP,
|
|
|
|
|
-1,
|
|
|
|
|
)
|
|
|
|
|
if err = service.AdminServiceClient.InitHTTPClient(); err != nil {
|
|
|
|
|
common.Warn("Failed to initialize heartbeat service", zap.Error(err))
|
|
|
|
|
} else {
|
|
|
|
|
// Start heartbeat reporter with 30 seconds interval
|
|
|
|
|
heartbeatReporter := utility.NewScheduledTask("Heartbeat reporter", 3*time.Second, func() {
|
|
|
|
|
if err = service.AdminServiceClient.SendHeartbeat(); err == nil {
|
|
|
|
|
local.SetAdminStatus(0, "")
|
|
|
|
|
} else {
|
|
|
|
|
local.SetAdminStatus(1, err.Error())
|
|
|
|
|
//logger.Warn(fmt.Sprintf(err.Error()))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
heartbeatReporter.Start()
|
|
|
|
|
defer heartbeatReporter.Stop()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait for either an OS signal or a shutdown command from the admin
|
|
|
|
|
select {
|
|
|
|
|
case sig := <-quit:
|
|
|
|
|
common.Info("Received signal", zap.String("signal", sig.String()))
|
|
|
|
|
common.Info(fmt.Sprintf("Shutting down RAGFlow file syncer %s ...", *args.name))
|
|
|
|
|
case <-fileSyncer.ShutdownCh:
|
|
|
|
|
common.Info(fmt.Sprintf("Received shutdown command from admin, stopping file syncer %s ...", *args.name))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create context with timeout for graceful shutdown
|
|
|
|
|
_, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
fileSyncer.Stop()
|
|
|
|
|
|
|
|
|
|
common.Info(fmt.Sprintf("File syncer %s shutdown complete", *args.name))
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
func runAPI(args *serverArgs) error {
|
2026-03-12 20:02:50 +08:00
|
|
|
// Initialize admin status (default: unavailable=1)
|
|
|
|
|
local.InitAdminStatus(1, "admin server not connected")
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
// Initialize tokenizer (rag_analyzer)
|
2026-06-03 20:55:53 +08:00
|
|
|
dictPath := os.Getenv("RAGFLOW_DICT_PATH")
|
|
|
|
|
if dictPath == "" {
|
|
|
|
|
dictPath = "/usr/share/infinity/resource"
|
|
|
|
|
}
|
2026-03-04 19:17:16 +08:00
|
|
|
tokenizerCfg := &tokenizer.PoolConfig{
|
2026-06-03 20:55:53 +08:00
|
|
|
DictPath: dictPath,
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
if err := tokenizer.Init(tokenizerCfg); err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Fatal("Failed to initialize tokenizer", zap.Error(err))
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
defer tokenizer.Close()
|
|
|
|
|
|
|
|
|
|
// Initialize global QueryBuilder using tokenizer's DictPath
|
|
|
|
|
// This ensures the Synonym uses the same wordnet directory as tokenizer
|
|
|
|
|
if err := nlp.InitQueryBuilderFromTokenizer(tokenizerCfg.DictPath); err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Fatal("Failed to initialize query builder", zap.Error(err))
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:21:10 +08:00
|
|
|
config := server.GetConfig()
|
2026-03-09 17:48:29 +08:00
|
|
|
startServer(config)
|
|
|
|
|
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Info("Server exited")
|
2026-07-02 21:21:10 +08:00
|
|
|
|
|
|
|
|
return nil
|
2026-03-09 17:48:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func startServer(config *server.Config) {
|
|
|
|
|
|
|
|
|
|
// Set Gin mode
|
|
|
|
|
if config.Server.Mode == "release" {
|
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
|
|
|
|
} else {
|
|
|
|
|
gin.SetMode(gin.DebugMode)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
// Initialize service layer
|
|
|
|
|
userService := service.NewUserService()
|
|
|
|
|
documentService := service.NewDocumentService()
|
2026-05-15 14:00:45 +08:00
|
|
|
datasetsService := service.NewDatasetService()
|
2026-05-20 20:32:06 +08:00
|
|
|
metadataService := service.NewMetadataService()
|
2026-06-09 22:48:50 +08:00
|
|
|
chunkService := chunk.NewChunkService()
|
2026-03-04 19:17:16 +08:00
|
|
|
llmService := service.NewLLMService()
|
|
|
|
|
tenantService := service.NewTenantService()
|
|
|
|
|
chatService := service.NewChatService()
|
2026-06-22 18:16:15 +08:00
|
|
|
chatChannelService := service.NewChatChannelService()
|
2026-06-25 19:25:55 +08:00
|
|
|
langfuseService := service.NewLangfuseService()
|
2026-03-04 19:17:16 +08:00
|
|
|
chatSessionService := service.NewChatSessionService()
|
2026-06-18 18:07:27 +08:00
|
|
|
openaiChatService := service.NewOpenAIChatService()
|
2026-03-04 19:17:16 +08:00
|
|
|
systemService := service.NewSystemService()
|
|
|
|
|
connectorService := service.NewConnectorService()
|
|
|
|
|
searchService := service.NewSearchService()
|
2026-06-29 20:07:12 +08:00
|
|
|
searchService.SetTenantService(tenantService)
|
2026-03-04 19:17:16 +08:00
|
|
|
fileService := service.NewFileService()
|
2026-03-27 09:49:50 +08:00
|
|
|
memoryService := service.NewMemoryService()
|
2026-05-27 22:43:21 -10:00
|
|
|
mcpService := service.NewMCPService()
|
2026-04-02 20:20:35 +08:00
|
|
|
modelProviderService := service.NewModelProviderService()
|
2026-03-04 19:17:16 +08:00
|
|
|
|
2026-04-30 12:36:03 +08:00
|
|
|
// Initialize doc engine for skill search
|
|
|
|
|
docEngine := engine.Get()
|
2026-06-26 22:55:49 +08:00
|
|
|
documentDAO := dao.NewDocumentDAO()
|
|
|
|
|
agenttool.SetRetrievalService(agenttool.NewNLPRetrievalAdapterFromDeps(docEngine, documentDAO))
|
|
|
|
|
common.Info("agent: retrieval service adapter installed")
|
2026-04-30 12:36:03 +08:00
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
// Initialize handler layer
|
2026-03-11 11:23:13 +08:00
|
|
|
authHandler := handler.NewAuthHandler()
|
2026-03-04 19:17:16 +08:00
|
|
|
userHandler := handler.NewUserHandler(userService)
|
2026-07-03 17:00:43 +08:00
|
|
|
tenantHandler := handler.NewTenantHandler(tenantService, userService, datasetsService)
|
2026-05-15 14:00:45 +08:00
|
|
|
documentHandler := handler.NewDocumentHandler(documentService, datasetsService)
|
2026-05-20 20:32:06 +08:00
|
|
|
datasetsHandler := handler.NewDatasetsHandler(datasetsService, metadataService)
|
2026-03-04 19:17:16 +08:00
|
|
|
systemHandler := handler.NewSystemHandler(systemService)
|
|
|
|
|
chunkHandler := handler.NewChunkHandler(chunkService, userService)
|
|
|
|
|
llmHandler := handler.NewLLMHandler(llmService, userService)
|
|
|
|
|
chatHandler := handler.NewChatHandler(chatService, userService)
|
2026-06-22 18:16:15 +08:00
|
|
|
chatChannelHandler := handler.NewChatChannelHandler(chatChannelService)
|
2026-06-25 19:25:55 +08:00
|
|
|
langfuseHandler := handler.NewLangfuseHandler(langfuseService)
|
2026-03-04 19:17:16 +08:00
|
|
|
chatSessionHandler := handler.NewChatSessionHandler(chatSessionService, userService)
|
2026-06-18 18:07:27 +08:00
|
|
|
openaiChatHandler := handler.NewOpenAIChatHandler(openaiChatService)
|
2026-03-04 19:17:16 +08:00
|
|
|
connectorHandler := handler.NewConnectorHandler(connectorService, userService)
|
|
|
|
|
searchHandler := handler.NewSearchHandler(searchService, userService)
|
|
|
|
|
fileHandler := handler.NewFileHandler(fileService, userService)
|
2026-03-27 09:49:50 +08:00
|
|
|
memoryHandler := handler.NewMemoryHandler(memoryService)
|
2026-05-27 22:43:21 -10:00
|
|
|
mcpHandler := handler.NewMCPHandler(mcpService)
|
2026-07-02 09:45:01 +08:00
|
|
|
|
|
|
|
|
// MCP server endpoint — exposes RAGFlow capabilities as MCP tools
|
|
|
|
|
// (ragflow_retrieval, ragflow_list_datasets, ragflow_list_chats) to
|
|
|
|
|
// external AI clients via JSON-RPC over HTTP.
|
|
|
|
|
mcpServerHandler := handler.NewMCPServerHandler(
|
|
|
|
|
func(userID string, page, pageSize int, orderby string, desc bool) ([]map[string]interface{}, int64, error) {
|
|
|
|
|
return handler.MCPListDatasets(datasetsService, userID, page, pageSize, orderby, desc)
|
|
|
|
|
},
|
|
|
|
|
func(userID string, page, pageSize int, orderby string, desc bool) ([]map[string]interface{}, int64, error) {
|
|
|
|
|
return handler.MCPListChats(chatService, userID, page, pageSize, orderby, desc)
|
|
|
|
|
},
|
|
|
|
|
func(userID string, req mcp.RetrievalRequest) (string, error) {
|
|
|
|
|
return handler.MCPRetrieval(datasetsService, userID, req)
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-04-30 12:36:03 +08:00
|
|
|
skillSearchHandler := handler.NewSkillSearchHandler(docEngine)
|
2026-04-02 20:20:35 +08:00
|
|
|
providerHandler := handler.NewProviderHandler(userService, modelProviderService)
|
2026-06-17 13:24:03 +08:00
|
|
|
// Install the agent service's Redis-backed run infrastructure
|
|
|
|
|
// (CheckPointStore / StateSerializer / RunTracker). When Redis
|
|
|
|
|
// is unreachable (degraded boot, stand-alone mode, no-redis CI)
|
2026-07-02 21:21:10 +08:00
|
|
|
// the constructors return errors, and we fall through to the
|
2026-06-17 13:24:03 +08:00
|
|
|
// 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()
|
feat[Go]: port agent attachment download, chatbot + agentbot completion/info endpoints from Python (#16405)
## Summary
Ports five Python agent APIs to Go under the v1 Gin router:
- `GET /api/v1/agents/attachments/<attachment_id>/download`
- `POST /api/v1/chatbots/<dialog_id>/completions` (SSE)
- `GET /api/v1/chatbots/<dialog_id>/info`
- `POST /api/v1/agentbots/<agent_id>/completions` (SSE)
- `GET /api/v1/agentbots/<agent_id>/inputs`
Mirrors the existing Python wire shape (`{code, message,
data:{answer,reference,...}}` per Python `canvas_service.completion`) so
the iframe SDK and existing JS widgets keep working.
## Behavioural parity with Python
| # | Concern | How it's met |
|---|---------|--------------|
| R0 | Bot routes must not require regular user session | Routes mount
on `apiNoAuth` (router.go:198-202), with `BetaAuthMiddleware` only |
| R3 | Two SSE formats in Go drift | F2: `AgentChatCompletions` and
`AgentbotCompletion` share `service.WriteChatbotRunEvent` |
| R7 | `GetBySessionID` returns `(nil, nil)` on miss | Defensive
nil-check before `session.UserID != tenantID` |
| R8 | Begin component name vs ID | `FindBeginComponentID` resolves name
→ ID first, then `ExtractComponentInputForm(dsl, beginID)` |
| R9 | Defensive PromptConfig parsing | `stringFromMap` helper used for
`prologue` and `tavily_api_key` |
| R10 | `BetaAuthMiddleware` Bearer-prefix pre-filter | Removed —
`GetUserByToken` is called unconditionally, falls back to
`GetUserByBetaAPIToken` |
| F8 | Multi-turn chatbot history | `ChatbotCompletion` reads prior
turns from `session.Message`, appends user turn, calls LLM, persists new
pair via new `API4ConversationDAO.Update` |
| F9 | UUID gate stricter than plan | Removed — only `filepath.Base` +
CR/LF/quote header sanitization remains |
| H2 | Defence-in-depth IDOR | `AgentbotCompletion` calls `loadCanvas`
before delegating to `RunAgent` |
| M2 | SSE error leakage | `WriteChatbotFrame` emits generic `"an
internal error occurred"`; real error logged via `common.Error` |
## Verification
```bash
$ go vet ./... # clean (only pre-existing issues)
$ go build ./... # success
$ go test ./internal/handler/ ./internal/service/ ./internal/agent/dsl/ ./internal/common/ ./internal/dao/
ok ragflow/internal/handler 0.617s
ok ragflow/internal/service 1.729s
ok ragflow/internal/agent/dsl 0.008s
ok ragflow/internal/common 0.087s
ok ragflow/internal/dao 0.083s
```
1199 tests pass across 5 packages.
## Known follow-ups (out of scope for this PR)
- **F1**: token-level streaming in `ChatbotCompletion` (currently emits
one frame per turn)
- **F3**: per-route `auth_types` attribute in Go (currently applied via
route group middleware)
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 16:52:21 +08:00
|
|
|
agentService := service.NewAgentServiceWithOptions(
|
2026-06-17 13:24:03 +08:00
|
|
|
agentOpts.checkpointStore,
|
|
|
|
|
agentOpts.stateSerializer,
|
|
|
|
|
agentOpts.runTracker,
|
feat[Go]: port agent attachment download, chatbot + agentbot completion/info endpoints from Python (#16405)
## Summary
Ports five Python agent APIs to Go under the v1 Gin router:
- `GET /api/v1/agents/attachments/<attachment_id>/download`
- `POST /api/v1/chatbots/<dialog_id>/completions` (SSE)
- `GET /api/v1/chatbots/<dialog_id>/info`
- `POST /api/v1/agentbots/<agent_id>/completions` (SSE)
- `GET /api/v1/agentbots/<agent_id>/inputs`
Mirrors the existing Python wire shape (`{code, message,
data:{answer,reference,...}}` per Python `canvas_service.completion`) so
the iframe SDK and existing JS widgets keep working.
## Behavioural parity with Python
| # | Concern | How it's met |
|---|---------|--------------|
| R0 | Bot routes must not require regular user session | Routes mount
on `apiNoAuth` (router.go:198-202), with `BetaAuthMiddleware` only |
| R3 | Two SSE formats in Go drift | F2: `AgentChatCompletions` and
`AgentbotCompletion` share `service.WriteChatbotRunEvent` |
| R7 | `GetBySessionID` returns `(nil, nil)` on miss | Defensive
nil-check before `session.UserID != tenantID` |
| R8 | Begin component name vs ID | `FindBeginComponentID` resolves name
→ ID first, then `ExtractComponentInputForm(dsl, beginID)` |
| R9 | Defensive PromptConfig parsing | `stringFromMap` helper used for
`prologue` and `tavily_api_key` |
| R10 | `BetaAuthMiddleware` Bearer-prefix pre-filter | Removed —
`GetUserByToken` is called unconditionally, falls back to
`GetUserByBetaAPIToken` |
| F8 | Multi-turn chatbot history | `ChatbotCompletion` reads prior
turns from `session.Message`, appends user turn, calls LLM, persists new
pair via new `API4ConversationDAO.Update` |
| F9 | UUID gate stricter than plan | Removed — only `filepath.Base` +
CR/LF/quote header sanitization remains |
| H2 | Defence-in-depth IDOR | `AgentbotCompletion` calls `loadCanvas`
before delegating to `RunAgent` |
| M2 | SSE error leakage | `WriteChatbotFrame` emits generic `"an
internal error occurred"`; real error logged via `common.Error` |
## Verification
```bash
$ go vet ./... # clean (only pre-existing issues)
$ go build ./... # success
$ go test ./internal/handler/ ./internal/service/ ./internal/agent/dsl/ ./internal/common/ ./internal/dao/
ok ragflow/internal/handler 0.617s
ok ragflow/internal/service 1.729s
ok ragflow/internal/agent/dsl 0.008s
ok ragflow/internal/common 0.087s
ok ragflow/internal/dao 0.083s
```
1199 tests pass across 5 packages.
## Known follow-ups (out of scope for this PR)
- **F1**: token-level streaming in `ChatbotCompletion` (currently emits
one frame per turn)
- **F3**: per-route `auth_types` attribute in Go (currently applied via
route group middleware)
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 16:52:21 +08:00
|
|
|
)
|
2026-07-02 21:21:10 +08:00
|
|
|
agentHandler := handler.NewAgentHandler(agentService, fileService)
|
feat[Go]: port agent attachment download, chatbot + agentbot completion/info endpoints from Python (#16405)
## Summary
Ports five Python agent APIs to Go under the v1 Gin router:
- `GET /api/v1/agents/attachments/<attachment_id>/download`
- `POST /api/v1/chatbots/<dialog_id>/completions` (SSE)
- `GET /api/v1/chatbots/<dialog_id>/info`
- `POST /api/v1/agentbots/<agent_id>/completions` (SSE)
- `GET /api/v1/agentbots/<agent_id>/inputs`
Mirrors the existing Python wire shape (`{code, message,
data:{answer,reference,...}}` per Python `canvas_service.completion`) so
the iframe SDK and existing JS widgets keep working.
## Behavioural parity with Python
| # | Concern | How it's met |
|---|---------|--------------|
| R0 | Bot routes must not require regular user session | Routes mount
on `apiNoAuth` (router.go:198-202), with `BetaAuthMiddleware` only |
| R3 | Two SSE formats in Go drift | F2: `AgentChatCompletions` and
`AgentbotCompletion` share `service.WriteChatbotRunEvent` |
| R7 | `GetBySessionID` returns `(nil, nil)` on miss | Defensive
nil-check before `session.UserID != tenantID` |
| R8 | Begin component name vs ID | `FindBeginComponentID` resolves name
→ ID first, then `ExtractComponentInputForm(dsl, beginID)` |
| R9 | Defensive PromptConfig parsing | `stringFromMap` helper used for
`prologue` and `tavily_api_key` |
| R10 | `BetaAuthMiddleware` Bearer-prefix pre-filter | Removed —
`GetUserByToken` is called unconditionally, falls back to
`GetUserByBetaAPIToken` |
| F8 | Multi-turn chatbot history | `ChatbotCompletion` reads prior
turns from `session.Message`, appends user turn, calls LLM, persists new
pair via new `API4ConversationDAO.Update` |
| F9 | UUID gate stricter than plan | Removed — only `filepath.Base` +
CR/LF/quote header sanitization remains |
| H2 | Defence-in-depth IDOR | `AgentbotCompletion` calls `loadCanvas`
before delegating to `RunAgent` |
| M2 | SSE error leakage | `WriteChatbotFrame` emits generic `"an
internal error occurred"`; real error logged via `common.Error` |
## Verification
```bash
$ go vet ./... # clean (only pre-existing issues)
$ go build ./... # success
$ go test ./internal/handler/ ./internal/service/ ./internal/agent/dsl/ ./internal/common/ ./internal/dao/
ok ragflow/internal/handler 0.617s
ok ragflow/internal/service 1.729s
ok ragflow/internal/agent/dsl 0.008s
ok ragflow/internal/common 0.087s
ok ragflow/internal/dao 0.083s
```
1199 tests pass across 5 packages.
## Known follow-ups (out of scope for this PR)
- **F1**: token-level streaming in `ChatbotCompletion` (currently emits
one frame per turn)
- **F3**: per-route `auth_types` attribute in Go (currently applied via
route group middleware)
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 16:52:21 +08:00
|
|
|
|
|
|
|
|
// Public chatbot/agentbot endpoints (api/v1/chatbots/...,
|
|
|
|
|
// api/v1/agentbots/...) and the agent attachment download.
|
|
|
|
|
// BotService delegates the agentbot completion to agentService so
|
|
|
|
|
// both paths share the same canvas runner. Reuse the llmService
|
|
|
|
|
// already constructed above (line 222) — do NOT redeclare with
|
|
|
|
|
// `:=` since the variable is in scope.
|
|
|
|
|
botService := service.NewBotService(agentService, llmService)
|
|
|
|
|
botHandler := handler.NewBotHandler(botService)
|
2026-06-17 13:24:03 +08:00
|
|
|
|
|
|
|
|
// 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)
|
feat: implement POST /api/v1/searchbots/retrieval_test (#15710)
## What problem does this PR solve?
Implements `POST /api/v1/searchbots/retrieval_test` in the Go API
server, aligning with the Python `bot_api.py` counterpart. Also applies
security hardening and consistency fixes discovered during CTO-level
code review:
- **Missing endpoint**: `retrieval_test` was not available in Go,
requiring Python fallback
- **Security**: Both `chunkHandler` and `searchBotHandler` leaked
`err.Error()` to API consumers
- **Python alignment**: Default values, empty question handling, and
`top_k <= 0` validation differed from Python behavior
- **Test gaps**: `chunkHandler.RetrievalTest` had zero unit tests;
several edge cases uncovered
## Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
## Summary
### New Endpoint
- `POST /api/v1/searchbots/retrieval_test` — retrieval test with full
field support (page, size, top_k, use_kg, cross_languages, keyword,
similarity_threshold, vector_similarity_weight)
### New Type
- `common.StringSlice` — JSON type that accepts both `"kb1"` and
`["kb1", "kb2"]`, matching Python API flexibility
### Security
- Both `searchBotHandler` and `chunkHandler` now use `common.Warn()` +
generic error messages instead of leaking `err.Error()` to API consumers
- All error responses include consistent `"data": nil` shape
- `chunkHandler.RetrievalTest` uses interface-based DI (`chunkService`)
to enable testability
### Python Alignment
- Handler-level defaults align with Python `bot_api.py` (page=1,
size=30, top_k=1024, similarity_threshold=0.0,
vector_similarity_weight=0.3)
- `top_k <= 0` validation matching Python behavior
- Empty/whitespace question returns 200 + empty result (matches
`chunk_api.py`)
- `chunkHandler` `Datasets` field uses `common.StringSlice` for
string-or-array flexibility
### Refactoring
- `ChunkServiceIface` → `ChunkRetriever`, `chunkSvcIface` →
`chunkService` (Go-conventional naming)
- Extracted `applyRetrievalDefaults`, `toRetrievalServiceRequest` from
handler body
- Regex moved to package-level var in `parseRelatedQuestions`
- `service.RetrievalTestRequest.Datasets` type changed to
`common.StringSlice`
- `chunkHandler` now uses consumer-side interface for DI
### Tests
- 37 unit tests across both handlers: auth, validation, defaults,
StringSlice edge cases, empty/whitespace KbID, service errors, JSON
format, `top_k <= 0`, field mapping verification
## Files Changed
| File | Change |
|------|--------|
| `cmd/server_main.go` | Wire new handler + chunkService +
difyRetrievalHandler |
| `internal/common/json_types.go` | New StringSlice type |
| `internal/common/json_types_test.go` | StringSlice tests |
| `internal/handler/chunk.go` | Interface-based DI, security, Python
alignment, defaults |
| `internal/handler/chunk_test.go` | New — 9 comprehensive tests |
| `internal/handler/searchbot.go` | New endpoint + refactoring + `top_k
<= 0` validation |
| `internal/handler/searchbot_test.go` | 18 tests covering all edge
cases |
| `internal/router/router.go` | Register new route +
difyRetrievalHandler |
| `internal/service/chunk.go` | Datasets type → StringSlice, Question
binding relaxed |
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:16:56 +08:00
|
|
|
searchBotHandler := handler.NewSearchBotHandler(
|
2026-06-04 19:13:58 +08:00
|
|
|
searchService,
|
|
|
|
|
tenantService,
|
2026-07-01 13:17:16 +08:00
|
|
|
modelProviderService,
|
feat: implement POST /api/v1/searchbots/retrieval_test (#15710)
## What problem does this PR solve?
Implements `POST /api/v1/searchbots/retrieval_test` in the Go API
server, aligning with the Python `bot_api.py` counterpart. Also applies
security hardening and consistency fixes discovered during CTO-level
code review:
- **Missing endpoint**: `retrieval_test` was not available in Go,
requiring Python fallback
- **Security**: Both `chunkHandler` and `searchBotHandler` leaked
`err.Error()` to API consumers
- **Python alignment**: Default values, empty question handling, and
`top_k <= 0` validation differed from Python behavior
- **Test gaps**: `chunkHandler.RetrievalTest` had zero unit tests;
several edge cases uncovered
## Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
## Summary
### New Endpoint
- `POST /api/v1/searchbots/retrieval_test` — retrieval test with full
field support (page, size, top_k, use_kg, cross_languages, keyword,
similarity_threshold, vector_similarity_weight)
### New Type
- `common.StringSlice` — JSON type that accepts both `"kb1"` and
`["kb1", "kb2"]`, matching Python API flexibility
### Security
- Both `searchBotHandler` and `chunkHandler` now use `common.Warn()` +
generic error messages instead of leaking `err.Error()` to API consumers
- All error responses include consistent `"data": nil` shape
- `chunkHandler.RetrievalTest` uses interface-based DI (`chunkService`)
to enable testability
### Python Alignment
- Handler-level defaults align with Python `bot_api.py` (page=1,
size=30, top_k=1024, similarity_threshold=0.0,
vector_similarity_weight=0.3)
- `top_k <= 0` validation matching Python behavior
- Empty/whitespace question returns 200 + empty result (matches
`chunk_api.py`)
- `chunkHandler` `Datasets` field uses `common.StringSlice` for
string-or-array flexibility
### Refactoring
- `ChunkServiceIface` → `ChunkRetriever`, `chunkSvcIface` →
`chunkService` (Go-conventional naming)
- Extracted `applyRetrievalDefaults`, `toRetrievalServiceRequest` from
handler body
- Regex moved to package-level var in `parseRelatedQuestions`
- `service.RetrievalTestRequest.Datasets` type changed to
`common.StringSlice`
- `chunkHandler` now uses consumer-side interface for DI
### Tests
- 37 unit tests across both handlers: auth, validation, defaults,
StringSlice edge cases, empty/whitespace KbID, service errors, JSON
format, `top_k <= 0`, field mapping verification
## Files Changed
| File | Change |
|------|--------|
| `cmd/server_main.go` | Wire new handler + chunkService +
difyRetrievalHandler |
| `internal/common/json_types.go` | New StringSlice type |
| `internal/common/json_types_test.go` | StringSlice tests |
| `internal/handler/chunk.go` | Interface-based DI, security, Python
alignment, defaults |
| `internal/handler/chunk_test.go` | New — 9 comprehensive tests |
| `internal/handler/searchbot.go` | New endpoint + refactoring + `top_k
<= 0` validation |
| `internal/handler/searchbot_test.go` | 18 tests covering all edge
cases |
| `internal/router/router.go` | Register new route +
difyRetrievalHandler |
| `internal/service/chunk.go` | Datasets type → StringSlice, Question
binding relaxed |
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:16:56 +08:00
|
|
|
chunkService,
|
2026-06-04 19:13:58 +08:00
|
|
|
)
|
2026-07-01 13:17:16 +08:00
|
|
|
searchBotHandler.SetStreamLLM(modelProviderService)
|
2026-06-29 20:07:12 +08:00
|
|
|
askService := service.NewAskService(chunkService, nil, 0, 0)
|
|
|
|
|
searchBotHandler.SetAskService(askService)
|
2026-07-01 13:17:16 +08:00
|
|
|
chatHandler.SetMindMapDependencies(searchService, tenantService, modelProviderService, chunkService)
|
|
|
|
|
searchHandler.SetCompletionDependencies(modelProviderService, askService)
|
2026-06-07 20:53:19 -07:00
|
|
|
pluginHandler := handler.NewPluginHandler(service.NewPluginService())
|
2026-06-08 21:38:15 +08:00
|
|
|
modelHandler := handler.NewModelHandler(service.NewModelProviderService())
|
2026-06-15 11:19:56 +08:00
|
|
|
fileCommitHandler := handler.NewFileCommitHandler(service.NewFileCommitService())
|
2026-03-04 19:17:16 +08:00
|
|
|
|
2026-06-05 21:16:25 +08:00
|
|
|
// Dify retrieval handler
|
2026-06-26 22:55:49 +08:00
|
|
|
docDAO := documentDAO
|
2026-06-05 21:16:25 +08:00
|
|
|
retrievalService := nlp.NewRetrievalService(docEngine, docDAO)
|
|
|
|
|
difyRetrievalHandler := handler.NewDifyRetrievalHandler(
|
2026-07-03 17:00:43 +08:00
|
|
|
datasetsService,
|
2026-06-05 21:16:25 +08:00
|
|
|
modelProviderService,
|
|
|
|
|
metadataService,
|
|
|
|
|
retrievalService,
|
|
|
|
|
docDAO,
|
|
|
|
|
docEngine,
|
|
|
|
|
)
|
2026-06-17 13:24:03 +08:00
|
|
|
// 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.
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
var adminRuntimeSelector *runtime.Selector
|
2026-07-02 21:21:10 +08:00
|
|
|
if redisClient := redis.Get(); redisClient != nil {
|
|
|
|
|
if rdb := redisClient.GetClient(); rdb != nil {
|
|
|
|
|
adminRuntimeSelector = runtime.NewSelector(rdb, common.Logger)
|
|
|
|
|
}
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
}
|
|
|
|
|
adminRuntimeHandler := handler.NewAdminRuntimeHandler(adminRuntimeSelector)
|
2026-07-05 20:43:52 +08:00
|
|
|
componentsHandler := handler.NewComponentsHandler(service.NewComponentsService())
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
// Initialize router
|
2026-07-02 21:21:10 +08:00
|
|
|
r := router.NewRouter(authHandler,
|
|
|
|
|
userHandler,
|
|
|
|
|
tenantHandler,
|
|
|
|
|
documentHandler,
|
|
|
|
|
datasetsHandler,
|
|
|
|
|
systemHandler,
|
|
|
|
|
chunkHandler,
|
|
|
|
|
llmHandler,
|
|
|
|
|
chatHandler,
|
|
|
|
|
chatChannelHandler,
|
|
|
|
|
langfuseHandler,
|
|
|
|
|
chatSessionHandler,
|
|
|
|
|
connectorHandler,
|
|
|
|
|
searchHandler,
|
|
|
|
|
fileHandler,
|
|
|
|
|
memoryHandler,
|
|
|
|
|
mcpHandler,
|
|
|
|
|
mcpServerHandler,
|
|
|
|
|
skillSearchHandler,
|
|
|
|
|
providerHandler,
|
|
|
|
|
agentHandler,
|
|
|
|
|
searchBotHandler,
|
|
|
|
|
difyRetrievalHandler,
|
|
|
|
|
pluginHandler,
|
|
|
|
|
modelHandler,
|
|
|
|
|
fileCommitHandler,
|
|
|
|
|
adminRuntimeHandler,
|
|
|
|
|
openaiChatHandler,
|
2026-07-05 20:43:52 +08:00
|
|
|
botHandler,
|
|
|
|
|
componentsHandler)
|
2026-07-02 21:21:10 +08:00
|
|
|
|
|
|
|
|
// Create Gin enginegit diff
|
2026-03-04 19:17:16 +08:00
|
|
|
|
|
|
|
|
ginEngine := gin.New()
|
|
|
|
|
|
|
|
|
|
// Middleware
|
2026-06-23 16:21:46 +08:00
|
|
|
// Note: common.GinLogger() is registered inside router.Setup so the
|
|
|
|
|
// HTTP request log captures every endpoint the router owns (including
|
|
|
|
|
// those registered by Setup itself). Registering it here would run
|
|
|
|
|
// it twice for those endpoints and double every access-log line.
|
2026-03-04 19:17:16 +08:00
|
|
|
ginEngine.Use(gin.Recovery())
|
|
|
|
|
|
|
|
|
|
// Setup routes
|
|
|
|
|
r.Setup(ginEngine)
|
|
|
|
|
|
2026-04-30 12:36:03 +08:00
|
|
|
// Create HTTP server with timeouts to prevent slow clients from blocking shutdown
|
2026-03-09 17:48:29 +08:00
|
|
|
addr := fmt.Sprintf(":%d", config.Server.Port)
|
2026-03-04 19:17:16 +08:00
|
|
|
srv := &http.Server{
|
2026-04-30 12:36:03 +08:00
|
|
|
Addr: addr,
|
|
|
|
|
Handler: ginEngine,
|
|
|
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
|
|
|
ReadTimeout: 60 * time.Second,
|
|
|
|
|
WriteTimeout: 120 * time.Second,
|
|
|
|
|
IdleTimeout: 120 * time.Second,
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start server in a goroutine
|
|
|
|
|
go func() {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Info(
|
2026-03-06 20:05:10 +08:00
|
|
|
"\n ____ ___ ______ ______ __\n" +
|
|
|
|
|
" / __ \\ / | / ____// ____// /____ _ __\n" +
|
|
|
|
|
" / /_/ // /| | / / __ / /_ / // __ \\| | /| / /\n" +
|
|
|
|
|
" / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /\n" +
|
|
|
|
|
" /_/ |_|/_/ |_|\\____//_/ /_/ \\____/ |__/|__/\n",
|
|
|
|
|
)
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Info(fmt.Sprintf("RAGFlow Go Version: %s", utility.GetRAGFlowVersion()))
|
|
|
|
|
common.Info(fmt.Sprintf("Server starting on port: %d", config.Server.Port))
|
2026-04-28 12:12:58 +08:00
|
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Fatal("Failed to start server", zap.Error(err))
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2026-03-09 17:48:29 +08:00
|
|
|
// Get local IP address for heartbeat reporting
|
2026-04-28 12:12:58 +08:00
|
|
|
localIP, err := utility.GetLocalIP()
|
|
|
|
|
if err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Fatal("fail to get local ip address")
|
2026-03-09 17:48:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize and start heartbeat reporter to admin server
|
2026-06-12 14:56:44 +08:00
|
|
|
service.AdminServiceClient = service.NewAdminClient(
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Logger,
|
2026-03-09 17:48:29 +08:00
|
|
|
common.ServerTypeAPI,
|
|
|
|
|
fmt.Sprintf("ragflow-server-%d", config.Server.Port),
|
|
|
|
|
localIP,
|
|
|
|
|
config.Server.Port,
|
|
|
|
|
)
|
2026-06-12 14:56:44 +08:00
|
|
|
if err = service.AdminServiceClient.InitHTTPClient(); err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Warn("Failed to initialize heartbeat service", zap.Error(err))
|
2026-03-09 17:48:29 +08:00
|
|
|
} else {
|
|
|
|
|
// Start heartbeat reporter with 30 seconds interval
|
|
|
|
|
heartbeatReporter := utility.NewScheduledTask("Heartbeat reporter", 3*time.Second, func() {
|
2026-06-12 14:56:44 +08:00
|
|
|
if err = service.AdminServiceClient.SendHeartbeat(); err == nil {
|
2026-03-12 20:02:50 +08:00
|
|
|
local.SetAdminStatus(0, "")
|
|
|
|
|
} else {
|
2026-03-13 14:41:02 +08:00
|
|
|
local.SetAdminStatus(1, err.Error())
|
2026-03-19 10:25:35 +08:00
|
|
|
//logger.Warn(fmt.Sprintf(err.Error()))
|
2026-03-09 17:48:29 +08:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
heartbeatReporter.Start()
|
|
|
|
|
defer heartbeatReporter.Stop()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
// Wait for interrupt signal to gracefully shutdown
|
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR2)
|
|
|
|
|
sig := <-quit
|
|
|
|
|
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Info(fmt.Sprintf("Receives %s signal to shutdown server", strings.ToUpper(sig.String())))
|
|
|
|
|
common.Info("Shutting down server...")
|
2026-03-04 19:17:16 +08:00
|
|
|
|
|
|
|
|
// Create context with timeout for graceful shutdown
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
// Shutdown server
|
2026-04-28 12:12:58 +08:00
|
|
|
if err = srv.Shutdown(ctx); err != nil {
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Fatal("Server forced to shutdown", zap.Error(err))
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-17 13:24:03 +08:00
|
|
|
|
|
|
|
|
// 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
|
2026-07-02 21:21:10 +08:00
|
|
|
// initialized at the top of main; the TTL is a conservative 24h for
|
2026-06-17 13:24:03 +08:00
|
|
|
// 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)")
|
|
|
|
|
}
|