15 KiB
RAGFlow Instructions
Use this file as the local operating guide for the current codebase. Prefer the code and the current AGENTS.md over any older convention or remembered project shape.
Core Stance
- Treat legacy code as liability, not as a compatibility target.
- Prefer deletion over shims, deprecated branches, wrapper APIs, and dual-track migration notes.
- If old and new implementations coexist, converge to one path unless an external contract forces compatibility.
- Remove dead tests, commented-out code, stale docs, and "move later" notes instead of preserving them.
- Reduce public surface area when a helper can be made private or internal.
- Keep refactors centered on the owning abstraction, not on adjacent compatibility layers.
Current stack
- Backend: Go implements the API, admin, ingestor, and syncer modes in
cmd/ragflow_server.go; the CLI runs fromcmd/ragflow-cli.go. The Python API, admin, and workers are still present anddocker/entrypoint.shstill defaults to Python whenAPI_PROXY_SCHEMEis unset. Go-only operation is the migration target, not the current repository state. - Frontend: React + TypeScript + Vite in
web/. When working underweb/, read and followweb/AGENTS.mdfor frontend conventions. - The Go module contains the server, ingestion, parsing, agent runtime, CLI, and supporting services.
- Runtime services commonly include MySQL/PostgreSQL, Kvrocks, NATS JetStream, MinIO, ClickHouse, and Elasticsearch/Infinity/OpenSearch depending on configuration.
Go-only target
- Remove all Python code and Python-specific dependencies, scripts, containers, CI jobs, documentation, SDKs, and tests from the repository. Do not add new Python code while this removal is in progress. Replace Python-based preparation of Go native libraries, DeepDoc models, and the BPE table before removing the existing download scripts.
- Move any still-required behavior and test coverage into the Go backend or the frontend before deleting the old implementation. Do not preserve a Python server, compatibility layer, fallback, or dual-backend path.
- Before deleting
rag/, move the Go DeepDoc.ortmodels andocr.resout ofinternal/rag/res/deepdoc/, then update model discovery, dependency preparation, Docker packaging, and tests together. - Remove the frontend's Go/Python backend variants and update
web/CLAUDE.mdwhen the frontend no longer needs them. Until then, treat its dual-backend instructions as migration-only guidance; new frontend work should target the Go API. - Update Docker's default entrypoint, dependency image, and CI workflows when removing their Python paths. Until then, select Go explicitly with
API_PROXY_SCHEME=gowhere that variable controls startup. - Keep this guide aligned with the checked-in tree. Once the Python directories are deleted, remove their migration inventory below.
Code Layout to Expect
cmd/ragflow_server.go: Go entrypoint for the API, admin, ingestor, and syncer modes (--api,--admin,--ingestor,--syncer).cmd/ragflow-cli.go: Go CLI entrypoint.api/,rag/,deepdoc/, andagent/: legacy Python implementation pending deletion. Do not add features or extend dependencies there.internal/: main Go application code. Important subtrees:internal/agent/: Go agent runtime, canvas execution, components, tool bindings, workflow helpers.internal/admin/: Go admin routes, handlers, and services.internal/binding/cpp/: C++ tokenizer binding built bybuild.shfor the Go server.internal/cli/: CLI parsing, HTTP transport, command execution, response formatting.internal/dao/: Go data-access layer and persistence-facing helpers.internal/deepdoc/: Go DeepDOC integrations, especially native-backed PDF/DOCX parsing.internal/engine/: search/index backends such as Elasticsearch and Infinity.internal/entity/: shared Go entities and model definitions.internal/handler/: HTTP handlers and route-facing request logic.internal/ingestion/: Go ingestion pipeline, canvas adapter, components, wiring, service orchestration.internal/ingestion/component/: stage implementations such as file/parser/chunker/tokenizer/extractor.internal/ingestion/pipeline/: DSL translation, canvas-driven execution, checkpoints, resume/run logic.internal/parser/: parser and chunk libraries used by ingestion and other Go paths.internal/parser/parser/: typed parse-result parsers for markdown/html/pdf/docx/xlsx/text and related families.internal/parser/chunk/: chunk operator library and DSL/typed execution helpers.internal/service/: higher-level business services used by handlers and server flows.internal/storage/: storage backends and in-memory test doubles.internal/router/: HTTP route registration.internal/server/: server bootstrap/config wiring.internal/syncer/: Go file synchronization service and connectors.internal/rag/: Go retrieval and RAG logic.internal/harness/: Go runtime harness and supporting execution code.web/: frontend application.docker/: local and production compose files.sdk/python/: legacy Python client SDK pending deletion.ragflow_deps/download_deps.py: Python helper for Go native libraries, DeepDoc models, and tokenizer assets. Replace it with non-Python preparation before removing Python dependencies.test/: contains Python tests, including tests that import the legacy Python implementation directly. Migrate needed coverage to Go tests or frontend tests, then remove the Python tests; keep only tests for supported code.
Go-Specific Rules
- Treat
internal/ingestion,internal/parser, andinternal/deepdocas actively refactored code. Prefer collapsing duplicate paths over preserving transitional wrappers. - Do not add or preserve deprecated Go APIs just to ease migration inside the repo.
- Remove commented-out Go code instead of leaving recovery notes in place.
- Keep package comments and doc comments aligned with the current runtime path, not with migration history.
GORM query traps
- Never pass a bare string ID as the second argument to
Take/First(e.g.Take(&x, id)). Under GORM v1.25.7 a non-numeric string is treated as a raw SQL condition and concatenated into theWHEREclause:Take(&x, "88b17c7a...")emitsWHERE 88b17c7a... LIMIT 1, which MySQL rejects withError 1054 (42S22): Unknown column '...' in 'where clause'. Only strings that parse viastrconv.Atoiare treated as primary-key values, so this bug is invisible for integer IDs and catastrophic for UUID primary keys. Always bind explicitly:Take(&x, "id = ?", id)orWhere("id = ?", id).Take(&x). - When you change a query form across many files, verify the generated SQL with a
Session(&gorm.Session{DryRun: true})probe before relying on it. A passing unit test with no rows and a brokenWHEREclause look identical unless the SQL is inspected.
Shared database schema (Go + Python)
The Go services and the Python API write to the same MySQL/PostgreSQL schema, and both own parts of it. Neither is authoritative over the whole.
Each ORM derives index names differently:
- Go (
internal/entity/): GORM derivesidx_<table>_<column>for indexes anduni_<table>_<column>for unique constraints. Tags in this repo name unique indexes explicitly, e.g.uniqueIndex:idx_commit_file. - Python (
api/db/db_models.py): Peewee derives<db_table>_<col1>_<col2>(truncated with an md5 suffix past 64 chars, seeplayhouse.migrate.make_index_name). There is noidx_prefix.
Both sides reconcile by column set, not by name: Go's hasUniqueIndex (internal/dao/migration.go) accepts an existing index only when its full column set equals the requested one, and Python's ensure_model_indexes (api/db/db_models.py) keys DB.get_indexes() on the column tuple. Consequences:
- Divergent names covering the same columns are expected, not a defect. Whichever runtime reaches the database first decides the name. Do not "fix" this by renaming inside a migration; it rewrites live databases for no functional gain.
- Add a shared unique index to both sides (Go tag + Peewee
Meta.indexesorunique=True), so each runtime recreates it when absent. Do not add a second index over a column set an existing one already covers. - In Go, declare single-column uniqueness with a named
uniqueIndex:, not a bareuniquetag. A bareuniquebecomes a table-level constraint in DDL, which SQLite materializes as an implicit index that cannot be dropped by name, and which GORM'sMigrateColumnUniquethen tries to drop under its ownuni_<table>_<column>name — MySQL answers 1091 every startup.namedIndexMigrator(internal/dao/database.go) suppresses exactly that phantom drop; seeTestNamedIndexMigratorSkipsPhantomUniqueDrop. - A Python model existing does not mean Python owns that table's constraints.
pipeline_operation_loghas norun_countcolumn and no unique index indb_models.py; both are added only byinternal/dao/migration.go.
Go Test Tiers
Go tests are classified by build tag. Tag a test file with //go:build <tier> placed before the package clause. Run them through build.sh so the required CGO configuration and native libraries are available.
| Tier | Build tag | Runs by default? | Needs |
|---|---|---|---|
| Unit | (none) | Yes (bash build.sh --test) |
Native CGO static libs; no external services — uses in-memory SQLite, miniredis, or httptest stubs. |
| Integration | integration |
No (-tags integration) |
A real service: MySQL/MinIO/Elasticsearch/Infinity/LLM. Single component, reasonably fast. |
| E2E | e2e |
No (-tags e2e) |
Full cross-component pipeline (ingest → index → retrieve) against real services; heavy/slow. |
| Manual | manual |
No (-tags manual) |
Very slow/expensive (deepdoc render/parity/snapshot/bench). Local opt-in ONLY — never run in CI. |
| Native (orthogonal) | cgo / !cgo |
cgo auto-satisfies under CGO_ENABLED=1 |
Native static libs (office_oxide/pdfium/pdf_oxide). Combine with tiers, e.g. //go:build cgo && integration. |
Run tiers locally via build.sh:
bash build.sh --test # unit tier (no tags)
bash build.sh --test-integration ./... # integration tier
bash build.sh --test-e2e # e2e tier
bash build.sh --test-manual # manual tier (very slow)
bash build.sh --test-all # integration + e2e (never includes manual)
Rules:
- New tests that touch a real external service MUST carry
integration/e2e/manual— do not rely ont.Skip+ env vars to soft-isolate them in the default unit run. Keep an env guard as a harmless secondary safety net if desired. manualis never wired into CI or any automated pipeline.unit(no tag) must stay free of external-service dependencies sobash build.sh --testruns without MySQL/MinIO/ES/Infinity/LLM. The native CGO static libraries (office_oxide/pdfium/pdf_oxide) are still required at build time and are wired bybuild.sh --test; that is expected, not an external service.
Working Rules
- When reviewing documentation or code, inspect the full affected path and report all verifiable findings in one review; do not return after only a few findings and expose further issues in later rounds.
- When handling review comments, independently verify each substantive claim against the current code or tests before accepting, rejecting, or acting on it.
- Before editing, inspect the nearest code path that actually owns the behavior.
- For server changes, work in the Go implementation. If a Python behavior is still needed, port it to Go and test the Go path before removing the Python code.
- Keep changes small and local unless the task is explicitly a broader refactor.
- Prefer one implementation path instead of preserving old and new versions side by side.
- Preserve behavior with focused tests when the behavior is still valid; do not keep tests that protect obsolete behavior.
- If a surface is only there for compatibility, remove it unless the user asks to keep it.
- Do not add new compatibility wording in comments or docs.
- When a maintainer takes over a community PR, a new commit generated by rewriting history (e.g.
merge,rebase -i) must preserve the original author and add the maintainer as co-author (via aCo-authored-by:trailer) instead of overwriting the author with the maintainer alone.
Commands
Go backend
bash build.sh --all # build native code and Go server
bash build.sh --go # build Go server with native library already built
bash build.sh --test ./internal/handler/... # targeted unit tests
bash build.sh --test # all unit tests
bash build.sh --test-integration ./... # integration tests; requires real services
bash build.sh --test-e2e # full-pipeline tests; requires real services
bash build.sh --test-manual # local opt-in only
bash build.sh --test-all # integration + e2e; excludes manual
Native libraries, Go DeepDoc models, tokenizer assets, and ragflow_deps/cl100k_base.tiktoken come from ragflow_deps/download_deps.py; see README_zh.md for its isolated setup. The downloader installs stagehand into the SDK cache using the version in go.mod. Build the Go resource image with cd ragflow_deps && docker build -f Dockerfile -t infiniflow/ragflow_deps:latest . after downloading the resources.
Frontend
cd web
npm install
npm run dev
npm run build
npm run lint
npm run test
npm run type-check
Validation Preference
- Run the narrowest relevant test, lint, or build command after a change.
- For backend changes, first run package-scoped
bash build.sh --test ./path/to/package/.... Run the integration or e2e tier when the changed behavior needs real services or a full pipeline. - For frontend changes, prefer the touched-package lint, type-check, or test command.
- Do not default to raw
go test,go build, or IDE Run/Debug for Go in this repo. They often miss the required CGO flags and native static libraries (office_oxide,pdfium-static,pdf_oxide) thatbuild.shwires correctly. - After a Go build, execute the resulting binary's help command for each changed server mode; a successful link alone does not prove the binary starts. On this host, the original
build.shselected default LLD 18 and produced binaries that crashed before Gomain; linking with LLD 20 started successfully. Verify the linker actually selected on Linux before trusting a new binary. - If Go native builds fail, inspect
build.shandinternal/development.mdbefore changing code. Common environment issues are missing downloaded native dependencies or an incompatibleld.lldon Linux.
Default review checklist
- Remove instead of retaining
deprecated,legacy, or compatibility-only code. - Collapse duplicate implementations to one path.
- Drop stale comments and documentation that describe a superseded design.
- Keep exported APIs only when the current code actually needs them.