- Gemini API
- Streamlit
- Python
- Synthetic data
Cyborg RM Nudge Engine
Turns a market headline into compliance-aware, client-personalized WhatsApp drafts for relationship managers.
Open live demoProblem
Relationship managers in Indian wealth firms sit on two unsolved gaps, not one.
Reactive gap. When a market event hits — a rate move, a sector selloff, a currency swing — RMs respond hours or days late, usually with a generic broadcast. By the time a client hears from their RM, they've read three newsletters about it already. Personalizing that outreach — the right message, to the right client, tied to their actual portfolio — takes roughly 15-30 minutes per client to write by hand. Across a book of 100+ clients, that math doesn't work, so it mostly doesn't happen.
Proactive gap — the one that matters more. When a new product launches — a PMS strategy, an AIF vintage — there's no systematic way to know which clients in a book are actually suitable for it. RMs either pitch it to everyone (noisy, and a compliance risk if suitability wasn't actually checked) or don't pitch it at all (a missed conversation with clients who were genuinely a fit). This is the workflow gap RMs describe as the bigger one when asked directly, and it's the one most tools in this space don't touch.
Cyborg RM is built to close both.
Solution
Cyborg RM is a two-mode copilot, scoped to a single RM's own book — never firm-wide, mirroring how access actually works inside a wealth firm.
Mode 1 — Reactive. Paste a market headline. The engine identifies which clients in the book are genuinely exposed — by holding, sector, or asset class — and drafts a compliant, personalized outreach message for each one, referencing the specific portfolio fact driving the relevance.
Mode 2 — Proactive. Select a newly launched product category. The engine splits the book into a suitability-filtered pitch list and an explicit exclusion list — with a stated reason for every exclusion (existing overexposure, risk-profile mismatch, ticket size, a recent-contact quiet period). Excluded clients have no Draft button in the UI; a one-click path to message someone the system just excluded would make the exclusion advisory rather than real.
Both modes sit behind one navigation flow, not a flat form: select an RM → a book dashboard (aggregate AUM, allocation mix, risk-profile breakdown, a searchable client list) → select a client → a client-360 view with an "AI Suggestions" tab running both modes, scoped to that one client.
Architecture
- Dataset. 1,000 synthetic clients across 10 RM books of deliberately uneven size — UHNI desks at 55–62 clients, HNI at 88–120, emerging-affluent at 135–145 — generated by a seeded Python script (
numpy/pandas), not an LLM. An LLM can't reliably hold correlated regulatory constraints — SEBI's ₹50L PMS and ₹1Cr AIF minimums — across 1,000 rows; a script with a fixed seed can, and the run is reproducible. - Mode 1 matching is ranked, not thresholded. Each scenario scores exposure per client and returns the top N by score. An early "everyone above a threshold" version returned 88 of 120 clients on one book — technically correct, not demo-able, and not useful to an RM either. A client can genuinely match more than one scenario at once (roughly a third of the dataset does); the UI renders one card per match.
- Mode 2 suitability is
score_product_fit— deterministic rules, not a model, partitioning the book by explicit, readable criteria. More on why below.
Where the LLM Fits — and Where It Doesn't
This is the actual design decision in Cyborg RM, and it's a product judgment, not an engineering one.
The model never decides who. In Mode 1, relevance is a ranked score computed in Python — the LLM has no input into which clients are exposed to a headline. In Mode 2, suitability and exclusion both come from score_product_fit's explicit rules — the LLM has no input into who gets pitched or why someone is excluded. In both modes, an LLM API is called exactly once per client, after the decision is already made, to do one job: turn a structured fact set into a compliant, personalized sentence.
The reason isn't caution for its own sake. If a model decides suitability, the decision can't be explained, reproduced, or audited — and "we can't explain why this client was pitched" is disqualifying in wealth management, not a minor gap. Every inclusion and every exclusion in Cyborg RM traces to a rule that can be read in the source.
Compliance is checked twice, on purpose. The system prompt instructs the model on what not to say. A second, independent function then regex-checks the actual output against a literal blocklist (buy/sell/should/recommend/guarantee/%/NAV/NFO/AMC). A flagged draft is withheld entirely — never shown with a warning label. The checker is plain regex, deliberately not a second LLM call: a model checking a model's output is still just a model, and can't be the thing standing between a client and a mis-selling complaint. A literal blocklist is auditable by a human in thirty seconds.
The bug that proves the boundary matters. score_product_fit originally accepted a product_category argument and used it only for display text — the actual filtering logic never referenced it, so every product category silently returned an identical pitch/exclusion split. It ran without errors and produced plausible-looking output. I caught it because I knew what the output should look like for different product types, not because I was reading the code line by line. That's the case for keeping suitability in code a human can inspect: had that same logic lived inside a model's judgment, this wouldn't have been a bug you could find — it would have been an unfalsifiable "the model decided."
Tech Stack
- Python + Streamlit, hosted on Streamlit Community Cloud
- LLM API (Gemini, current generation) for drafting only — the drafting step is provider-agnostic by design; model name verified at deploy time, never hardcoded
- Pandas for the client book and scoring logic, on pandas 3.0 (copy-on-write by default;
"None"as a literal string is auto-coerced toNaNon read, so the dataset uses"No holding"as a real sentinel instead) - Secrets via
st.secrets— same variable name, same code path, on both the local machine and Streamlit Cloud, so there's noif local: … else: …branch that configuration can silently drift across - Visual theme pulled directly from the website's own design tokens (background, surface, brand-blue, card radius), so the Streamlit app and the Next.js site read as one system despite sharing no codebase
Business Impact
Reactive gap closed: response latency to a market event drops from "whenever the RM gets to it" to as long as it takes to paste a headline — personalized, per client, not a broadcast.
Proactive gap closed: product pitching moves from "everyone or no one" to a suitability-filtered list with an auditable reason attached to every exclusion. That's the difference between a marketing blast and something a compliance team can actually stand behind — and it's the gap RMs name as the more valuable one to fix.
Every draft an RM sends still carries a specific, checkable reason — the client's own portfolio fact, or their own suitability profile. Nothing is generic, and nothing skips the compliance layer.
This would help RM reach more clients and eventually convert more business. At the very least, RM will have more strong relationship with the client
Lessons Learned
Domain. The proactive pitching gap is the more valuable problem to solve — RMs volunteer this before the reactive one when asked. Mode 2 exists because of that, not because it was in the original spec.
Product. Access boundaries are architectural, not cosmetic. RM visibility into any client's holdings — including unlisted ones — is client-scoped, not firm-wide, and that constraint reshaped the data model and the navigation mid-build, not just the UI copy.
AI judgment. The Mode 2 bug is the clearest lesson in the whole build: deterministic, readable suitability logic is what made a silent, plausible-looking defect catchable at all. The same failure inside a model's judgment would have been invisible.
Engineering. Three separate bugs — Streamlit Cloud secrets not traveling with the repo, a widget-key collision when a client matched more than one scenario, and a session-state caching bug where a redrafted message in a new language silently kept showing the old text — were all the same root cause wearing different clothes: configuration or identity that lives outside the code doesn't travel with the code by default. That's now the first thing I check when something works locally and breaks in production.
Future Improvements
- Draft sentence-quality refinement
- IPO scenario scoring by absolute rupee value rather than allocation percentage
- A wider, region-correlated language set (Tamil, Telugu, Kannada) in the underlying dataset