A self-hostable RAG question-answer bot. Users ask in natural language via Telegram; the system retrieves grounded context from a Qdrant document store and generates a cited answer via an LLM. It is a Python modular monolith β one process, in-process function calls, with external sidecar services managed by Docker Compose.
The current live domain is real-estate/apartments. The domain layer is replaceable.
The live Telegram bot is a real-estate assistant: a RAG Q&A core plus a service menu. All actions are kept; only the question path is the pure RAG core, the rest is the (replaceable) domain layer.
| Menu action | What it does | Layer |
|---|---|---|
| π¬ Ask a question | RAG Q&A over the document store | core |
| π Find an apartment | Filtered catalog search | domain |
| π Services | Service info | domain |
| π Book a viewing | Schedule a viewing | domain |
| π€ Contact a manager | Human handoff (HITL) | domain/agent |
| π My bookmarks | Saved listings | domain |
| π― Demo | Guided demo flow | domain |
User message (Telegram)
β
βΌ
run_assistant_request() src/core/assistant.py
β
βΌ
run_assistant_pipeline() src/runtime/pipeline/assistant_pipeline.py
β
ββ classify_query() src/runtime/graph/nodes/classify.py
β
βΌ
rag_pipeline() src/runtime/pipeline/rag.py
β cache check β hybrid Qdrant search (dense+sparse+ColBERT)
β β grade docs β optional rerank β optional query-rewrite loop
β returns: grounded document context
β
βΌ
generate_answer() src/runtime/generation/service.py
β LLM call with retrieved context
β
βΌ
AssistantResult (answer + citations)
β
βΌ
Telegram reply
run_assistant_request (src/core/assistant.py) is the single public entrypoint used by all adapters and the golden E2E test.
One Python process. Three layers:
| Layer | Path | Role |
|---|---|---|
| Adapter | telegram_bot/ |
Telegram interface β converts messages to/from AssistantRequest / AssistantResult |
| Public boundary | src/core/ |
contracts.py defines Protocol-based DI types; assistant.py is the entrypoint |
| Engine | src/runtime/ |
Pipeline, RAG, retrieval, generation, grounding |
External sidecar services (Docker Compose β not part of the Python binary):
| Service | Purpose |
|---|---|
| Qdrant | Vector store β dense, sparse, and ColBERT-style retrieval |
| BGE-M3 (ONNX) | Self-hosted embeddings served via a local API |
| Redis | Five independent caches: semantic answer, embedding, search, rerank, extraction. Version-prefixed keys; graceful degradation on miss |
| PostgreSQL | Persistent state (conversation, ingestion tracking) |
An optional LangGraph supervisor + tool-routing layer exists in telegram_bot/agents/ for CRM-style workflows (lead scoring, manager handoff, HITL confirmation). It is not required for the core Q&A path.
src/ingestion/unified/ β deterministic, idempotent, production-ready:
- SHA256-based file identity: re-ingesting the same file is a no-op.
- Idempotent upsert: changed files replace prior chunks by source path. Deleted source files are a known limitation β their chunks remain in Qdrant until manual cleanup.
- Error handling: failed documents are logged and skipped;
run_watchretries on the next polling cycle (60 s). No DLQ or exponential backoff β orphaned chunks from deleted source files remain in Qdrant until manual cleanup (known limitation). - Docling handles parsing in-process (native SDK, no HTTP sidecar); the unified pipeline handles chunking and embedding writes.
Replace the domain layer; keep the engine and infrastructure.
Replaceable: telegram_bot/services/apartment_* prompts and extraction logic, search schema fields, CRM/tool integrations, UI copy, i18n strings.
Keep: src/core/, src/runtime/, src/ingestion/unified/, Redis cache layer, Docker Compose profiles.
The current domain (real-estate/apartments) lives entirely in the adapter and service layers. Swapping it does not require touching the retrieval engine or pipeline.
Prerequisites: Python 3.12+, uv, Docker with Compose.
Runtime: Docker Compose only. No k8s, no Mini App, no CRM/Kommo integration.
cp .env.example .env # fill in credentials
make core-min-up # start Qdrant + Redis (minimal)
# or
make core-up # start full sidecar stack (adds BGE-M3, PostgreSQL)Run the bot natively:
make run-botRun the Compose bot stack:
make docker-bot-upNotable configurable env vars (see .env.example): QDRANT_QUANTIZATION_MODE, REDIS_MAX_CONNECTIONS.
make check # Ruff lint + MyPy type checking (non-strict; disallow_untyped_defs=false)
make test-core # Fast core gate (~91 tests, ~8s) β run first for any src/core or src/runtime change
make test # Broader fast gate (unit + graph paths) β run for adapter/service changes
make e2e-core-live # Golden E2E: indexes fixture corpus, runs full spine through run_assistant_request
make qdrant-audit-indexes # Audit Qdrant payload indexesmake e2e-core-live is the main proof of the core path. It exercises classification, retrieval, generation fallback, and runs without Telegram or voice. It requires local Qdrant and BGE-M3 running (make core-up).
CI runs static/lint guardrails only (Ruff, MyPy, Semgrep, lockfile check). Pytest suites are local/manual.
The core pipeline (src/core/ + src/runtime/) is healthy and well-tested. The following surfaces are physically in-tree but are archived/reference β not part of the active production path, and being trimmed in open issues:
- LangGraph dead nodes β some graph nodes are no longer on the live execution path but remain in the file tree.
Langfuse removed β Langfuse SDK and tracing are fully removed (no from langfuse imports anywhere). The @observe decorators that remain are local no-op shims (src.observability / telegram_bot.observability) β not tracing. Observability is through structured logs.
The active production adapter is Telegram (telegram_bot/). Voice input is active via telegram_bot/dialogs/ (catalog and demo dialogs).
| Area | Path |
|---|---|
| Core entrypoint | src/core/assistant.py |
| Pipeline + RAG engine | src/runtime/pipeline/ |
| Telegram adapter | telegram_bot/ |
| Domain tools + agents | telegram_bot/agents/, telegram_bot/services/ |
| Unified ingestion | src/ingestion/unified/ |
| Compose runtime | compose.yml, DOCKER.md |
| Document | Use it for |
|---|---|
DOCKER.md |
Compose services, profiles, ports, env, runtime contracts |
The project is being hardened to a senior-grade codebase without dropping any feature β tracking epic #2983. In short: keep the full feature menu, remove migration cruft (dead LangGraph nodes, stale tests), decompose the bot.py god-object into per-feature handlers, document the feature map, and freeze the entry-path contracts β with no new frameworks and no over-engineering.
This project is licensed under the MIT License.