westfarn 2e9e95e16c
Deploy Beta / unit-tests (push) Successful in 10s
Unit Tests / test (push) Successful in 9s
Deploy Beta / docker (push) Successful in 18s
Deploy Beta / deploy-beta (push) Successful in 46s
Stove-pipe RAG retrieval to prevent cross-tenant leakage (#40) (#41)
## Summary

- Closes [#40](#40)
- Aligns chat/RAG with the abc_worker stove-pipe pattern ([b13cec8](b13cec88f9)): immutable `ChatCompanyScope` per turn, conversation ownership validation, fail-closed Chroma filters
- Prefer ASGI/JWT identity over client email; never bind identity from `conversation_id` alone
- Close `ConversationDetailView` IDOR (prompts only for `request.user`)

## Changes

- New `services/chat_tenant_scope.py` with frozen `ChatCompanyScope` + ownership checks
- WebSocket consumers (`consumers.py` / `consumers_graph.py`) validate scope before `get_messages` / RAG
- `search_documents` requires a workspace (no more `filter: None` over the shared collection)
- Ingest writes `company_id` metadata (retrieval still keys on `workspace_id` for back-compat)
- Legacy `get_retriever` always applies a workspace filter

## Test plan

- [x] `manage.py test chat_backend.tests.test_chat_tenant_scope chat_backend.tests.test_consumers chat_backend.tests.test_services_rag chat_backend.tests.test_views_conversations`
- [ ] Manual: user A cannot stream RAG context from user B `conversation_id`
- [ ] Manual: RAG still returns own-company docs after deploy (existing vectors with `workspace_id` only)
- [ ] Follow-up: FE can send JWT `token`/`access` on WS payloads for stronger identity bindingReviewed-on: #41
2026-08-01 12:35:00 -07:00
2025-12-07 06:31:06 -06:00
2025-03-07 12:23:00 -06:00

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 (node-static, not Docker).

Ticket: chat_backend#6

Layout

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
  • Docker + Docker Compose (optional, recommended)
  • Ollama reachable at OLLAMA_BASE_URL for LLM features

uv (host)

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:

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:

cd llm_be
RUN_LIVE_OLLAMA_TESTS=1 uv run python manage.py test chat_backend.tests.test_live_ollama

Docker (dev, bundled Postgres)

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):

~/Documents/secrets/chat_backend/chat_backend_prod.env
~/Documents/secrets/chat_backend/chat_backend_beta.env

Validate with:

./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. Chromas 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:
# beta (day-to-day)
~/Documents/repos/server-infra/scripts/deploy.sh \
  --app chat_backend --env beta --ref <sha>

# prod (intentional)
~/Documents/repos/server-infra/scripts/deploy.sh \
  --app chat_backend --env prod --ref <sha>

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 and frontend chat_web_app#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)

Method / path DELETE /api/user/
Auth JWT (authenticated user only; always deletes request.user)
Optional body { "refresh_token": "<current refresh>" }
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/.

S
Description
Backend django project for the chat site
Readme
1.2 MiB
Languages
Python 98.9%
HTML 0.7%
Shell 0.3%