A system that acts first — watches time, patterns, and live signals, and surfaces a one-tap suggestion when a ride or food order is likely. Built for the "Proactive Assistant" assignment.
- Backend — NestJS + TypeScript + Prisma + SQLite, with six internal modules: connection, adapters, memory, trigger, suggestions, debug.
- Real adapters — Uber (GraphQL) and Zomato (REST), both using session cookies captured via Playwright.
- Mock adapters — Ola, Rapido, Swiggy (and Uber/Zomato fallbacks), so the full multi-platform architecture is demonstrable with or without real logins.
- Cookie vault — AES-256-GCM encrypted cookie storage with an auto-generated per-machine key.
- Playwright login flow — headful browser launches, user signs in normally, cookies are captured and encrypted.
- Frontend — React + Vite + TypeScript + Tailwind; polls the backend every 3 s.
- Seed data — 3 months of realistic fake history for a single demo user (so the UI has something to show before any real connection).
- Debug panel — time-travel + live-signal injection so flows can be reproduced without waiting for real-world events.
Suggestion card — urgent ride with traffic delay applied

Connection status + debug panel (time-travel and live signals)

Edit flow — tweak any field before confirming

Prerequisites: Node.js 20+, npm.
# 1. Install
cd backend && npm install
npx playwright install chromium
cd ../frontend && npm install
# 2. Initialise DB and seed
cd ../backend
npx prisma migrate dev --name init
npx ts-node src/seed/seed.ts # 3 months of fake history
# 3. Start backend + frontend (two terminals)
npx nest start # backend on :4000
# in another terminal:
cd frontend && npm run dev # UI on :5173Open http://localhost:5173. The Vite dev server proxies /api/* to the backend on :4000.
┌─ Frontend (React, polls /api) ───────────────────────────┐
│ ConnectAccount • SuggestionCard • EditModal │
│ PatternsView • DebugPanel • RecentActivity │
└───────────────────────────────┬──────────────────────────┘
│
┌─ Backend (NestJS) ─────────────▼──────────────────────────┐
│ │
│ ConnectionModule │
│ ├─ ConnectionManagerService (Playwright headful) │
│ └─ CookieVaultService (AES-256-GCM) │
│ │
│ AdaptersModule │
│ ├─ UberAdapter (REAL, GraphQL) │
│ ├─ ZomatoAdapter (REAL, REST) │
│ ├─ Mock{Uber,Ola,Rapido,Swiggy,Zomato}Adapter │
│ ├─ CircuitBreakerService (3 fails → 5-min skip) │
│ └─ AdapterRegistry (real if healthy, else mock) │
│ │
│ MemoryModule (Ingestion → PatternLearner → Feedback) │
│ └─ exponential decay (30-day half-life) │
│ │
│ TriggerModule (pure TriggerEngine + @Cron every 30 s) │
│ │
│ SuggestionsModule (Builder + StateMachine + REST) │
│ │
│ DebugModule (time-travel, signal injection, Uber-down) │
│ │
│ Prisma → SQLite (data/app.db) │
└───────────────────────────────────────────────────────────┘
1. Trigger logic — TriggerEngineService.shouldTrigger() is a pure function (no DB/HTTP), fully unit-tested. It applies, in order:
- Day-of-week + time-window match
- Confidence threshold (default 0.25)
- Dismissal penalty:
confidence × 0.8^(dismissals in last 7 days) - Already-acted suppression (user placed a ride/order in the last 2 h → no trigger)
- Cooldown (default 30 min per pattern)
- Daily limit per type (default 4)
- Urgency elevation when traffic ≥10 min over baseline or delivery delay ≥10 min
2. Memory — two-tier storage so a better algorithm doesn't lose data:
RawEvent(append-only, source of truth)Pattern(derived, recomputed from raw events)Feedback(confirm/dismiss/edit log, fed back into future triggers)
Pattern learner uses exponential recency decay with a 30-day half-life:
weight = exp(-ln(2) · daysAgo / 30). Confidence for a (dayOfWeek, time-bucket, target) group is the ratio of its weight sum to the total weight of events in that same time bucket.
3. Real-world data integration — every adapter implements DataSource. Adapters are wrapped in CircuitBreakerService (3 failures → 5-min skip). AdapterRegistry routes every call to the real adapter if its healthCheck() passes (connection active with cookies), otherwise it transparently falls back to the mock. SuggestionBuilderService adds a second tier of fallback: live → LiveDataCache → graceful empty. Stale data surfaces with a visible "Live data unavailable" badge in the UI. Schema changes are caught by SchemaChangedError from the adapter — it never returns broken data upstream.
4. UI — every suggestion card shows a "Why:" line (time + frequency + signal reason). Confirm / Edit / Dismiss are all one-tap. StateMachineService enforces valid transitions (shown → confirmed|dismissed|expired) and every action is logged to Feedback for the learning loop. Failure states (stale data, expired cookies) are always visible to the user.
- User clicks Connect next to Uber or Zomato in the UI.
ConnectionManagerServicelaunches a visible Chromium window (Playwright,headless: false).- User signs into the platform normally (email + OTP — nothing new to learn).
- A background watcher polls the page URL; when it matches the post-login signature and the required cookies are present, the cookies are encrypted with AES-256-GCM using the vault key and stored in
PlatformConnection. - The browser closes automatically. The UI shows the platform as connected.
- Clicking Fetch history ingests real history through the real adapter.
- Every live call checks connection health; if the server ever returns 401/403, the connection is marked
expiredand the UI prompts the user to reconnect.
If cookies are missing or expired, AdapterRegistry.get() silently returns the mock adapter instead — nothing breaks, suggestions still surface, and the "Connected" indicator in the UI reflects the true state.
| Component | State |
|---|---|
| Connection + cookie capture (Playwright) | Real |
| Cookie encryption (AES-256-GCM) | Real |
| Uber adapter (GraphQL) | Real — hits riders.uber.com/graphql with captured cookies |
| Zomato adapter (REST) | Real — hits zomato.com/webroutes/user/orders with captured cookies |
| Ola / Rapido / Swiggy adapters | Mock (Ola/Rapido live APIs are optional in the assignment; Swiggy is included as a mock alongside the real Zomato) |
| Pattern learning, trigger engine, memory | Real logic |
| Circuit breaker, state machine, feedback loop | Real |
| Live ETA/price/surge | Both adapters return a real healthCheck() gate, but the Estimate body itself is best-effort (Uber/Zomato's public estimate endpoints are ephemeral) — see "Honest caveats" below |
- The Uber GraphQL
Activitiesquery uses a reasonable shape based on community-documented reverse-engineering (see sources). If Uber changes the schema,SchemaChangedErrorfires and the mock transparently takes over. - The Zomato order-history endpoint parser walks several possible nestings (
sections.SECTION_USER_ORDER_HISTORY,entities.ORDER,page_data.sections...) because the exact shape changes across builds. Again, schema issues fall through to the mock. - Live fares and ETAs are best-effort values — real-time pricing APIs are behind additional flows that differ between rider/driver/guest and aren't needed to demonstrate the assignment's trigger logic. The architecture isolates this in one method per adapter, so a production replacement plugs in without touching anything else.
- Single user — hardcoded
demo-usereverywhere. Multi-user auth is not implemented (thePlatformConnectiontable is keyed onuserIdalready, so the slot is there). - Time zone — everything runs in local machine time. Time-travel uses ISO strings.
- Scheduler cadence —
@Cronfires every 30 s (demo-friendly). Flip toEVERY_5_MINUTESfor production. - Booking is not executed — "Confirmed" is a visual state, not a real API call (per assignment).
- Vault key —
data/vault.keyis auto-generated on first run, stored with 0600 permissions, and gitignored.
| Endpoint | What it does |
|---|---|
GET /api/connections |
All platforms + connection status |
POST /api/connections/:p/start |
Launch Playwright login for a platform |
POST /api/connections/:p/disconnect |
Forget cookies for a platform |
POST /api/memory/ingest |
Fetch history via the current adapter (real or mock fallback) |
GET /api/memory/patterns |
Learned patterns (for transparency) |
POST /api/memory/recompute |
Re-derive patterns from raw events |
GET /api/suggestions/active |
Currently shown suggestions |
POST /api/suggestions/:id/confirm |
One-tap confirm |
POST /api/suggestions/:id/edit-confirm |
Edit + confirm in one call |
POST /api/suggestions/:id/dismiss |
Dismiss (penalty applies) |
POST /api/debug/time |
{ iso } — set virtual time (null resets) |
POST /api/debug/signals |
{ rideEtaDeltaMin, deliveryDelayMin } |
POST /api/debug/uber-down |
{ down: true/false } — simulate adapter failure |
POST /api/debug/tick |
Run one trigger evaluation immediately |
POST /api/debug/reset-suggestions |
Clear suggestions + feedback |
cd backend
npx jest # 7 trigger engine tests, all passingTests cover: time match, miss, already-acted, cooldown, urgency boost, dismissal math, confidence threshold.
pokus/
├── ass.txt # original assignment
├── README.md
├── backend/
│ ├── prisma/schema.prisma
│ └── src/
│ ├── connection/ # Playwright headful + AES-256-GCM vault
│ ├── adapters/
│ │ ├── uber/ # Real GraphQL adapter
│ │ ├── zomato/ # Real REST adapter
│ │ ├── mock/ # Mock for every platform
│ │ ├── base/ # DataSource + CircuitBreaker
│ │ └── adapter-registry.service.ts
│ ├── memory/ # Ingestion, PatternLearner (decay), Feedback
│ ├── trigger/ # Pure TriggerEngine + @Cron scheduler
│ ├── suggestions/ # Builder, StateMachine, REST controllers
│ ├── debug/ # Time-travel + signal injection for demo
│ └── seed/seed.ts # 3-month fake history
├── frontend/src/
│ ├── components/
│ │ ├── ConnectAccount.tsx # Real connection UI
│ │ ├── SuggestionCard.tsx
│ │ ├── EditModal.tsx
│ │ ├── PatternsView.tsx
│ │ ├── DebugPanel.tsx
│ │ └── RecentActivity.tsx
│ ├── hooks/ # useSuggestions, useDebugState
│ ├── api/client.ts # Typed REST client
│ └── App.tsx
└── data/
├── app.db # SQLite (generated)
└── vault.key # AES key (generated, gitignored)
- Multi-user auth — single hardcoded user (
demo-user) throughout. - Live deployment — localhost only. Deployment was marked optional in the assignment.
- WebSocket push — frontend polls every 3 s. Equivalent UX for a demo, simpler wire.
- Production-grade Uber/Zomato price APIs — the live
Estimatebody is best-effort; the health gate and architecture are correct.
- Uber hidden API documentation: https://dev.to/nrrb/how-i-hacked-ubers-hidden-api-to-download-4379-rides-35ai
- Zomato order-history parsing: https://medium.com/@srivastavahardik/how-i-reverse-engineered-the-zomato-app-to-build-my-own-order-tracking-notification-system-22289a68dcb2
- Swiggy unofficial API spec (for context, though we don't use it): https://captnemo.in/swiggy-unofficial-api-specs/
