Spaces:
Sleeping
Sleeping
Update README.md
Browse files
README.md
CHANGED
|
@@ -1,375 +1,8 @@
|
|
| 1 |
-
# ThreadHouse — Backend
|
| 2 |
-
|
| 3 |
-
A FastAPI application that powers both halves of the project: the **shop** (auth, products, orders, analytics) and the **customer-intelligence engine** (CSV upload → RFM / CLV / anomaly / insights ML pipeline). Both halves talk to the same Postgres database; the shop half uses raw `asyncpg`, the intel half uses SQLAlchemy 2.
|
| 4 |
-
|
| 5 |
---
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
-
|
| 13 |
-
- **Git** (for cloning)
|
| 14 |
-
|
| 15 |
-
### 2. Create the database
|
| 16 |
-
|
| 17 |
-
Open `psql` or pgAdmin and create an empty database called `mindless`:
|
| 18 |
-
|
| 19 |
-
```sql
|
| 20 |
-
CREATE DATABASE mindless;
|
| 21 |
-
```
|
| 22 |
-
|
| 23 |
-
You don't need to create any tables — the app auto-creates them on first boot.
|
| 24 |
-
|
| 25 |
-
### 3. Set up the Python environment
|
| 26 |
-
|
| 27 |
-
From inside the `backend/` folder:
|
| 28 |
-
|
| 29 |
-
```bash
|
| 30 |
-
# Create a virtual environment (recommended)
|
| 31 |
-
python -m venv .venv
|
| 32 |
-
|
| 33 |
-
# Activate it
|
| 34 |
-
# Windows (PowerShell):
|
| 35 |
-
.venv\Scripts\Activate.ps1
|
| 36 |
-
# Windows (cmd):
|
| 37 |
-
.venv\Scripts\activate.bat
|
| 38 |
-
# macOS / Linux:
|
| 39 |
-
source .venv/bin/activate
|
| 40 |
-
|
| 41 |
-
# Install dependencies
|
| 42 |
-
pip install -r requirements.txt
|
| 43 |
-
```
|
| 44 |
-
|
| 45 |
-
`torch` is the largest dependency (~700 MB on Windows). If your network is slow, run `pip install torch --index-url https://download.pytorch.org/whl/cpu` first to grab the smaller CPU-only build, then `pip install -r requirements.txt`.
|
| 46 |
-
|
| 47 |
-
### 4. Configure `.env`
|
| 48 |
-
|
| 49 |
-
Copy the template (or just create `.env` directly) inside `backend/`:
|
| 50 |
-
|
| 51 |
-
```
|
| 52 |
-
DB_USER=postgres
|
| 53 |
-
DB_PASSWORD=YOUR_POSTGRES_PASSWORD
|
| 54 |
-
DB_NAME=mindless
|
| 55 |
-
DB_HOST=localhost
|
| 56 |
-
DB_PORT=5432
|
| 57 |
-
DATABASE_URL=postgresql+psycopg2://postgres:YOUR_POSTGRES_PASSWORD@localhost:5432/mindless
|
| 58 |
-
|
| 59 |
-
# JWT secrets must be >= 32 chars. Generate with:
|
| 60 |
-
# python -c "import secrets; print(secrets.token_urlsafe(48))"
|
| 61 |
-
JWT_SECRET=PUT_A_LONG_RANDOM_STRING_HERE
|
| 62 |
-
SECRET_KEY=PUT_A_LONG_RANDOM_STRING_HERE
|
| 63 |
-
|
| 64 |
-
UPLOAD_DIR=uploads
|
| 65 |
-
MODEL_DIR=app/ML/artifacts
|
| 66 |
-
PRODUCTS_JSON=products.json
|
| 67 |
-
|
| 68 |
-
# Optional — only needed for /api/results/{id}/query and LLM-narrated insights.
|
| 69 |
-
GROQ_API_KEY=
|
| 70 |
-
```
|
| 71 |
-
|
| 72 |
-
The app refuses to boot if `JWT_SECRET` is shorter than 32 characters.
|
| 73 |
-
|
| 74 |
-
### 5. Run the server
|
| 75 |
-
|
| 76 |
-
```bash
|
| 77 |
-
uvicorn app.main:app --reload --port 8000
|
| 78 |
-
```
|
| 79 |
-
|
| 80 |
-
You should see:
|
| 81 |
-
|
| 82 |
-
```
|
| 83 |
-
SQLAlchemy tables ready: jobs, customer_profiles, insights.
|
| 84 |
-
asyncpg pool ready: users, orders, analytics_events, products tables verified.
|
| 85 |
-
INFO: Application startup complete.
|
| 86 |
-
```
|
| 87 |
-
|
| 88 |
-
Test it:
|
| 89 |
-
|
| 90 |
-
- API root: <http://localhost:8000>
|
| 91 |
-
- Health check: <http://localhost:8000/health>
|
| 92 |
-
- Interactive API docs: <http://localhost:8000/docs>
|
| 93 |
-
|
| 94 |
-
### 6. Create an admin user
|
| 95 |
-
|
| 96 |
-
```bash
|
| 97 |
-
python scripts/create_admin.py --email you@example.com --password StrongPass1
|
| 98 |
-
```
|
| 99 |
-
|
| 100 |
-
Use a public-TLD email — pydantic's email-validator rejects `.local` / `.test`.
|
| 101 |
-
|
| 102 |
-
You can now log into the admin panel with these credentials.
|
| 103 |
-
|
| 104 |
-
---
|
| 105 |
-
|
| 106 |
-
## How it works (architecture)
|
| 107 |
-
|
| 108 |
-
```
|
| 109 |
-
┌─────────────┐ POST /api/analytics/event ┌────────────────────┐
|
| 110 |
-
│ Shop SPA │ ────────────────────────────────► │ │
|
| 111 |
-
│ (React) │ POST /api/orders/ │ FastAPI │
|
| 112 |
-
└─────────────┘ ────────────────────────────────► │ (this folder) │
|
| 113 |
-
│ │
|
| 114 |
-
┌─────────────┐ POST /api/order/list │ │
|
| 115 |
-
│ Admin SPA │ ────────────────────────────────► │ asyncpg pool ────┐│
|
| 116 |
-
│ (React) │ WS /api/analytics/ws ◄────────►│ SQLAlchemy ────┐ ││
|
| 117 |
-
└─────────────┘ └──────────────┬──┴─┘
|
| 118 |
-
│ │
|
| 119 |
-
┌──────▼──▼──┐
|
| 120 |
-
│ Postgres │
|
| 121 |
-
│ (mindless) │
|
| 122 |
-
└────────────┘
|
| 123 |
-
```
|
| 124 |
-
|
| 125 |
-
### Two database drivers, one database
|
| 126 |
-
|
| 127 |
-
- **`asyncpg` pool** — for the shop half (auth, products, orders, analytics, audit log). Async, fast, hand-written SQL. Tables: `users`, `orders`, `analytics_events`, `products`, `audit_log`.
|
| 128 |
-
- **SQLAlchemy 2** — for the intel half (the CSV → ML pipeline). Synchronous, ORM-driven. Tables: `jobs`, `customer_profiles`, `insights`.
|
| 129 |
-
|
| 130 |
-
Both are bootstrapped in `app/main.py`'s `lifespan` handler on startup. The shop tables are created/migrated via idempotent `CREATE TABLE … IF NOT EXISTS` and `ALTER TABLE … ADD COLUMN IF NOT EXISTS`, so re-running on a populated DB is safe.
|
| 131 |
-
|
| 132 |
-
### The ML pipeline
|
| 133 |
-
|
| 134 |
-
```
|
| 135 |
-
CSV upload ──► schema_detection ──► rfm_extraction ──► segmentation
|
| 136 |
-
│
|
| 137 |
-
┌─────────────────────────┤
|
| 138 |
-
▼ ▼
|
| 139 |
-
HVR prediction CLV (BG/NBD + Gamma-Gamma)
|
| 140 |
-
│ │
|
| 141 |
-
└────────────┬────────────┘
|
| 142 |
-
▼
|
| 143 |
-
anomaly detection
|
| 144 |
-
(PyTorch autoencoder)
|
| 145 |
-
│
|
| 146 |
-
▼
|
| 147 |
-
insights (LLM)
|
| 148 |
-
│
|
| 149 |
-
▼
|
| 150 |
-
write CustomerProfile + Insight
|
| 151 |
-
mark Job complete
|
| 152 |
-
```
|
| 153 |
-
|
| 154 |
-
Each stage lives in `app/pipeline/`. The orchestrator is `app/services/ml_services.py::run_full_pipeline`, kicked off as a FastAPI `BackgroundTask` by either `POST /api/upload` (CSV) or `POST /api/intel/run-on-current-users` (live orders).
|
| 155 |
-
|
| 156 |
-
### Real-time analytics (WebSocket)
|
| 157 |
-
|
| 158 |
-
`app/live_tracking.py` is an in-process pub/sub built on `asyncio.Queue`. Every `POST /api/analytics/event` insert is followed by `publish()` which fans the event to every subscribed queue. The `/api/analytics/ws` WebSocket endpoint holds one queue per connected admin and forwards events as JSON.
|
| 159 |
-
|
| 160 |
-
⚠️ **Single-process only.** If you run multiple uvicorn workers, an event posted on worker A won't reach a WebSocket on worker B. For production, swap the queue for Redis pub/sub or NATS.
|
| 161 |
-
|
| 162 |
-
---
|
| 163 |
-
|
| 164 |
-
## File map
|
| 165 |
-
|
| 166 |
-
```
|
| 167 |
-
backend/
|
| 168 |
-
├── .env # secrets (NOT committed)
|
| 169 |
-
├── requirements.txt
|
| 170 |
-
├── app/
|
| 171 |
-
│ ├── main.py # FastAPI app, router wiring, lifespan
|
| 172 |
-
│ ├── auth_deps.py # JWT dependencies (user / admin)
|
| 173 |
-
│ ├── audit.py # audit_log helper
|
| 174 |
-
│ ├── live_tracking.py # WebSocket pub/sub
|
| 175 |
-
│ ├── core/config.py # pydantic-settings Settings
|
| 176 |
-
│ ├── db/
|
| 177 |
-
│ │ ├── session.py # SQLAlchemy engine + SessionLocal
|
| 178 |
-
│ │ ├── models.py # ORM: Job, CustomerProfile, Insight
|
| 179 |
-
│ │ └── asyncpg_pool.py# pool + DDL for shop tables
|
| 180 |
-
│ ├── routers/
|
| 181 |
-
│ │ ├── auth.py # /api/auth/{signup,login,admin/login,me,profile,logout}
|
| 182 |
-
│ │ ├── orders.py # /api/orders/ + /me
|
| 183 |
-
│ │ ├── admin_orders.py# /api/order/{list,status} (admin UI alias)
|
| 184 |
-
│ │ ├── analytics.py # /api/analytics/{event,summary,segments,live,customer/{id},ws}
|
| 185 |
-
│ │ ├── products.py # /api/product/{list,add,remove,{id},seed}
|
| 186 |
-
│ │ ├── upload.py # POST /api/upload (CSV → pipeline)
|
| 187 |
-
│ │ ├── intel_live.py # POST /api/intel/run-on-current-users
|
| 188 |
-
│ │ ├── results.py # GET /api/results/{id}/{status,overview,customers,insights,top-customers}
|
| 189 |
-
│ │ ├── query.py # POST /api/results/{id}/query (NL Q&A)
|
| 190 |
-
│ │ └── admin.py # POST /api/admin/train (retrain HVR model)
|
| 191 |
-
│ ├── schemas/
|
| 192 |
-
│ │ ├── shop.py # AnalyticsEvent, SignUpRequest, LoginRequest, AuthResponse, …
|
| 193 |
-
│ │ └── customers.py # QueryRequest, QueryResponse
|
| 194 |
-
│ ├── services/ml_services.py # run_full_pipeline orchestrator
|
| 195 |
-
│ ├── pipeline/
|
| 196 |
-
│ │ ├── schema_detection.py
|
| 197 |
-
│ │ ├── rfm_extraction.py
|
| 198 |
-
│ │ ├── segmentation.py
|
| 199 |
-
│ │ ├── clv.py
|
| 200 |
-
│ │ ├── prediction.py
|
| 201 |
-
│ │ ├── anomaly.py
|
| 202 |
-
│ │ └── insights.py
|
| 203 |
-
│ └── ML/artifacts/ # hvr_model.pkl + hvr_scaler.pkl + hvr_features.pkl
|
| 204 |
-
├── scripts/
|
| 205 |
-
│ └── create_admin.py # CLI to create/promote an admin user
|
| 206 |
-
├── static/images/ # uploaded product images (served at /static/images/...)
|
| 207 |
-
└── uploads/ # uploaded CSVs (gitignored)
|
| 208 |
-
```
|
| 209 |
-
|
| 210 |
-
---
|
| 211 |
-
|
| 212 |
-
## API reference
|
| 213 |
-
|
| 214 |
-
Open <http://localhost:8000/docs> for interactive Swagger UI. Quick summary:
|
| 215 |
-
|
| 216 |
-
### Public
|
| 217 |
-
|
| 218 |
-
| Method | Path | Purpose |
|
| 219 |
-
|---|---|---|
|
| 220 |
-
| GET | `/health` | Liveness probe |
|
| 221 |
-
| POST | `/api/auth/signup` | Register; returns JWT |
|
| 222 |
-
| POST | `/api/auth/login` | Login; returns JWT |
|
| 223 |
-
| POST | `/api/auth/admin/login` | Admin login; returns JWT (role-checked) |
|
| 224 |
-
| POST | `/api/user/admin` | Legacy alias for the admin UI |
|
| 225 |
-
| POST | `/api/analytics/event` | Ingest one analytics event (no auth — best-effort) |
|
| 226 |
-
| POST | `/api/orders/` | Place an order (optional auth — guests allowed) |
|
| 227 |
-
| GET | `/api/product/list` | Public catalogue |
|
| 228 |
-
| GET | `/api/product/{id}` | One product |
|
| 229 |
-
| POST | `/api/product/seed` | Dev importer from `products.json` |
|
| 230 |
-
|
| 231 |
-
### Authenticated user
|
| 232 |
-
|
| 233 |
-
| Method | Path | Purpose |
|
| 234 |
-
|---|---|---|
|
| 235 |
-
| GET | `/api/auth/me` | Current user from JWT |
|
| 236 |
-
| PATCH | `/api/auth/profile` | Update name / password |
|
| 237 |
-
| POST | `/api/auth/logout` | Symbolic (JWT is stateless) |
|
| 238 |
-
| GET | `/api/orders/me` | List my orders newest-first |
|
| 239 |
-
|
| 240 |
-
### Admin only
|
| 241 |
-
|
| 242 |
-
| Method | Path | Purpose |
|
| 243 |
-
|---|---|---|
|
| 244 |
-
| POST | `/api/product/add` | Multipart with up to 4 images |
|
| 245 |
-
| PATCH | `/api/product/{id}` | Edit product |
|
| 246 |
-
| POST | `/api/product/remove` | Delete product |
|
| 247 |
-
| POST | `/api/order/list` | List every order in admin shape |
|
| 248 |
-
| POST | `/api/order/status` | Update order status + audit |
|
| 249 |
-
| GET | `/api/analytics/summary` | Aggregations (totals, top pages, funnel, …) |
|
| 250 |
-
| GET | `/api/analytics/segments` | Live RFM segmentation over `orders` |
|
| 251 |
-
| GET | `/api/analytics/live?minutes=5` | Last-N-minutes snapshot |
|
| 252 |
-
| GET | `/api/analytics/customer/{user_id}` | Per-user drilldown |
|
| 253 |
-
| WS | `/api/analytics/ws?token=<JWT>` | Real-time event stream |
|
| 254 |
-
| POST | `/api/upload` | CSV upload → pipeline |
|
| 255 |
-
| POST | `/api/intel/run-on-current-users` | Build CSV from live orders → pipeline |
|
| 256 |
-
| GET | `/api/results/{job_id}/status` | Poll job status |
|
| 257 |
-
| GET | `/api/results/{job_id}/overview` | KPIs + segment distribution |
|
| 258 |
-
| GET | `/api/results/{job_id}/customers` | Filterable customer table |
|
| 259 |
-
| GET | `/api/results/{job_id}/insights` | LLM insight cards |
|
| 260 |
-
| GET | `/api/results/{job_id}/top-customers` | Top N by 12-month CLV |
|
| 261 |
-
| POST | `/api/results/{job_id}/query` | Natural-language Q&A (Groq) |
|
| 262 |
-
| POST | `/api/admin/train` | Retrain HVR model from `models/train_data.csv` |
|
| 263 |
-
|
| 264 |
-
### Auth header conventions
|
| 265 |
-
|
| 266 |
-
Most endpoints accept **either**:
|
| 267 |
-
|
| 268 |
-
```
|
| 269 |
-
Authorization: Bearer <JWT>
|
| 270 |
-
```
|
| 271 |
-
|
| 272 |
-
**or** (for the legacy admin UI):
|
| 273 |
-
|
| 274 |
-
```
|
| 275 |
-
token: <JWT>
|
| 276 |
-
```
|
| 277 |
-
|
| 278 |
-
`app/auth_deps.py::_strip_bearer` handles both.
|
| 279 |
-
|
| 280 |
-
---
|
| 281 |
-
|
| 282 |
-
## Database schema
|
| 283 |
-
|
| 284 |
-
The first time you start the server it creates these tables.
|
| 285 |
-
|
| 286 |
-
**Shop side (asyncpg, hand-written SQL):**
|
| 287 |
-
|
| 288 |
-
```
|
| 289 |
-
users(id, name, email UNIQUE, password_hash, role, created_at)
|
| 290 |
-
orders(order_id UNIQUE, user_id FK, items JSONB, delivery_info JSONB,
|
| 291 |
-
payment_method, status, total, created_at)
|
| 292 |
-
products(name, description, price, image JSONB, category, sub_category,
|
| 293 |
-
sizes JSONB, bestseller, date, stock, created_at)
|
| 294 |
-
analytics_events(session_id, user_id, event_type, page, element, value,
|
| 295 |
-
monetary_value, timestamp, created_at)
|
| 296 |
-
audit_log(actor_id, actor_email, action, target, detail JSONB, created_at)
|
| 297 |
-
```
|
| 298 |
-
|
| 299 |
-
**Intel side (SQLAlchemy ORM):**
|
| 300 |
-
|
| 301 |
-
```
|
| 302 |
-
jobs(id UUID PK, status, filename, row_count, customer_count,
|
| 303 |
-
error_message, created_at, completed_at)
|
| 304 |
-
customer_profiles(id UUID, job_id FK, customer_id,
|
| 305 |
-
recency, frequency, monetary, avg_order_value,
|
| 306 |
-
total_items, distinct_products, tenure_days,
|
| 307 |
-
avg_items_per_order,
|
| 308 |
-
r_score, f_score, m_score, segment,
|
| 309 |
-
clv_12months, clv_segment, prob_alive, predicted_purchases_90d,
|
| 310 |
-
hvr_probability, hvr_potential,
|
| 311 |
-
anomaly_score, is_anomaly, anomaly_severity, anomaly_type)
|
| 312 |
-
insights(id UUID, job_id FK, category, title, body, priority)
|
| 313 |
-
```
|
| 314 |
-
|
| 315 |
-
---
|
| 316 |
-
|
| 317 |
-
## How the ML pipeline works
|
| 318 |
-
|
| 319 |
-
The pipeline accepts any reasonably-named transactions CSV (`customer_id`, `date`, `amount`, `quantity`, `invoice_id` — exact names auto-detected by fuzzy matching).
|
| 320 |
-
|
| 321 |
-
1. **`schema_detection.py`** — `detect_schema(df)` matches your CSV's columns against the canonical names using `rapidfuzz`. Falls back to an LLM call if confidence is low.
|
| 322 |
-
2. **`rfm_extraction.py`** — groups by `CustomerID`, computes Recency, Frequency, Monetary plus `AvgOrderValue / TotalItems / DistinctProducts / TenureDays / AvgItemsPerOrder`.
|
| 323 |
-
3. **`segmentation.py`** — quintile-scores R, F, M into 1–5 with the small-sample-safe `_safe_score`. Maps `(r, f, m)` onto 11 named segments.
|
| 324 |
-
4. **`prediction.py`** — loads `hvr_model.pkl` and adds `hvr_probability` + `hvr_potential` (High/Med/Low). Silent no-op if the model file isn't there.
|
| 325 |
-
5. **`clv.py`** — BG/NBD + Gamma-Gamma from `lifetimes`. Predicts 12-month value, `prob_alive`, `predicted_purchases_90d`.
|
| 326 |
-
6. **`anomaly.py`** — small PyTorch autoencoder (8→16→8→2→8→16→8). Reconstruction MSE → anomaly score. Top quantile flagged. Type assigned rule-based (Whale / Dormant / Bot-like …).
|
| 327 |
-
7. **`insights.py`** — Groq (qwen3-32b) generates Executive Summary, Anomaly Report, Segment Spotlight. Skipped entirely if `GROQ_API_KEY` is empty.
|
| 328 |
-
|
| 329 |
-
The orchestrator (`services/ml_services.py`) writes one `CustomerProfile` row per customer and one `Insight` row per card, then marks the job `complete`.
|
| 330 |
-
|
| 331 |
-
### Retraining the HVR model
|
| 332 |
-
|
| 333 |
-
If you have a transactions CSV at `app/ML/artifacts/train_data.csv`, you can retrain:
|
| 334 |
-
|
| 335 |
-
```
|
| 336 |
-
POST /api/admin/train
|
| 337 |
-
```
|
| 338 |
-
|
| 339 |
-
The handler does a temporal split (first 8 months �� features, remainder → label = high future spend AND ≥2 future orders), engineers extra features (`monetary_per_day`, `orders_per_day`, `avg_gap`, `spend_diversity`, `basket_value`), clips outliers at the 99.9th percentile, fits a `GradientBoostingClassifier` (200 estimators, depth 3, LR 0.05, positives weighted 2×), and saves `hvr_model.pkl + hvr_scaler.pkl + hvr_features.pkl`. Test AUC is returned in the response.
|
| 340 |
-
|
| 341 |
-
---
|
| 342 |
-
|
| 343 |
-
## Troubleshooting
|
| 344 |
-
|
| 345 |
-
| Symptom | Fix |
|
| 346 |
-
|---|---|
|
| 347 |
-
| `RuntimeError: JWT_SECRET must be set to a secure value` on boot | Set `JWT_SECRET` in `.env` to a string ≥32 chars |
|
| 348 |
-
| `asyncpg.InvalidPasswordError` | `DB_PASSWORD` in `.env` doesn't match your Postgres install |
|
| 349 |
-
| `Connection refused on localhost:5432` | Postgres isn't running |
|
| 350 |
-
| `ModuleNotFoundError: No module named 'torch'` | `pip install -r requirements.txt` (torch is the big one) |
|
| 351 |
-
| `/api/intel/run-on-current-users` returns *No orders with linked user_id* | Place at least one order while logged in |
|
| 352 |
-
| `/api/results/{id}/query` returns 503 | `GROQ_API_KEY` not set in `.env` |
|
| 353 |
-
| CORS error in browser | Make sure you're hitting localhost / 127.0.0.1 — regex allows any port |
|
| 354 |
-
| WebSocket closes immediately with code 4401 | Token missing/invalid in the query string |
|
| 355 |
-
| WebSocket closes with code 4403 | User isn't an admin — run `scripts/create_admin.py --email …` |
|
| 356 |
-
|
| 357 |
-
---
|
| 358 |
-
|
| 359 |
-
## Notes for collaborators
|
| 360 |
-
|
| 361 |
-
- **Don't commit `.env`** — it's gitignored on purpose. Share secrets out of band.
|
| 362 |
-
- The `static/images/` and `uploads/` folders are created on first run. The latter is gitignored; the former contains product images uploaded via the admin panel.
|
| 363 |
-
- The OpenAPI schema at `/openapi.json` is the source of truth. Generate client SDKs from it if you build new frontends.
|
| 364 |
-
- If you're seeing a Pydantic validation error on a JSON request, check that field names match `schemas/shop.py`. The frontend sometimes sends `subCategory` but the backend uses `sub_category` in some places — `routers/products.py` normalises this for the admin form.
|
| 365 |
-
|
| 366 |
-
---
|
| 367 |
-
|
| 368 |
-
## Stack
|
| 369 |
-
|
| 370 |
-
- Python 3.11, FastAPI, Uvicorn, Pydantic v2
|
| 371 |
-
- SQLAlchemy 2 + asyncpg (dual-driver Postgres)
|
| 372 |
-
- bcrypt, PyJWT, email-validator
|
| 373 |
-
- scikit-learn, PyTorch (CPU), `lifetimes`, shap, rapidfuzz, joblib
|
| 374 |
-
- Groq SDK (`qwen/qwen3-32b`)
|
| 375 |
-
- python-dotenv, python-decouple, pydantic-settings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ThreadHouse
|
| 3 |
+
emoji: 🧵
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|