wealth-alpha-chat-widget 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,357 +0,0 @@
1
- # Chat Widget Backend — Design & Frontend Integration Guide
2
-
3
- > **Scope of this doc**: covers only the two files I authored for the chat widget:
4
- > - `app/api_v1/chat_widget.py` — the widget's HTTP router (chip + multi-step + proxy)
5
- > - `app/api_v1/chat_formatters.py` — converts upstream JSON into chat-bubble text
6
- >
7
- > The team-authored files (`chat.py` for persistence, `intent.py`, `ama.py`) are **out of scope** here.
8
-
9
- ---
10
-
11
- ## 1. What problem does `chat_widget.py` solve?
12
-
13
- The npm library (`wealth-alpha-chat`) speaks one tiny protocol:
14
-
15
- ```
16
- GET /me → who is logged in?
17
- POST /chip/{chip_id} → user clicked a chip
18
- POST /message → user typed text
19
- ```
20
-
21
- Each response is a `BotResponse`:
22
-
23
- ```jsonc
24
- {
25
- "message": "text to show in the bubble",
26
- "chips": [{ "id": "...", "label": "...", "icon": "..." }], // next chips
27
- "sessionId": "ses_...",
28
- "endOfFlow": false,
29
- "metadata": { "endpoint": "/api/v1/...", "inputs": { ... } }
30
- }
31
- ```
32
-
33
- But your **business endpoints** (the 20 upstream telegram-api routes) speak a completely different language — they need specific query params (`symbol`, `entry_price`, `side`, `category`, `stance`, `market_range`, …) and return structured templates (`LONG_TERM`, `SWING`, `CRYPTO_ANALYSIS`, …).
34
-
35
- `chat_widget.py` is the **adapter** between those two worlds.
36
-
37
- ```
38
- Browser (npm lib)
39
- │ POST /chat-widget/chip/act_long_term {sessionId:"abc"}
40
-
41
- chat_widget.py
42
- │ chip "act_long_term" → prompt user for symbol
43
- ▼ returns {message: "Type ticker", chips: [Back], awaiting: "symbol"}
44
- Browser shows prompt
45
-
46
- User types RELIANCE
47
- │ POST /chat-widget/message {message:"RELIANCE", sessionId:"abc"}
48
-
49
- chat_widget.py
50
- │ session.awaiting=symbol → fill collected["symbol"]="RELIANCE"
51
- │ all fields collected → call upstream
52
- ▼ httpx GET /api/v1/stock-analysis/long-term_analysis?symbol=RELIANCE
53
- upstream returns JSON {template: "LONG_TERM", price_snapshot: {...}, signal: {...}, ...}
54
-
55
- ▼ chat_formatters.format_payload(payload)
56
- │ dispatches by `template` → format_long_term() → multi-line text with ₹, •, secti.ons
57
- ▼ returns BotResponse {message: "📈 LONG-TERM\n...\n*SIGNAL*\n...", chips: [Back], endOfFlow: true}
58
- Browser shows formatted result in chat bubble
59
- ```
60
-
61
- ---
62
-
63
- ## 2. The three building blocks of `chat_widget.py`
64
-
65
- ### A. Chip tree (`ROOT_CHIPS`, `NAV_TREE`, `ACTIONS`, `DIRECT_CALLS`)
66
-
67
- A chip is one of three kinds:
68
-
69
- | Kind | Data structure | Behavior on click |
70
- |---|---|---|
71
- | **nav** | entry in `NAV_TREE` | Returns next-level chips. No upstream call. (e.g. `stock_analysis` → 6 analysis-type chips) |
72
- | **action** | entry in `ACTIONS` | Starts a multi-step flow: prompts the user for one field at a time, then fires the upstream call. (e.g. `act_position_summary` asks ticker → entry price → LONG/SHORT) |
73
- | **direct** | entry in `DIRECT_CALLS` | One click → immediately fires the upstream call with bundled params. (e.g. `macro_monetary` → `GET /market-forecast/macro?category=MONETARY`) |
74
-
75
- Special chip IDs:
76
- - `__root__` — return to the main 8-chip menu, clear pending state
77
- - `__set:FIELD=VALUE` — a chip that fills the currently-awaited field (e.g. `__set:side=LONG`)
78
-
79
- ### B. Multi-step session store (`_SessionStore`)
80
-
81
- In-memory dict keyed by `sessionId`. Each entry:
82
-
83
- ```python
84
- {
85
- "action": "act_position_summary",
86
- "collected": {"symbol": "BPCL", "entry_price": 310.0},
87
- "awaiting": "side",
88
- "updated_at": 1716800000.0,
89
- }
90
- ```
91
-
92
- TTL is 30 minutes (matches the frontend session). Each `/chip` or `/message` request either:
93
- - starts a new action (creates the entry with `collected={}`, `awaiting=fields[0].name`)
94
- - advances an action (fills `collected[awaiting]`, sets `awaiting` to next missing field)
95
- - fires the call (when all fields collected → `httpx` → format → response)
96
-
97
- > ⚠️ **Production note**: the dict lives in process memory. For multi-instance deploys, swap `_SessionStore` for a Redis-backed implementation. The interface (`get`, `set`, `clear`) is small enough that this is a 1-hour swap.
98
-
99
- ### C. Upstream proxy (`_call_upstream`)
100
-
101
- Async `httpx.AsyncClient` that:
102
- - Re-uses the **caller's** `Authorization` header → upstream sees the same JWT
103
- - Derives base URL from the incoming `Request` → works under any host/scheme without config
104
- - Maps upstream HTTP errors into clean `{"_error": "..."}` payloads:
105
- - `401`/`403` → "auth failed / premium required"
106
- - `404` → "no data for these inputs"
107
- - `5xx` → "try again shortly"
108
-
109
- The formatter (next section) checks for `_error` first and returns it as-is, so error text shows up in the chat bubble.
110
-
111
- ---
112
-
113
- ## 3. How `chat_formatters.py` works
114
-
115
- One Python function per template. Each turns a JSON payload into bubble-friendly text using `•` bullets, `*headings*`, `₹` / `$`, `+/−` percentages.
116
-
117
- ### Dispatcher
118
-
119
- ```python
120
- _TEMPLATE_MAP = {
121
- "LONG_TERM": format_long_term,
122
- "LONG_TERM_DETAILED": format_long_term_detailed,
123
- "SWING": format_swing,
124
- "INTRADAY": format_intraday,
125
- "POSITION_SUMMARY": format_position_summary,
126
- "POSITION_DETAILED": format_position_detailed,
127
- "PORTFOLIO_CONSTRUCT": format_portfolio_construct,
128
- "EXISTING_PORTFOLIO": format_existing_portfolio,
129
- "PORTFOLIO_REBALANCE": format_portfolio_rebalance,
130
- "MACRO_EVENT_UPDATE": format_macro_event,
131
- "MACRO_OVERVIEW": format_macro_event,
132
- "CRYPTO_ANALYSIS": format_crypto_analysis,
133
- "CRYPTO_LIST": format_crypto_list,
134
- "CRYPTO_LIST_ITEM": format_crypto_list_item,
135
- "SMART_MONEY_PICKS": format_smart_money_picks,
136
- "POTENTIAL_STARS": format_potential_stars,
137
- "EXIT_WATCH": format_exit_watch,
138
- "PEER_COMPARISON": format_peer_comparison,
139
- }
140
-
141
- def format_payload(payload, fallback_prefix=""):
142
- # 1. error short-circuit
143
- if isinstance(payload, dict) and "_error" in payload:
144
- return payload["_error"]
145
- # 2. shape detection for sector-news / discovery / IPO (no template field)
146
- # 3. template dispatch via _TEMPLATE_MAP
147
- # 4. unknown → compact JSON dump
148
- ```
149
-
150
- ### Helper toolbox
151
-
152
- Shared building blocks all formatters use:
153
- - `_money_rs(value)` → `₹1,450.25`
154
- - `_money_usd(value)` → `$2,369.41`
155
- - `_pct(value)` → `+1.23%` / `-4.47%`
156
- - `_bullet_list(items, limit=10)` → `• item1\n• item2\n• … and 5 more`
157
- - `_section(title, body)` → `\n*TITLE*\nbody`
158
- - `_format_signal()`, `_format_invalidation()`, `_format_business_health()`, `_format_indicators()`, `_format_peers()` — composable sub-blocks reused across LONG_TERM, SWING, POSITION_*, etc.
159
-
160
- ### Why backend-side formatting
161
-
162
- | | Format on backend | Format on frontend |
163
- |---|---|---|
164
- | Iterate text without rebuilding library | ✅ | ❌ npm publish/install loop |
165
- | Bundle size | Smaller (no 16 formatters in JS) | Larger |
166
- | Reusable for Telegram bot | ✅ same Python code | ❌ would need a JS port |
167
- | Compliance redaction (omit fields) | ✅ at the boundary | ❌ raw data already in browser |
168
- | Trade-off: structured rich UI (cards) later | Need to switch to passing raw JSON | Already raw on client |
169
-
170
- The current `BotResponse.message` is plain text. `BotResponse.metadata` includes `endpoint` and `inputs` for tracing/debugging — and if you later want structured rendering, you can move the raw payload there.
171
-
172
- ---
173
-
174
- ## 4. The 20 upstream endpoints — exact mapping
175
-
176
- Every chip ID lives in one of these dicts (in `chat_widget.py`):
177
-
178
- ### `ACTIONS` (multi-step — collect inputs then fire)
179
-
180
- | Chip ID | Upstream | Fields collected |
181
- |---|---|---|
182
- | `act_long_term` | `GET /api/v1/stock-analysis/long-term_analysis` | `symbol` |
183
- | `act_long_term_detail` | `GET /api/v1/stock-analysis/long-term_detail_analysis` | `symbol` |
184
- | `act_swing_trade` | `GET /api/v1/stock-analysis/swing_trade_analysis` | `symbol` |
185
- | `act_intraday` | `GET /api/v1/stock-analysis/intraday_analysis` | `symbol` |
186
- | `act_position_summary` | `GET /api/v1/stock-analysis/existing_investor_position_analysis` | `symbol`, `entry_price`, `side` |
187
- | `act_position_detail` | `GET /api/v1/stock-analysis/existing_investor_position_analysis_detailed` | `symbol`, `entry_price`, `side` |
188
- | `act_portfolio_construct` | `POST /api/v1/portfolio-construct/construct` (body) | `capital`, `risk_port_pct` |
189
- | `act_portfolio_analyze` | `GET /api/v1/portfolio-existing/analyze` | `portfolio_id` |
190
- | `act_portfolio_rebalance` | `GET /api/v1/portfolio-existing/rebalance` | `portfolio_id` |
191
- | `act_sector_news` | `GET /api/v1/market-forecast/sector-news` | `sector` |
192
- | `act_crypto_analyze` | `GET /api/v1/crypto/analyze` | `symbol` |
193
- | `act_crypto_coin` | `GET /api/v1/crypto/coin` | `symbol` |
194
- | `act_peer_compare` | `GET /api/v1/peer-analysis` | `symbol` |
195
-
196
- ### `DIRECT_CALLS` (one click — params bundled)
197
-
198
- | Chip ID | Upstream | Bundled params |
199
- |---|---|---|
200
- | `macro_{monetary,economic,markets,policy,global}` | `GET /market-forecast/macro` | `category=…` |
201
- | `crypto_list_{investible,watchlist,avoid}` | `GET /crypto/crypto_list` | `stance=…` |
202
- | `smart_money_{large,mid,small}` | `GET /tg-tradable-candidate/smart-money-picks` | `market_range=…` |
203
- | `stars_{large,mid,small}` | `GET /tg-tradable-candidate/potential-stars` | `market_range=…` |
204
- | `exit_{large,mid,small}` | `GET /tg-tradable-candidate/exit-watch` | `market_range=…` |
205
- | `discover_{large,mid,small}` | `GET /stock-ranks/discovery/stocks` | `sector=all, market_range=…, timeframe=D` |
206
- | `ipo_{large,mid,small}` | `GET /stock-ranks/discovery/ipo-stocks` | `sector=all, market_range=…` |
207
-
208
- ---
209
-
210
- ## 5. How the frontend library plugs into this
211
-
212
- ```tsx
213
- <WealthChat
214
- apiBase="http://localhost:8013/api/v1/chat-widget" // ← the chat router prefix
215
- authCheck="/me" // → GET {apiBase}/me
216
- loginUrl="/login"
217
- sessionTTL={1800} // 30 min sliding
218
- brandName="Wealth Alpha AI"
219
- brandColor="#1a2d5a"
220
- position="bottom-right"
221
- />
222
- ```
223
-
224
- The library appends `/me`, `/chip/{id}`, `/message` to `apiBase`. **Do not include `/chat/` in apiBase or it doubles up** (was the recent 404 cause).
225
-
226
- ### Token discovery (no bridge required)
227
-
228
- `readSession()` looks for a JWT in this order:
229
- 1. `localStorage["wac_session"].token` (full session — written by the library itself, or by the host app for custom flows)
230
- 2. `localStorage["token"]` (your app's normal login key)
231
- 3. `localStorage["access_token"]`, `localStorage["auth_token"]`, `localStorage["jwt"]` (other common keys)
232
-
233
- If a raw JWT is found, the library decodes its `exp` and `id` claims, builds a session on the fly, and persists it as `wac_session` for subsequent reads. On logout (the fallback key is removed), the library clears `wac_session` automatically — no host-side sync code needed.
234
-
235
- ---
236
-
237
- ## 6. Three ways to improve / extend on the library side
238
-
239
- Pick one based on how much surface area you want to give the host app. Each is independent and can be done without touching the backend.
240
-
241
- ### Option A — Render structured cards instead of plain text (highest visual impact)
242
-
243
- Today the bubble shows `BotResponse.message` as `pre-wrap` text. Cards would render:
244
-
245
- ```
246
- ┌─ RELIANCE — Reliance Industries Ltd ────────┐
247
- │ Energy · As of 27 May 2026 │
248
- │ Price ₹1,450.25 +1.23% │
249
- │ 52W high ₹1,600 (-9.36%) low ₹1,100 (+32%) │
250
- │ ─── SIGNAL ───────────────────────────── │
251
- │ [BUY] Long · Entry ₹1,455 · Stop ₹1,390 │
252
- │ T1 ₹1,550 (+6.5%) R/R 1:2.1 │
253
- └─────────────────────────────────────────────┘
254
- ```
255
-
256
- **What to change:**
257
- - Backend: stop formatting in `chat_formatters.py` — put the raw payload in `BotResponse.metadata.payload`
258
- - Library: add a `<TemplateCard template={metadata.template} data={metadata.payload} />` component, with sub-components per template (`<LongTermCard>`, `<SwingCard>`, `<PositionCard>`, …)
259
- - Trade-off: ~16 React components to write + maintain. Library bundle ~2× larger. But the UI is dramatically better.
260
-
261
- ### Option B — Streaming token-by-token responses (best perceived speed)
262
-
263
- When you add the AI Research Analyst (LLM-powered free-text), responses can take 5–10 s. Streaming makes them feel instant.
264
-
265
- **What to change:**
266
- - Backend: add `/chat-widget/stream` SSE endpoint that yields chunks
267
- - Library: open an `EventSource` instead of `fetch`, append tokens to the active bubble as they arrive
268
- - Trade-off: ~150 lines of new code; requires CORS to allow `text/event-stream`; harder to retry on disconnect
269
-
270
- ### Option C — Pluggable token / chat-tree config (most reusable across projects)
271
-
272
- Make the library generic enough to drop into any host app or any backend.
273
-
274
- **What to change:**
275
- - Add props: `getToken?: () => string | null`, `tokenStorageKey?: string`, `welcomeBuilder?: (user) => string`
276
- - Add prop: `chipTree?: ChipTree` — let the host app override the root chips and labels without touching the npm package
277
- - Trade-off: more API surface; documentation effort
278
-
279
- ### Honorable mention — Persist chat history server-side
280
-
281
- The team's new `chat.py` already exposes `/api/v1/chat/conversation` and `/api/v1/chat/session/{id}`. Have the library `POST /chat/conversation` after each turn so the conversation is recoverable across devices, not just `localStorage`. ~50 lines, big UX win.
282
-
283
- ---
284
-
285
- ## 7. Where each piece lives — file map
286
-
287
- ```
288
- WealthAlpha-Backend/
289
- app/api_v1/
290
- chat_widget.py ← chip router, multi-step state, upstream proxy
291
- chat_formatters.py ← 18 templates → text
292
- router.py ← includes chat_widget at prefix "/chat-widget"
293
- app/utils/auth.py ← get_current_user dep (reused, untouched)
294
-
295
- Wealth-alpha-chat-UI/ (the npm library)
296
- src/
297
- components/WealthChat.tsx ← entry component, props, welcome
298
- components/AuthGate.tsx ← login required panel + disclaimer
299
- components/ChatBody.tsx ← scrolling message list
300
- components/MessageBubble.tsx ← single message + chips
301
- api/chatApi.ts ← fetch wrapper (timeout, retry, auth, abort)
302
- hooks/useAuth.ts ← /me check
303
- hooks/useChat.ts ← message state + sendText
304
- hooks/useChip.ts ← chip click → sendChip
305
- hooks/useSession.ts ← TTL, sliding window, storage events
306
- utils/session.ts ← localStorage adapter (auto-discovers JWT)
307
-
308
- WealthAlpha-Frontend/ (consumer)
309
- src/app/layout.tsx ← <WealthChat apiBase="…/chat-widget" />
310
- .env.local ← NEXT_PUBLIC_API_BASE, NEXT_PUBLIC_CHAT_DEMO
311
- ```
312
-
313
- ---
314
-
315
- ## 8. The 4 reliable commands while developing
316
-
317
- ```bash
318
- # Backend reload (when chat_widget.py or chat_formatters.py changes)
319
- cd WealthAlpha-Backend && source venv/bin/activate
320
- uvicorn main:app --reload --port 8013
321
-
322
- # Library rebuild & ship (when src/ changes)
323
- cd Wealth-alpha-chat-UI
324
- npm run typecheck && npm run build && npm pack
325
-
326
- # Frontend pickup
327
- cd ../WealthAlpha-Frontend
328
- npm install ../Wealth-alpha-chat-UI/wealth-alpha-chat-0.1.0.tgz --force
329
- rm -rf .next && npm run dev
330
-
331
- # Smoke-test from terminal (no UI needed)
332
- TOKEN=$(curl -s -X POST http://localhost:8013/api/v1/auth/login \
333
- -H 'Content-Type: application/json' \
334
- -d '{"email":"you@example.com","password":"…"}' | jq -r '.data')
335
-
336
- curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8013/api/v1/chat-widget/me
337
- curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
338
- -X POST http://localhost:8013/api/v1/chat-widget/chip/stock_analysis \
339
- -d '{"chipId":"stock_analysis","sessionId":"test"}'
340
- ```
341
-
342
- ---
343
-
344
- ## 9. Common failures & exact fixes
345
-
346
- | Symptom | Cause | Fix |
347
- |---|---|---|
348
- | `404 /chat-widget/chat/chip/…` (path doubled) | `apiBase` ended with `/chat-widget` AND library prefixed `/chat/` | Fixed in chatApi.ts — now uses `/chip/{id}` & `/message` |
349
- | `401 Unauthorized` on `/chat-widget/me` | Library couldn't find a JWT, OR JWT expired | Library now reads `localStorage["token"]` as fallback. Or re-login to refresh the JWT. |
350
- | Chat shows AuthGate even when logged in | `wac_session` has stale token, host's `token` key not detected | `localStorage.removeItem("wac_session"); location.reload();` |
351
- | `Upstream /api/v1/… is unreachable` | The 14 telegram-api routes not registered in `router.py` | Already fixed — they're included at the bottom of router.py |
352
- | `503` or `httpx.SSL` after deploy | Reverse proxy strips `X-Forwarded-Proto` | `uvicorn main:app --proxy-headers --forwarded-allow-ips='*'` |
353
- | Chip click does nothing | Chip ID not in `ROOT_CHIPS`/`NAV_TREE`/`ACTIONS`/`DIRECT_CALLS` | Check `chat_widget.py` — add the entry, restart backend |
354
-
355
- ---
356
-
357
- *Doc version: 1.0 — kept beside the npm library for easy reference. Update when the chip tree or formatter list changes.*