# Chat Backend Django + Channels API for AIML Operations chat (`chatbackend.aimloperations.com`). Packaging via `uv`; production deploy via `server-infra`. Companion frontend: [`chat_web_app`](https://git.aimloperations.com/ai_ml_operations/chat_web_app) (node-static, not Docker). Ticket: [chat_backend#6](https://git.aimloperations.com/ai_ml_operations/chat_backend/issues/6) ## Layout ```text chat_backend/ ← repo root (Dockerfile, compose, pyproject, .gitea) ├── llm_be/ ← Django project root (manage.py) │ ├── manage.py │ ├── llm_be/ ← settings, urls, asgi/wsgi │ └── chat_backend/ ← app (models, consumers, services, storage) ├── scripts/ │ ├── docker-entrypoint.sh │ └── validate-env.sh ├── docker-compose.yml ← local/CI (bundled Postgres) └── docker-compose.prod.yml ← server-infra (external DATABASE_URL) ``` ## Local development ### Prerequisites - Python 3.12+ - [uv](https://docs.astral.sh/uv/) - Docker + Docker Compose (optional, recommended) - Ollama reachable at `OLLAMA_BASE_URL` for LLM features ### uv (host) ```bash cp .env.example .env uv sync cd llm_be uv run python manage.py migrate uv run python manage.py runserver 0.0.0.0:8003 ``` Without `DATABASE_URL` / `DB_HOST`, settings fall back to SQLite (`llm_be/db.sqlite3`). Tests: ```bash cd llm_be uv run python manage.py test ``` The suite is offline by default — the custom test runner (`llm_be/test_runner.py`) sets `SKIP_RAG_INIT=1` and a cheap password hasher, and LLM chains are faked, so no Ollama, Chroma, SMTP or network access is needed. Tests live in `llm_be/chat_backend/tests/` (models, storage, serializers, views, services, signals, consumers). Non-deterministic checks against a real model server are opt-in: ```bash cd llm_be RUN_LIVE_OLLAMA_TESTS=1 uv run python manage.py test chat_backend.tests.test_live_ollama ``` ### Docker (dev, bundled Postgres) ```bash docker compose up --build ``` App: http://localhost:8003 — Postgres via bundled `db` (`postgres://chat_backend:chat_backend@db:5432/chat_backend`). Compose does **not** read host `DATABASE_URL` (avoids CI/prod leaks); override with `COMPOSE_DATABASE_URL` if needed. ## Environment variables | Variable | Dev default | Prod required | Notes | |----------|-------------|---------------|-------| | `DJANGO_ENV` | `dev` | `prod` / `beta` | | | `DJANGO_SECRET_KEY` | insecure default | yes | Must be real in prod/beta | | `DJANGO_DEBUG` | true when `dev` | `false` | | | `DJANGO_ALLOWED_HOSTS` | localhost + chat hosts | yes | Comma-separated | | `DJANGO_CSRF_TRUSTED_ORIGINS` | derived from hosts | optional | Full origins | | `DATABASE_URL` | SQLite fallback | yes | Shared Postgres in prod | | `WEB_PORT` | n/a (compose maps 8003) | `8003` | Host port for prod compose | | `OLLAMA_BASE_URL` | `http://127.0.0.1:11434` | yes | GPU host in prod: `http://10.0.0.128:11434` | | `OLLAMA_MODEL` / `OLLAMA_EMBED_MODEL` | from `DEBUG` | optional | Override model names | | `EMAIL_HOST_*` | empty | yes (prod/beta) | SMTP2GO | | `CAPTCHA_SECRET_KEY` | empty | recommended | | | `ENABLE_ACCOUNT_REGISTRATION` | `false` | optional | Self-serve sign-up; keep false until ready | | `STRIPE_SECRET_KEY` / `STRIPE_PUBLISHABLE_KEY` / `STRIPE_WEBHOOK_SECRET` | empty | yes for billing | Stripe API + webhook | | `STRIPE_PRICE_ID` | empty | optional | Pre-created Price; else `$10/mo` from settings | | `FRONTEND_BASE_URL` | `http://localhost:3000` | set in prod/beta | Checkout success/cancel, portal return, OAuth return | | `STRIPE_PORTAL_RETURN_URL` | `{FRONTEND}/account/` | optional | Stripe Customer Portal return URL | | `CORS_ALLOWED_ORIGINS` | local + chat FE (+ beta FE default) | set in prod/beta | Frontend origin(s) | | `USE_TLS_PROXY` | false (dev) | true behind NPM | Sets `SECURE_PROXY_SSL_HEADER` | | `GUNICORN_WORKERS` / `GUNICORN_BIND` | 2 / `0.0.0.0:8000` | optional | Entrypoint | | `SKIP_RAG_INIT` | unset | CI/migrate often `1` | Skip Chroma/Ollama boot work | Assistant identity (`Hesychia`) lives in code: `llm_be/chat_backend/services/assistant_identity.py` — prepended to user-facing generation prompts (chat, RAG, data analysis). Not env-configurable. Templates: `.env.example` (local), `.env.prod.example` (control-node secret). Control-node secret paths (server-infra on ai-server-4080): ```text ~/Documents/secrets/chat_backend/chat_backend_prod.env ~/Documents/secrets/chat_backend/chat_backend_beta.env ``` Validate with: ```bash ./scripts/validate-env.sh ~/Documents/secrets/chat_backend/chat_backend_prod.env ./scripts/validate-env.sh ~/Documents/secrets/chat_backend/chat_backend_beta.env ``` If `DATABASE_URL` password contains `$`, escape each as `$$` for Compose. ## Ollama All clients (`ollama.Client`, `OllamaLLM`, `OllamaEmbeddings`, `ChatOllama`) use `OLLAMA_BASE_URL` — never hardcoded localhost in deployed code. | Env | Typical URL | |-----|-------------| | Local (Ollama on same machine) | `http://127.0.0.1:11434` | | prod / beta (containers on adama/roslin/ai-server) | `http://10.0.0.128:11434` | Firewall / Ollama listen on ai-server-4080 must allow `10.0.0.0/24` → `:11434`. ## File storage Prompt attachments and workspace documents use **`DatabaseStorage`** (`chat_backend.StoredFile` BinaryField in Postgres). Blobs are **not** written to the container filesystem under `media/`. RAG loaders that need a path materialize a short-lived temp file, then delete it. Chroma’s vector index may still use a volume (`chroma_db`); that is embeddings metadata, not the original upload. ## Production / beta (docker-compose.prod.yml) - Single `web` service; **no** bundled DB — `DATABASE_URL` → shared Postgres (`10.0.0.230`). - Host port from `WEB_PORT` (prod **8003**; beta **8013**). - Entrypoint: wait DB → migrate → collectstatic → `gunicorn` + `UvicornWorker` (ASGI for HTTP **and** WebSockets). - Active/active on **adama + roslin + ai-server-4080**; NPM balances upstreams. - Manual / local deploy: ```bash # beta (day-to-day) ~/Documents/repos/server-infra/scripts/deploy.sh \ --app chat_backend --env beta --ref # prod (intentional) ~/Documents/repos/server-infra/scripts/deploy.sh \ --app chat_backend --env prod --ref ``` Beta hosts / CORS: `beta.chatbackend.aimloperations.com` API + `https://beta.chat.aimloperations.com` SPA (see `.env.prod.example` beta block). DB: `chat_backend_beta`. Pair with [server-infra#7](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/7) and frontend [chat_web_app#35](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/35). ## CI / CD (Gitea Actions) | Workflow | Trigger | Action | |----------|---------|--------| | `unittests.yml` | push + PR → `master` | `uv sync` + `manage.py test` | | `ci.yml` | PR → `master` | same unit tests | | `deploy-beta.yml` | **push** to `master` | unit tests → docker compose tests → `deploy.sh --env beta` | | `deploy-prod.yml` | **manual** `workflow_dispatch` only | unit tests → docker compose tests → `deploy.sh --env prod` | Push/merge to `master` auto-deploys **beta** only. Prod requires the Gitea **Run workflow** button on **Deploy Prod**. Deploy never runs on PRs. ## Frontend API notes ### Self-delete account ([#34](https://git.aimloperations.com/ai_ml_operations/chat_backend/issues/34)) | | | |--|--| | Method / path | `DELETE /api/user/` | | Auth | JWT (authenticated user only; always deletes `request.user`) | | Optional body | `{ "refresh_token": "" }` | | Success | `200` `{ "detail": "Account deleted.", "deleted": true }` | | Effects | Sets `deleted=True`, `is_active=False`; soft-deletes conversations; blacklists outstanding refresh tokens; logs `UserAuthEvent` `account_deleted` | | Staff | Staff/superuser self-delete rejected (`400`, `code=staff_forbidden`) | | Privacy v1 | Soft-delete only (no anonymization / hard purge) | Post-delete UX: clear local tokens → redirect to sign-in. Subsequent `/token/obtain/` fails. Do **not** send another user's id/email — ignored. ### Subscription change / cancel (portal + webhooks) Plan change and cancel stay on Stripe Customer Portal (`POST /api/finance/portal/`). Local state syncs via `customer.subscription.updated` / `deleted` webhooks. `GET /api/finance/subscription/` includes `cancel_at_period_end` and `current_period_end` for Account UI messaging. Subscription audit (`UserAuthEvent` on the user admin): - `subscription_started` — first active plan (Checkout, Backer redeem, admin assign) - `subscription_updated` — plan/status/cancel-at-period-end changes (portal + webhooks) ## Security note Secrets previously hardcoded in `settings.py` (email password, captcha, Django secret) must live only in the control-node env file. Rotate anything that was ever committed; never commit `.env` or `~/Documents/secrets/`.