Fastwhisper / docs /BACKEND.md
Mbonea's picture
Deploy Habit Journal backend S0-S10 to Hugging Face Space.
990895d
|
Raw
History Blame Contribute Delete
11.2 kB
# Backend Design Document
## Habit Journal (Discrete PWA) — Hugging Face Spaces
**Version:** 2.0.0
**Scope:** Backend API, storage, auth, server-side statistics, coach, traces,
debugging, export, and operations. The frontend is separate.
This is the repository specification. If implementation and this document
conflict, this document wins until a human changes it.
## Implementation rules
- Work in the vertical slices listed below and keep each diff small.
- Prefer stdlib, FastAPI, httpx, Pydantic, and pydantic-settings.
- Use no SQL database, Redis, Celery, DI framework, or repository hierarchy.
- Every module has a short purpose docstring and public functions are typed.
- Request and response bodies use Pydantic models.
- Secrets come only from environment variables. Never log passwords or keys.
- Keep generic Habit Journal naming and private biography out of git.
- Disable OpenAPI, Swagger, and ReDoc when `ENV=prod`.
- After each slice, the app must import and start with uvicorn.
- Target at most about 15 Python files under `app/`; merge stores if needed.
## Runtime contract
On a fresh Hugging Face Docker Space with `APP_PASSWORD` and
`APP_SECRET_KEY`, startup creates `/data/loop_logger`, login works, entries and
daily rows survive restart, statistics are computed in Python, coach always
returns model or backup text, and every debug paste bundle is understandable
without prior user context.
The Docker image uses Python 3.11 slim and runs:
```text
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}
```
Required and optional environment:
- `APP_PASSWORD`: bootstrap password when `config.json` does not exist
- `APP_SECRET_KEY`: required session-signing secret
- `DATA_ROOT=/data/loop_logger`
- `ENV=prod`; `APP_NAME=Habit Journal`
- `OPENROUTER_API_KEY`; `OPENROUTER_MODEL=openrouter/free`
- `OPENROUTER_BASE_URL=https://openrouter.ai/api/v1`
- `COACH_TIMEOUT_SEC=25`; `COACH_TEMPERATURE=0.3`
- `COACH_MAX_TOKENS=400`; `COACH_HISTORY_K=10`
- `COACH_TRACE_LIMIT=200`
- `MIN_STATS_N=5`; `STATS_SHRINK_K=3`; `MATCH_ALPHA=0.5`
- `SESSION_MAX_AGE_SEC=604800`
- `DEBUG_INCLUDE_FULL_BRIEF=false`
- `LOGIN_RATE_LIMIT=10`; `LOGIN_RATE_WINDOW_SEC=900`
## Flat architecture and durable files
Routers call services, services call stores, and stores access the filesystem.
Do not introduce circular imports. `main.py` owns wiring.
`DATA_ROOT` contains `config.json`, `entries.jsonl`, `daily.jsonl`,
`coach_brief.md`, and `traces.jsonl`. Each mutated path uses a lock. Entry
creation appends one flushed JSONL line under lock. Updates and deletes read,
map/filter, then write a flushed temporary file and `os.replace` it.
If configuration is absent and `APP_PASSWORD` exists, create:
```json
{
"password_hash": "pbkdf2_sha256$260000$...",
"session_version": 1,
"created_at": "ISO-8601",
"updated_at": "ISO-8601"
}
```
PBKDF2 uses SHA-256, at least 200,000 iterations, a random salt, and
`hmac.compare_digest`. Without config and bootstrap password, readiness is
false and protected data routes return `setup_required`.
`coach_brief.md` is operator-editable, read to at most 32 KiB, and seeded with
generic policy only. Debug includes its hash and first 500 characters unless
`DEBUG_INCLUDE_FULL_BRIEF=true`.
## API envelope and security
Every success is `{"ok":true,"data":...,"error":null}`. Every failure is
`{"ok":false,"data":null,"error":{"code":"...","message":"..."}}`.
Codes are `unauthorized`, `forbidden`, `validation_error`, `not_found`,
`setup_required`, `rate_limited`, and `internal`.
Use Starlette signed cookie sessions containing `auth`, `sv`, and Unix `exp`.
Reject expired sessions and sessions whose version differs from config.
Cookies are HTTP-only, SameSite Lax, and secure in production. Rate-limit
login attempts by IP in memory and return generic credential failures.
Limit request bodies to 64 KiB. Do not mount `DATA_ROOT`. Best-effort strip
emails and long digit runs from coach prompts. Never return stack traces.
Unauthenticated routes are only `GET /api/health`, `GET /api/ready`,
`POST /api/auth/login`, and `GET /api/auth/me`.
Auth routes:
- `POST /api/auth/login` with `{password}`
- `POST /api/auth/logout`
- `GET /api/auth/me`
- `POST /api/auth/change-password` with old/new password; bump session version
## Entries and daily rows
Entry fields are `id`, `ts`, timestamps, `activity`, `happened`, `emotions`,
`intensity`, `remedy`, `result`, `tags`, `notes`, and nested coach metadata.
Activity is 1–500 characters, happened 1–4000, emotions at most 12,
intensity 1–10, remedy and notes at most 2000, and tags at most 20. Normalize
emotions to lowercase stripped text; normalize tags likewise and replace
spaces with underscores. Results are `worked`, `partial`, `failed`, or
`pending`; pending never enters probability denominators.
Entries support create, list/filter, get, patch, delete, and per-entry coach.
List filters are start/end date, tag, result, emotion, substring `q`, limit
(50 default, 200 max), and offset; sort newest first.
Daily rows are unique by date and contain brick, brick completion, corn
sessions, delay flag, daydream, rerun, court, points, note, and update time.
Enums are brick `A|B|C|D|E|S|none`, daydream `none|done|fc`, rerun `clean|R`,
and court `closed|court`. Corn sessions range from 0 to 50.
Server-authoritative daily points:
```text
2 if brick_done
+1 if corn_sessions == 0 or (corn_sessions <= 1 and delay_ok)
+1 if daydream != "fc"
+1 if rerun == "clean"
+1 if court == "closed"
```
For a seven-calendar-day window, missing days produce `incomplete`; otherwise
28–42 is `strong`, 18–27 `mixed`, and 0–17 `escape_heavy`.
## Server statistics and evidence
`stats_math.py` contains pure functions only. Excluding pending outcomes:
- `p_worked(r) = N(worked,r) / N(r)`
- `p_helped(r) = N(worked|partial,r) / N(r)`
- `p_failed(t) = N(failed,t) / N(t)`
- `rank(r) = p_helped(r) * n/(n+k)`
- `match(r,T) = |T intersect helped_tags(r)| / max(|T|,1)`
- `pick(r) = rank(r) * (1 + alpha*match(r,T))`
Intensity buckets are 1–3, 4–6, and 7–10. Emotions are multi-label.
Top coach tables hide remedies below `min_n`. Server picks are the top five by
pick score. `DATA_THIN` means fewer than ten scored entries.
Daily statistics include brick done, corn okay, no FC, clean rerun, closed
court, and average points rates. `/api/stats` returns counts, outcomes,
breakdowns, daily rates, formula strings, and generation time.
`/api/stats/remedies` returns rank-sorted remedies. The model never computes
statistics.
## Coach
Coach is an optional OpenRouter selector/copywriter over server evidence and
server picks. A hardcoded ordered rule table is mandatory and always available.
A valid coach request always returns HTTP 200 with non-empty text and source
`model`, `backup`, or `model_unparsed_fallback`.
Pipeline: resolve current input; load last K truncated entries and today's
daily row; compute evidence and picks; build system/user messages; start a
trace; call OpenRouter; strictly parse the template; fall back on empty,
failed, timed-out, or unparseable output; set flags; append/ring-trim trace;
optionally persist coach metadata on the entry.
OpenRouter receives `POST {BASE}/chat/completions`, Bearer authorization,
JSON content type, `X-Title: Habit Journal`, configured model, messages,
temperature, token limit, and timeout.
Required model output:
```text
LOOP: <tag>
FEELING: <word>
INTENSITY_GUESS: <1-10|n/a>
REMEDY: <one concrete action>
NEXT_BRICK: <A|B|C|D|E|S|specific>
DO_NOT: <one thing>
LINE: <12 words or fewer>
NOTE: <optional 20 words or fewer>
DATA_THIN: <true|false>
```
The prompt forbids invented numbers, canceling committed processes merely from
fear, pity, and pep talks. Self-harm language triggers crisis redirection.
Rules are ordered: crisis, court, rerun, rage_movie, daydream/fc, urge/corn,
bully, shame, spain/admin, build/earn, home, loneliness, default. Non-crisis
rules may substitute server pick number one.
Flags include `DATA_THIN`, `PARSE_FAIL`, `TIMEOUT`, `EMPTY_MODEL`,
`NO_API_KEY`, and `OUT_OF_EVIDENCE`.
## Traces, debug, export, and operations
Every coach call stores request, current situation, truncated history,
evidence, picks, brief hash/excerpt/full value according to config, both
prompts, model settings, latency/status/error, raw output, parse, source,
backup rule, flags, app version, and final text. Keep the newest configured N.
Authenticated debug endpoints list traces, return full traces, return one or
the newest context-free Markdown paste bundle, preview prompts without a model
call, and test coach with optional forced backup. The paste bundle contains:
how an external AI should help, authoritative formulas, evidence, picks,
current input, history, prompts, raw response, parse/source/flags, final text,
and a reviewer checklist for invented numbers, ignored picks, format,
fallback quality, evidence length, and weight tuning.
Authenticated exports provide entries JSONL, entries CSV, and daily JSONL.
CSV has a fixed entry header and pipe-joins emotions and tags.
Health is public. Readiness reports writable data plus config presence.
Authenticated settings status reports app name, coach configured state, model,
data status, environment, and app version—never an API key.
Mount `./static` at `/` only when present, after `/api` routes. Never mount the
data directory.
## Default generic policy
- A: admin/critical checklist item
- B: environment/hygiene one act
- C: build/earn one ship
- D: logistics one line
- E: boundary one line then silence
- S: stop spiral — water/shower/sleep
Court is closed by default. Daydream requires a same-day brick. Urge policy is
delay then brick, with a ceiling of one session per day expressed only in
policy text.
## Ordered implementation slices
1. S0: Dockerfile, requirements, settings, paths, fs utilities, health/ready
2. S1: password hashing, config store, auth, session, dependencies
3. S2: entries CRUD JSONL
4. S3: daily upsert, points, range band
5. S4: pure statistics and stats routes
6. S5: evidence and server picks
7. S6: rule backup and offline coach
8. S7: OpenRouter, parser, flags
9. S8: traces and debug paste endpoints
10. S9: export and settings status
11. S10: seed brief, production checks, error polish
Do not begin S7 before S6. Tests must cover daily point edge cases,
hand-calculated helped/rank math, and backup rule precedence; an authenticated
temporary-data API test is optional.
## Acceptance
Storage survives restart; good login sets a cookie and bad login is 401;
unauthenticated data access is 401; daily and probability fixtures match;
coach without a key and failed model calls return backup text; evidence and
debug paste contain probabilities and server picks; paste contains formulas;
authenticated export works; production OpenAPI is absent; and no secret is
committed.
Explicit decisions are final: JSONL only, server-only math, probabilities and
picks passed to the model, hardcoded backup, context-free debug bundles,
single shared password, entry result as truth, and private life details only
in the operator brief on disk.