Commit Graph
34 Commits
Author SHA1 Message Date
westfarn 95edf3090e Add Drive sync progress, paginated docs list, and prompt heatmap API.
CI / test (pull_request) Successful in 11s
Unit Tests / test (pull_request) Successful in 10s
Expose sync_total/processed counters for FE progress bars (#59), support
documents pagination/search/sort/workspace scope (#60), and add
weekday×hour user prompt heatmap analytics (#94).
2026-08-02 06:44:13 -05:00
westfarn fd12bb972d Async Drive sync via Django tasks (#57) (#58)
Deploy Beta / unit-tests (push) Successful in 11s
Unit Tests / test (push) Successful in 10s
Deploy Beta / deploy-beta (push) Successful in 49s
Deploy Beta / docker (push) Successful in 20s
## Summary
- Closes [#57](#57)
- Companion FE: [chat_web_app#90](ai_ml_operations/chat_web_app#90) / [PR #91](ai_ml_operations/chat_web_app#91)
- `POST /api/drive/connections/<id>/sync/` enqueues via Django 6 Tasks (`drive_tasks.enqueue_drive_sync`) and returns **202** with `last_sync_status=pending`
- Webhooks + `manage.py sync_drive_connections` use the same enqueue path (`--sync-now` for inline/cron)
- With default `ImmediateBackend`, sync still runs in-process but on a **daemon thread** so HTTP returns quickly; swap `TASKS` later for a durable worker
- Duplicate syncs while already `pending` are skipped (unless `force=True`)

## Test plan
- [x] `manage.py test chat_backend.tests.test_drive_tasks chat_backend.tests.test_views_drive chat_backend.tests.test_management_drive_sync`
- [ ] Manual: Sync now returns fast; connection flips pending → ok/error
- [ ] Manual: Google Drive API disabled → `last_sync_status=error` + message in `last_sync_error`
- [ ] Manual: second Sync while pending does not stack jobsReviewed-on: #58
2026-08-02 04:06:54 -07:00
westfarn 7025dab857 Personal Drive/RAG without a company (#55) (#56)
Deploy Beta / unit-tests (push) Successful in 10s
Unit Tests / test (push) Successful in 10s
Deploy Beta / docker (push) Successful in 21s
Deploy Beta / deploy-beta (push) Successful in 49s
## Summary
- Closes [#55](#55) (related [#46](#46))
- **Personal** Google Drive / OneDrive works for users **without** a company (personal RAG)
- **Company** Drive still requires company + manager
- Schema: nullable `DriveConnection.company`, personal `DocumentWorkspace.user`, ownership check constraints + conditional uniques
- Runtime: `ensure_personal_workspace`, personal sync → personal WS, chat/document APIs fall back to personal WS when `company_id` is null
- Supersedes the interim "reject with `no_company`" approach (wrong for personal connect)

## Test plan
- [x] OAuth: personal Drive callback with `user.company=NULL` succeeds (`company_id=NULL` on connection)
- [x] OAuth: company Drive still returns `no_company` / `forbidden` appropriately
- [x] Chat tenant scope creates personal workspace for solo users
- [x] Drive sync + document view suites (`91` related tests)
- [ ] Manual: solo entitled user connects Google Drive → success, sync lands in personal workspace
- [ ] Manual: company manager company Drive still works
- [ ] Migrate prod/staging with `0029_personal_drive_rag_without_company`Reviewed-on: #56
2026-08-02 03:44:42 -07:00
westfarn d54094f5e0 Tier-gated RAG + Drive document sources (#42) (#54)
Unit Tests / test (push) Successful in 10s
Deploy Beta / unit-tests (push) Successful in 10s
Deploy Beta / docker (push) Successful in 21s
Deploy Beta / deploy-beta (push) Successful in 40s
## Summary

Implements epic [#42](#42) (children #43–#53) and advances [#11](#11).

- **Entitlement:** `allows_rag` on plans (founders / backer / pro / business; not standard); exposed as `features.rag`
- **Gates:** document REST + WS `PromptType.RAG` use `assert_feature_allowed(..., "rag")`
- **Lifecycle:** dedupe ingest, delete vectors by `document_id`, honor `active`, fix document detail PATCH/DELETE
- **Workspaces:** auto-create default company workspace; fail-closed scoping
- **Drive:** personal + company Google/Microsoft connect (`link_drive` / `link_company_drive`), resource selection, sync, webhooks stubs, `sync_drive_connections` management command
- **Docs/env:** README + `.env*.example` updated

Companion FE: `chat_web_app` branch `feature/rag-epic-42-ui` (#81–#85).

## Test plan

- [x] `SKIP_RAG_INIT=1 uv run python manage.py test` (457 OK)
- [ ] Migrate finance `0004` + chat_backend `0028` on beta
- [ ] Verify Standard user: Documents API 403 + no RAG retrieval
- [ ] Verify Founders/Pro: upload + list + active toggle
- [ ] Connect Google/Microsoft Drive (incremental scopes) and Sync
- [ ] Company manager: `link_company_drive`; non-manager 403
- [ ] Run `manage.py sync_drive_connections`Reviewed-on: #54
2026-08-01 14:02:36 -07:00
westfarn 2e9e95e16c Stove-pipe RAG retrieval to prevent cross-tenant leakage (#40) (#41)
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
## 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
westfarn eedc842b08 Add account self-delete and subscription lifecycle sync (#34) (#39)
Deploy Beta / unit-tests (push) Successful in 10s
Unit Tests / test (push) Successful in 10s
Deploy Beta / docker (push) Successful in 26s
Deploy Beta / deploy-beta (push) Successful in 6m46s
## Summary
- Closes [#34](#34)
- Companion for [chat_web_app#75](ai_ml_operations/chat_web_app#75) (portal cancel/change local sync)
- Soft-delete `DELETE /api/user/` for the authenticated user only: `deleted=True`, `is_active=False`, hide conversations, blacklist outstanding refresh tokens; staff self-delete rejected
- Stripe `customer.subscription.updated` / `deleted` webhooks sync plan status, `cancel_at_period_end`, and `current_period_end`; checkout assigns plan from `metadata.plan_slug`
- **UserAuthEvent audit**: `account_deleted`, `subscription_started` (first active plan), `subscription_updated` (plan/status/cancel changes) — visible on user admin
- Document FE contract in README (endpoint, response, post-delete logout)

## Test plan
- [ ] `uv run python manage.py test chat_backend.tests.test_views_users.CustomUserSelfDeleteTestCase finance.tests`
- [ ] Authenticated `DELETE /api/user/` soft-deletes self, hides conversations, blocks re-login, writes `account_deleted` auth event
- [ ] Checkout / Backer assign writes `subscription_started`; portal cancel/change writes `subscription_updated`
- [ ] Anonymous / staff self-delete rejected; body cannot target another user
- [ ] After portal cancel, webhook sets `cancel_at_period_end` / `canceled` on `GET /finance/subscription/`Reviewed-on: #39
2026-08-01 12:24:17 -07:00
westfarn cc45ae5808 Persist Ollama token usage from streamed LLM responses (#16) (#38)
Deploy Beta / unit-tests (push) Successful in 10s
Unit Tests / test (push) Successful in 10s
Deploy Beta / deploy-beta (push) Successful in 48s
Deploy Beta / docker (push) Successful in 19s
## Summary
- Fixes token tracking for [#16](#16): streaming chat never persisted `PromptMetric.tokens_in` / `tokens_out` (admin + account usage showed `—`).
- Drop `StrOutputParser` on async LLM/RAG/data-analysis chains so Ollama `generation_info` (`prompt_eval_count` / `eval_count`) survives; collect usage while streaming via `TokenUsageCollector`.
- Stop calling `self.close()` in `disconnect` (fixes Grafana `Unexpected ASGI message 'websocket.close'`).

## Test plan
- [x] Unit tests: `test_utils`, consumers, LLM/RAG/data-analysis services, finance quotas
- [ ] Deploy / local: send a chat prompt, confirm admin Prompt Metrics shows Tokens In/Out
- [ ] Reload Account usage card — in/out no longer `—` for new turns
- [ ] Confirm WS disconnect no longer raises double-close in logsReviewed-on: #38
2026-07-31 10:46:57 -07:00
westfarn 841c0962d9 Multi-plan subscriptions, quotas, and token usage APIs (#16 #17 #36) (#37)
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
## Summary
Implements [#16](#16), [#17](#17), and [#36](#36) in one backend PR.

- **#36 Multi-plan catalog**: Founders ($10, public), Standard ($15), Pro ($40), Business ($99), Backer ($0). Future tiers seeded but hidden/`is_selectable=false`. Backer email whitelist auto-assigns Founders-level access with no checkout.
- **#36 Feature + prompt gating**: plan feature flags (text vs image); rolling **6h** prompt windows (100 / 200 / 300 / 300 / 300). Enforced in both chat consumers when `ENFORCE_SUBSCRIPTION_GATES=true`.
- **#17 Token-period quotas**: optional `monthly_token_quota` on plans + per-user override; calendar-month aggregation from `PromptMetric`; warn/block when reported token totals exceed cap. Null provider usage never fabricated as 0; tracked via `turns_missing_token_usage`.
- **#16 Token API exposure**: `tokens_in` / `tokens_out` on conversation + prompt serializers (null when unknown). `GET /api/finance/subscription/` returns plan + usage snapshot for the FE.
- Checkout defaults to **Founders**; Stripe paid webhooks assign Founders. Registration/OAuth redeem Backer whitelist and return `needs_checkout`.

Companion FE PR: `chat_web_app` branch `feature/plans-quotas-token-usage`.

## Test plan
- [ ] `manage.py migrate` seeds five plans; admin can add Backer emails
- [ ] Public `GET /api/finance/plans/` returns only Founders
- [ ] Register with Backer email → active Backer, `needs_checkout=false`, checkout rejected
- [ ] Founders checkout + paid webhook → active Founders subscription
- [ ] Chat turn blocked without subscription / when prompt window exceeded / when token period exceeded
- [ ] Standard plan denies image feature; Pro/Founders/Backer allow
- [ ] Conversation/prompt API returns `null` tokens when unreported, sums when present
- [ ] `finance.tests.test_plans_quotas` + existing finance/checkout tests passReviewed-on: #37
2026-07-31 04:24:20 -07:00
westfarn 67f16565e9 Add Stripe Customer Portal session API for account billing (#35)
Deploy Beta / unit-tests (push) Successful in 9s
Unit Tests / test (push) Successful in 10s
Deploy Beta / docker (push) Successful in 26s
Deploy Beta / deploy-beta (push) Successful in 6m49s
## Summary
- Companion to [chat_web_app#33](ai_ml_operations/chat_web_app#33) (Account billing + Customer Portal)
- Follow-on from finance MVP [#21](#21): add authenticated `POST /api/finance/portal/` that creates a Stripe Billing Portal session and returns `portal_url`
- Resolve Stripe customer from the user's latest `Invoice.stripe_customer_id`; return `400` when missing (user must complete Checkout first)
- Document `STRIPE_PORTAL_RETURN_URL` (default `{FRONTEND_BASE_URL}/account/`) in settings + env examples

## Test plan
- [ ] `manage.py test finance.tests.test_portal finance.tests.test_checkout`
- [ ] Authenticated portal create with invoice that has `stripe_customer_id` → `201` + `portal_url`
- [ ] No customer / unpaid user → `400` with clear detail
- [ ] Missing `STRIPE_SECRET_KEY` → `503`
- [ ] Unauthenticated → `401`
- [ ] Custom `return_url` in body overrides default portal return URLReviewed-on: #35
2026-07-31 03:54:28 -07:00
westfarn ee3d47c8c3 Ignore WS heartbeats and reject empty chat messages (#32)
Deploy Beta / docker (push) Successful in 25s
Deploy Beta / unit-tests (push) Successful in 10s
Unit Tests / test (push) Successful in 9s
Deploy Beta / deploy-beta (push) Successful in 56s
## Summary
- Closes #31
- Ignore WebSocket `type: ping` heartbeats so keepalives no longer create conversations or hit title/LLM pipelines
- Reject empty/whitespace user messages in both chat consumers, `PromptSerializer`, and REST conversation prompt POST

## Test plan
- [x] `UserPromptGuardTestCase`, `PromptSerializerTestCase` blank/whitespace cases
- [x] `WebSocketReceiveGuardTestCase` ping ignore + empty message rejection (both WS routes)
- [ ] Deploy to beta; leave idle tab open and confirm no new rogue conversations
- [ ] Confirm normal chat send still works

Related FE: https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/51Reviewed-on: #32
2026-07-28 05:12:13 -07:00
westfarn 5d5b448868 Enable beta auto-deploy on master; manual prod button (#26) (#30)
Unit Tests / test (push) Successful in 9s
Deploy Beta / unit-tests (push) Successful in 10s
Deploy Beta / docker (push) Successful in 17s
Deploy Beta / deploy-beta (push) Successful in 1m3s
## Summary

Closes #26.

- Replace auto-prod `deploy.yml` with `deploy-beta.yml` (push to `master` → tests → `--env beta`) and `deploy-prod.yml` (`workflow_dispatch` only → `--env prod`), matching `dta_service` / `chat_web_app`
- Default `ALLOWED_HOSTS` / `CORS_ALLOWED_ORIGINS` include beta API + SPA hosts so beta frontend (and Tianji-instrumented pages) can call the API
- Expand `.env.prod.example` beta block (CORS, CSRF, OAuth callbacks, `FRONTEND_BASE_URL`, Tianji note)
- Harden `validate-env.sh` for beta secrets; README documents beta auto / prod button

## Tianji

Backend does not load `tracker.js`. FE already owns wiring (`chat_web_app#35`, closed).

- Beta SPA origin: `https://beta.chat.aimloperations.com`
- Beta Tianji website ID (FE `.env.beta`): `cms38bw671mf9n5jjw3xp1j3q`

## Coordination

- Infra: [server-infra#7](ai_ml_operations/server-infra#7) (`host_apps` beta `:8013`, `chat_backend_beta` DB, secret, NPM)
- FE companion: [chat_web_app#35](ai_ml_operations/chat_web_app#35)

## Test plan

- [ ] PR CI / unit tests green
- [ ] Merge to `master` triggers **Deploy Beta** only (not prod)
- [ ] Manual **Deploy Prod** `workflow_dispatch` still deploys `--env prod`
- [ ] After infra#7: beta container healthy on **8013** with `chat_backend_beta`
- [ ] Beta hosts / CSRF / CORS allow `https://beta.chat.aimloperations.com`Reviewed-on: #30
2026-07-27 08:35:07 -07:00
westfarn acb3a51618 Add Google/Microsoft SSO OAuth for register and sign-in (#24) (#29)
Unit Tests / test (push) Successful in 10s
## Summary
- Closes #24 (backend half)
- Add `OAuthIdentity` model (provider + `sub`, access/refresh tokens) for SSO now and Drive reuse later (#11)
- Endpoints: `GET /api/auth/oauth/<google|microsoft>/start/` and `/callback/`
- Create or link `CustomUser` by verified email; issue same JWT access/refresh; redirect FE to `/auth/callback/`
- Document `GOOGLE_OAUTH_*` / `MICROSOFT_OAUTH_*` / `OAUTH_CALLBACK_BASE_URL` in `.env.example` and `.env.prod.example`
- Expose configured providers on `GET /api/public/settings/` as `oauth.google` / `oauth.microsoft`

## Pair with
- Frontend PR: `chat_web_app` branch `feature/sso-oauth-24`

## Test plan
- [ ] `python manage.py test chat_backend.tests.test_oauth`
- [ ] With local Google/Microsoft client IDs set, complete start → IdP → callback → JWT redirect
- [ ] Existing password user with same email links identity (no duplicate)
- [ ] Unverified / missing email redirects with error code
- [ ] Registration disabled: signup start 403; login without account → `account_not_found`
- [ ] Secrets not committed; env examples onlyReviewed-on: #29
2026-07-27 05:13:32 -07:00
westfarn 16442b336c Fix password reset flow (#1) (#27)
Unit Tests / test (push) Successful in 9s
## Summary
- Closes #1
- Replace broken `csrf_exempt` `reset_password` FBV (responses never returned; missing `requests` import) with working DRF `ResetUserPassword`
- Deduplicate reset email helper; build set-password links from `FRONTEND_BASE_URL`
- Harden `SetUserPassword`: require unusable password, min 8 chars, handle missing slug
- Accept reCAPTCHA v2 (success only) and v3 (score ≥ 0.5); avoid email enumeration (200 after valid captcha)
- Add unit tests for reset + set-password edge cases

## Test plan
- [ ] `manage.py test chat_backend.tests.test_views_users.ResetPasswordTestCase chat_backend.tests.test_views_users.SetPasswordTestCase`
- [ ] With SMTP configured: request reset for known email → receive link → set password → sign in
- [ ] Unknown email still returns 200 and sends no mail
- [ ] Failed captcha returns 400
- [ ] Pair with chat_web_app `feature/password-reset-1` PRReviewed-on: #27
2026-07-27 05:07:42 -07:00
westfarn 30ce3d048d Gate self-serve registration behind ENABLE_ACCOUNT_REGISTRATION (#25)
Unit Tests / test (push) Successful in 9s
## Summary
- Add `ENABLE_ACCOUNT_REGISTRATION` (default `false`) for self-serve sign-up, set via env / control-node secret (`chat_backend_<env>.env`)
- Harden `POST /api/user/create/` to create company + user, return JWTs, and reject when the flag is off
- Expose `GET /api/public/settings/` with `enable_account_registration` for the SPA

Supports frontend: [chat_web_app#31](ai_ml_operations/chat_web_app#31)

## Test plan
- [ ] `cd llm_be && uv run python manage.py test chat_backend.tests.test_views_users`
- [ ] Confirm `POST /api/user/create/` returns 403 when flag unset/false
- [ ] With `ENABLE_ACCOUNT_REGISTRATION=true`, register returns access/refresh + creates company
- [ ] `GET /api/public/settings/` returns the flag
- [ ] Add `ENABLE_ACCOUNT_REGISTRATION=false` to control-node `chat_backend_prod.env` / `chat_backend_beta.env` before deploy (see `.env.prod.example`)Reviewed-on: #25
2026-07-27 04:19:59 -07:00
westfarn ad44359804 Add finance app with Stripe Checkout subscriptions (#21) (#23)
Unit Tests / test (push) Successful in 10s
## Summary
- Closes #21 — new Django `finance` app with Stripe as payment provider
- Subscription price defaults to **$10 USD / month** via `SUBSCRIPTION_PRICE_AMOUNT_CENTS = 1000` in `settings.py` (env-overridable)
- Persists **Invoice** and **Payment** rows; both registered in Django admin (with payment inline on invoices)
- Checkout Session API redirects users to Stripe hosted payment; webhook verifies signatures and upserts ledger idempotently

## API
- `POST /api/finance/checkout/` — JWT auth → `{ checkout_url, session_id }`
- `GET /api/finance/invoices/` / `GET /api/finance/payments/` — own records
- `POST /api/finance/webhooks/stripe/` — Stripe signature-verified webhook

## Config
Documented in `.env.example` / `.env.prod.example`:
`STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`, `STRIPE_WEBHOOK_SECRET`, optional `STRIPE_PRICE_ID`, `FRONTEND_BASE_URL`

## Test plan
- [x] `uv run python manage.py test finance` (17 tests)
- [ ] Set Stripe test keys locally; create checkout session; complete payment in Stripe test mode
- [ ] Confirm Invoice/Payment appear in `/admin/`
- [ ] Point Stripe webhook to `/api/finance/webhooks/stripe/` and verify `checkout.session.completed` / `invoice.paid`Reviewed-on: #23
2026-07-26 17:35:06 -07:00
westfarn 9984d1c340 Name AI assistant Hesychia in system prompts (#22)
Unit Tests / test (push) Successful in 9s
## Summary
- Closes #20 — name the AI assistant **Hesychia** in system / generation prompts
- Add shared `assistant_identity.py` with `ASSISTANT_NAME` + concise calm/stillness tone
- Prepend identity to chat (`llm_service`), RAG, data analysis, and the views system message
- Document that identity lives in code (not env); add unit coverage

## Test plan
- [x] `uv run python manage.py test chat_backend.tests.test_assistant_identity chat_backend.tests.test_services_llm chat_backend.tests.test_services_data_analysis`
- [ ] Fresh chat: ask "who are you?" → responds as Hesychia
- [ ] Confirm classifiers/moderators/title generator unchanged (not assistant identity)

Related: companion frontend rebrand `chat_web_app#29`Reviewed-on: #22
2026-07-26 17:08:08 -07:00
westfarn 92aa277a37 Allow Capacitor WebView origins in CORS/CSRF (#22) (#19)
Unit Tests / test (push) Successful in 9s
## Summary
- Supports [chat_web_app#22](ai_ml_operations/chat_web_app#22)
- Always merge Capacitor WebView origins (`https://localhost`, `capacitor://localhost`, `http://localhost`) into `CORS_ALLOWED_ORIGINS` and `CSRF_TRUSTED_ORIGINS`
- Keep `CORS_ALLOW_CREDENTIALS = False` (JWT header-only; no cookie credentials)

## Test plan
- [x] `uv run python manage.py test chat_backend.tests.test_settings_cors`
- [ ] OPTIONS preflight from `https://localhost` / `capacitor://localhost` against prod/beta with `CORS_ORIGIN_ALLOW_ALL=false`
- [ ] Authenticated JWT GET/POST from Capacitor shell after FE #22 landsReviewed-on: #19
2026-07-26 14:04:09 -07:00
westfarn a049e4f685 Track token in/out per prompt on PromptMetric (#18)
Unit Tests / test (push) Successful in 9s
Closes #15

## Summary
- Add nullable `tokens_in` / `tokens_out` `IntegerField`s to `PromptMetric` to record real prompt/completion token counts per turn.
- New `extract_token_usage()` helper parses provider usage payloads (LangChain `usage_metadata`, OpenAI-style `prompt_tokens`/`completion_tokens`, Ollama `prompt_eval_count`/`eval_count`). When a provider reports no usage, the fields stay **null** — counts are never estimated/fabricated.
- `create_prompt_metric` / `finish_prompt_metric` in both `consumers.py` and `consumers_graph.py` accept and persist optional `tokens_in` / `tokens_out` (added to `update_fields` only when present).
- Admin panel (this ticket's deliverable):
  - `PromptMetricAdmin` lists `tokens_in` / `tokens_out` and adds `event` / `model_name` / `has_file` filters.
  - `ConversationAdmin` shows summed `tokens_in` / `tokens_out` / `tokens_total` per conversation.
- Migration `0023_promptmetric_tokens_in_promptmetric_tokens_out` (existing rows remain valid — null).

## Note on live capture
The streaming chat path uses LangChain `StrOutputParser`, which yields plain string chunks with no usage metadata, so live turns currently persist `null` tokens (honest, per acceptance criteria — no fabricated counts). The plumbing + helper are in place so wiring real provider usage is a drop-in once the services expose it.

## Follow-ups
- #16 — Show token in/out in chat web app UI (FE + API exposure)
- #17 — Token-based billing, quotas, and enforcement

## Test plan
- [x] `uv run python manage.py test` — full suite green (266 tests, 6 skipped)
- [x] Model: token fields default null + persist when set
- [x] `extract_token_usage`: LangChain / OpenAI / Ollama key variants, attribute sources, bool/float handling, missing usage → (None, None)
- [x] Metric lifecycle: tokens persist when provided, stay null when absent (both consumers)
- [x] Admin: conversation token totals sum across metrics and ignore other conversationsReviewed-on: #18
2026-07-26 08:22:34 -07:00
westfarn 85637e3db6 Unpin langgraph stack; upgrade langchain-core instead (#14)
Unit Tests / test (push) Successful in 9s
Closes #10

## Summary
- Remove force-pins on `langgraph==1.0.4` / `langgraph-checkpoint==3.0.1` / `langgraph-prebuilt==1.0.5` / `langgraph-sdk==0.2.14`
- Upgrade langchain stack so current langgraph-checkpoint (4.x) works with `Reviver(allowed_objects=...)`
- Keep direct `langgraph>=1.2.5,<1.3.0` (matches langchain 1.3.x); checkpoint/prebuilt/sdk resolve transitively
- Adapt `BaseMessage.text()` → `.text` property for langchain-core 1.5.x

## Resolved versions (uv.lock)
| Package | Before | After |
|---|---|---|
| langchain-core | 1.1.1 | 1.5.1 |
| langchain | 1.1.2 | 1.3.14 |
| langgraph | 1.0.4 | 1.2.9 |
| langgraph-checkpoint | 3.0.1 | 4.1.1 |
| langgraph-prebuilt | 1.0.5 | 1.1.0 |
| langgraph-sdk | 0.2.14 | 0.4.2 |

## Test plan
- [x] `uv sync --frozen`
- [x] `uv run python manage.py test` — 248 OK (6 skipped)
- [x] Import `consumers_graph` CompiledStateGraph OK
- [x] Confirm `Reviver.__init__` accepts `allowed_objects`
- [ ] Manual smoke: WebSocket chat + graph path (`consumers_graph`)

## References
- Issue: #10
- Prior pin: #9Reviewed-on: #14
2026-07-26 05:11:39 -07:00
westfarn 0525f9559b Add offline unit test suite for chat_backend (#12)
Unit Tests / test (push) Successful in 9s
Closes #5

## Summary

- Replaces the three scattered test modules (`chat_backend/tests.py`, `services/tests.py`, `services/prompt_classifier/tests.py`) with a `chat_backend/tests/` package: **242 deterministic tests plus 6 opt-in live-Ollama checks**, up from 10 tests (3 of which were skipped and 4 of which were never even discovered).
- The suite runs fully offline — no Ollama, Chroma, SMTP or network access. LangChain runnables are replaced by a small `FakeChain`, Chroma/embeddings are mocked, email uses Django's locmem backend, and blobs go through `DatabaseStorage`.
- New `llm_be/test_runner.py` (wired via `TEST_RUNNER`) sets `SKIP_RAG_INIT=1` and an MD5 password hasher, so the suite cannot accidentally reach a model server and finishes in ~5s on SQLite (~17s on Postgres) instead of ~30s.

## Coverage

| Area | File |
|------|------|
| `TimeInfoBase.save`, slugs, `get_duration`, `file_exists`, cascades | `test_models.py` |
| `DatabaseStorage` save/open/exists/size/listdir/delete/times | `test_storage.py` |
| JWT claim, prompt/user/feedback/document serializers | `test_serializers.py` |
| auth + token, invite, feedback, company users, set-password, TOS | `test_views_users.py` |
| conversation list/order/create/detail/soft-delete | `test_views_conversations.py` |
| all four analytics endpoints, including empty-month behaviour | `test_views_analytics.py` |
| workspace + document upload/list/detail, 404 and 400 paths | `test_views_documents.py` |
| prompt classifier rules/parsing, moderation fail-safe, title cleanup | `test_services_classifiers.py` |
| CSV/XLSX/DOCX/PDF analysis, plot generation, error payloads | `test_services_data_analysis.py` |
| loader selection, filename sanitising, ingest, temp-file cleanup, search filters | `test_services_rag.py` |
| history formatting and streaming | `test_services_llm.py` |
| document re-index on create/delete, `SKIP_RAG_INIT` guard | `test_signals.py` |
| consumer DB helpers, LangGraph nodes (moderation, classification, generation, search flags), websocket routes | `test_consumers.py` |

Live checks (non-deterministic, need a model server):

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

## Bugs the tests surfaced (fixed here)

1. **`ConversationDetailView.post` silently dropped every prompt.** `import datetime` shadowed `from datetime import datetime`, so `datetime.now()` raised `AttributeError` inside a bare `except` and the endpoint returned 200 without saving. Now uses `timezone.now()`.
2. **Prompt attachments never reached the LLM.** `get_conversation_file_async` (both consumers) did `sync_to_async(prompt.file.read)` — with `DatabaseStorage` the attribute access itself opens the blob, i.e. a DB query in async context, raising `SynchronousOnlyOperation` that was swallowed and returned `(None, None)`. The read now happens inside the thread.
3. **`DatabaseStorage._save` crashed on a str-backed `ContentFile`** (`TypeError: sequence item 0: expected a bytes-like object`); chunks are encoded when needed.
4. **`services/prompt_classifier/__init__.,py`** (note the comma) meant the directory was only an implicit namespace package, which is why its test module was never collected. Renamed, and its duplicate live-Ollama tests folded into `test_live_ollama.py`.

Known-broken paths deliberately left untested and unchanged: `reset_password` / `ResetUserPassword` reference an unimported `requests` plus undefined locals, and `DocumentDetailView.get` references an undefined `workspaces` on its success path. Worth a follow-up ticket.

## Test plan

- [x] `cd llm_be && uv run python manage.py test` → 248 tests, OK (6 skipped, all opt-in live)
- [x] Same suite against Postgres 16 (`DATABASE_URL=postgres://…`) → OK, matching the containerized run in `deploy.yml`
- [x] `uv run black` clean on all added files
- [ ] Gitea Actions **Unit Tests** + **CI** green on this PRReviewed-on: #12
2026-07-26 05:00:17 -07:00
westfarn 383c571137 update requirements (#9)
Unit Tests / test (push) Successful in 13s
Reviewed-on: #9
2026-07-25 19:06:16 -07:00
westfarn ad81257ecb fixing deploymennt (#8)
Unit Tests / test (push) Successful in 14s
Reviewed-on: #8
2026-07-25 06:17:46 -07:00
westfarn d1660792ad Dockerize chat_backend + Ollama LAN + DB file storage (#6) (#7)
Unit Tests / test (push) Successful in 13s
## Summary

Implements [chat_backend#6](#6) Part A:

- **uv** packaging (`pyproject.toml` + `uv.lock`), Docker/compose (dev + prod), entrypoint/validate-env, Gitea unit-test + auto-deploy workflows (mirror `scha`)
- Env-driven Django settings (`DJANGO_*`, `DATABASE_URL`, CSRF/CORS)
- **`OLLAMA_BASE_URL`** wired through all Ollama/LangChain clients (prod → `http://10.0.0.128:11434`)
- **DatabaseStorage** — prompt/document file blobs in Postgres (`StoredFile`), not container FS; RAG materializes temp paths for loaders
- ASGI via `gunicorn` + `UvicornWorker` (HTTP + WebSockets)

Companion server-infra PR registers `app_catalog` / `host_apps` (port **8003**).

## Test plan

- [ ] `uv sync && cd llm_be && SKIP_RAG_INIT=1 uv run python manage.py test`
- [ ] `docker compose build && docker compose up` against bundled Postgres
- [ ] Confirm Ollama calls use `OLLAMA_BASE_URL` (not hardcoded localhost)
- [ ] Upload a document / prompt file → row in `chat_backend_storedfile`, no disk under `media/`
- [ ] After server-infra merge + secret/Postgres/NPM: deploy via `deploy.sh --app chat_backend --env prod`Reviewed-on: #7
2026-07-25 05:23:33 -07:00
westfarn 77d7edd0dc Closes #4
Added site tracking
Can pick the model the use
Better handle llm model based on debug or not
2025-12-08 13:52:30 -06:00
westfarn eed1abedc8 updates 2025-12-07 06:31:06 -06:00
westfarn 91bdb2fd2d Merging from prod 2025-09-24 12:05:22 -05:00
westfarn 8a259158c8 Updated data analysis to generate images to perform data analysis 2025-09-24 11:49:08 -05:00
westfarn 14d8211715 Allow for data analysis 2025-09-08 12:29:20 -05:00
westfarn 951a58f2fa fixed chat service 2025-05-28 03:25:14 -05:00
westfarn a85f1222eb Syncing with updates from prod and formatted 2025-05-18 06:15:07 -05:00
westfarn d8a912e2c3 updated the requirements.txt 2025-05-14 03:28:16 -05:00
westfarn f5d29166a6 RAG implementation, content moderation, prompt classification, new LLM chain, document storage 2025-05-14 03:27:38 -05:00
westfarn 57695353d0 temp checkin 2025-03-07 12:23:00 -06:00
westfarn 058f961b0d Initial commit 2025-03-03 10:53:23 +00:00