5.1 KiB
5.1 KiB
Decisions (Architecture Decision Record)
| # | Decision | Chosen | Alternatives rejected | Why |
|---|---|---|---|---|
| 1 | Language/runtime | TypeScript + Node.js (LTS) | Python (aiogram), Go | Single-language team, grammY ecosystem, shared types across adapter/core |
| 2 | Telegram framework | grammY | Telegraf, raw Bot API | Actively maintained, first-class webhook + middleware, TS-native |
| 3 | AI orchestration | Minimal orchestration (no LangGraph/LangChain yet) | LangGraph.js, LangChain.js | Phase 1 needs a single LLM call; the spec allows "smallest useful surface". Introduced later only when agent/tools/memory justify it. |
| 4 | LLM access | Existing LiteLLM gateway (litellm:4000/v1), model alias mem0-openai (upstream OpenAI gpt-5-mini primary + OpenRouter fallback, load-balanced inside LiteLLM) |
Direct OpenAI + OpenRouter dual-provider in app code | Reuses the mem0 stack's existing failover; one OpenAI-compatible endpoint. NOTE: LiteLLM serves ONLY declared aliases — gpt-5-mini is NOT valid, must use mem0-openai (caused a 400 Invalid-model bug). |
| 5 | Database | Reuse mem0-postgres (pgvector/pgvector:pg17) in an isolated bot database |
Project-specific postgres container, NocoDB | Avoids a second Postgres; pgvector already provisioned for Phase 3; bot db isolates from mem0 tables. |
| 6 | DB provisioning | Self-migration on startup (CREATE DATABASE + schema_migrations) |
Manual psql one-time | Reproducible, spec §14 requires migration/startup checks. |
| 7 | Chat state | Postgres bot_user/conversation/message |
In-process memory, Redis | Spec §8.2: Postgres is the durable system of record. |
| 8 | RAG / product facts | IMPLEMENTED 2026-08-31: CatalogRetriever (keyword, not vector) replaces NoopRetriever — SKU-token + name/category substring + CJK-particle-stripped segments over the generated catalog.ts; top-5 facts injected into the system prompt. Live in prod wiring (server.ts). Paid SKU hits also surface their FREE trial twin (CZH01 → FREECZH01) so free-trial questions answer from facts. |
Vector DB in Phase 1 | Spec §8.3: no corpus justifies it yet; keyword search is enough at 254 SKUs; pgvector first later. |
| 9 | n8n role | Automation/tool layer via constrained HTTP client | n8n as the app backend | Spec §9. Tool inputs validated; no arbitrary workflow execution exposed to the LLM. |
| 10 | Multi-channel | Shared core + per-channel adapters (Channel/IncomingMessage/OutgoingMessage + capability flags) |
Per-channel monoliths | Spec §7. Future Shopee/Lazada adapters implement the contract without touching the AI core. |
| 11 | Deployment | Docker multi-stage (dev bind-mount + tsx watch; prod immutable image) | Host-installed Node | Spec §4/§5: reproducible prod, hot-reload dev. |
| 12 | Secrets | Env vars via .env (gitignored); source-of-truth in repo ../SECRETS.md |
Hard-coded, dotenv-in-repo | Spec §12/§15; repo convention: single credential store. |
| 13 | Webhook ingress | CF tunnel bot.digikedai.com → directly to http://digikedai-bot:8080 (container name on bridge_hoelee), no Traefik hop |
Traefik labels, DSM nginx | cloudflared and the bot share bridge_hoelee; the bot's own Hono server answers /health + /<secret>/webhook, so no proxy middleware is needed. Simplest path that requires no shared-proxy changes. |
| 14 | Webhook timeout | grammY onTimeout:"return" + 50s; LLM client timeout 45s |
grammY default throw@10s |
LLM (GPT-5-mini w/ reasoning) replies exceed 10s → grammY never returns 200 → Telegram re-delivers the same update forever ("bot keeps replying" loop). |
| 15 | Host port | 5247:8080 |
8080 (taken by DSM nginx), 5244-5246 (alist stack) | 5247 was the first free port. |
| 16 | Rate limiting | In-memory fixed-window RateLimiter keyed by channel:userId, enforced in the shared MessageService (after normalize, before persist/LLM); deny replies once per window, then silence; webhook still answers 200 |
Redis, @grammyjs/ratelimiter, per-IP limits |
Single replica today (no Redis needed); channel-agnostic so Shopee/Lazada inherit it; never lets Telegram re-deliver a throttled update (same principle as D14). |
| 17 | User long-term memory | Postgres user_memory KV table (keyed by bot_user.id, not conversation) + cheap LLM async summary via SUMMARY_MODEL (default = main model); trigger = new facts only (lang change / new SKU / identity keywords / ≥10 turns fallback); every memory failure degrades silently, never blocks the reply |
mem0 HTTP API, pgvector, per-turn summarization, rule-only extraction | Reuses existing Postgres + LiteLLM with zero new infra; cross-session/cross-channel personalization with bounded cost (docs/MEMORY_FEATURE.md) |
Open decisions (Phase 2)
- D1 — Mint a dedicated LiteLLM virtual key for the bot (rate-limit + independent revocation) instead of sharing the master key.
- D2 —
Traefik routeRESOLVED 2026-08-29: CF tunnel targetshttp://digikedai-bot:8080directly (D13), no Traefik/nginx hop needed. - D3 — Exact free-account n8n workflow path + input schema (currently a placeholder).
- D4 — User allowlist: enabled-by-default for production (currently open in development).