From approval to adoption, every HCP interaction has to count.
Commercial teams must convert clinical evidence, approved content, market-access context and omnichannel engagement signals into one timely, compliant and explainable decision.
An end-to-end machine-learning system that determines which HCP to engage, what action to recommend, which approved content to use, through which channel and at what time — and measures whether the action created incremental value.
End-to-end machine-learning system · architecture specification
The objective is not more commercial activity. It is better allocation of commercial effort.
Where commercial value is lost
Speed to adoption
Once a therapy is approved, commercial teams have a limited window to establish awareness, communicate the evidence, address access barriers and build appropriate adoption. Slow or poorly coordinated execution weakens launch momentum.
Lost launch velocity
Relevance at scale
HCP profiles, content, consent, access, campaign and engagement data sit across disconnected systems. The right HCP can therefore receive the wrong message, through the wrong channel, at the wrong point in the journey.
Fragmented decision-making
Commercial efficiency
Field capacity, marketing budgets and HCP attention are finite. Every unnecessary visit, repeated email or underused content asset consumes budget and increases fatigue and opt-out risk.
Wasted commercial effort
One connected decision for every planning cycle
HCP×Action×Approved Content×Channel×Time Window
Inputs
HCP profile
Commercial context
Approved content
Access barriers
Consent
Channel history
Recent intent
Field capacity
Decision process
Retrieve
Rank
Adjust for business utility
Enforce eligibility
Explain
Measure incrementality
Optimisation objective
Expected incremental value
− fatigue
− opt-out risk
− operational cost
Mandatory constraint
Approval, effective date, segment, channel permission, consent and contact-frequency compliance.
This is not another campaign dashboard. It is a decision system for allocating constrained commercial resources to the interactions most likely to create incremental value.
Domain terminology used in the architecture
HCP
Healthcare professional — the prescriber this system recommends to.
HCO
Healthcare organisation — the hospital, clinic or practice an HCP is affiliated to.
KeyMessage
The smallest approved unit of content: a single claim, a safety block, one data figure.
ContentModule
A group of key messages approved to appear together.
Campaign
The campaign-level container that delivers content modules through a channel.
MLR
Medical, legal and regulatory review. Approves content and sets its expiry date. Authoritative — the platform reads its decisions and cannot override them.
NBA
Next-best-action — the decision this platform produces.
Eligibility
The deterministic decision that an HCP–action–content–channel–window combination satisfies approval, effective date, segment, channel permission, consent and contact-frequency rules, and is therefore servable. Distinct from relevance, and evaluated separately.
Approval statesApprovedInReviewExpired — only Approved is servable
From commercial problem to operating boundary
Before a model can recommend an action, the platform must know which systems provide the evidence, who owns the objective and which decisions it is never permitted to override.
One governed decision loop from approved content to HCP engagement
The platform sits between content governance, commercial strategy, field execution and HCP response. It does not decide what is allowed, and it does not replace the field team — it turns fragmented signals into one ranked, eligible and explainable action.
What matters is not more outreach — it is coordinated, compliant and measurable commercial action.
Who controls what
MLR & Governance
Controls what is permitted
Commercial Leadership
Controls what is optimised
Machine Learning Platform
Controls what is prioritised inside the eligible action set
Field Representative
Controls what is executed in the customer interaction
Why this matters
01
Reduces fragmented channel planning.
02
Improves allocation of field and marketing effort.
03
Creates a measurable feedback loop from action to incremental impact.
Key takeaway. The platform does not author content, override approval or replace the field team. It coordinates approved evidence, business goals and current HCP context into one auditable recommendation.
From operating model to decision contract
Now that the system boundary, ownership and feedback loop are clear, the next question is: what exactly is the unit of decision this platform produces?
View 02L0 · Decision
The six questions resolve into one recommendation
Six questions, one scored tuple. Building them as separate models is how a channel decision ends up contradicting a content decision.
Question 6 is not an afterthought — eligibility_proof is emitted by the policy layer, not reconstructed later by an LLM.
View 03Data
Dataset, schema and feature construction
Scale is set by what each downstream method needs to be statistically meaningful: enough interactions per HCP for sequence modelling, enough randomisation units for cluster-randomised experiments, and enough treated/control contrast for uplift estimation.
Leakage discipline. All splits are temporal, never random. Features are computed as of the event timestamp through point-in-time joins, and a dedicated leakage test asserts that no feature in a training row was observable only after that row's label.
View 04L2 · Graph
HCP-360 knowledge graph schema
Three retrieval paths the feature tables cannot serve: content lineage for asset-level attribution, affiliation for cold-start, and barrier matching for the highest-intent recommendation in the system.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart LR
HCP(["HealthcareProfessional"])
HCO(["HCO"])
SEG(["HCPSegmentation"])
REP(["Representative"])
TER(["Territory"])
ENG(["Engagement"])
TAC(["Campaign"])
MOD(["ContentModule"])
ATOM(["KeyMessage"])
BRAND(["Brand"])
CH(["Channel"])
TOP(["Topic"])
BAR(["AccessBarrier"])
EVT(["Event"])
HCP -->|"HCP_AFFILIATED_WITH_HCO"| HCO
HCP -->|"hasSegment"| SEG
HCP -->|"HCP_SIMILAR_TO_HCP"| HCP
HCP -->|"HCP_ATTENDED_EVENT"| EVT
HCP -->|"HCP_FACES_BARRIER"| BAR
ENG -->|"isDeliveredTo"| HCP
TAC -->|"Delivers"| ENG
TAC -->|"isComprisedOf"| MOD
MOD -->|"isComprisedOf"| ATOM
TAC -->|"promotes"| BRAND
ENG -->|"hasChannel"| CH
ATOM -->|"describes"| TOP
ATOM -->|"CONTENT_ADDRESSES_BARRIER"| BAR
REP -->|"REP_COVERS_TERRITORY"| TER
HCP -->|"located in"| TER
classDef semantic fill:#FAEDDD,stroke:#A85D08,stroke-width:1.5px,color:#6B3B05;
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
class HCP,SEG,ENG,TAC,MOD,ATOM,BRAND,CH semantic;
class HCO,REP,TER,TOP,BAR,EVT model;
Amber nodes are the core content and engagement ontology. Violet nodes support retrieval, territory planning and barrier-aware content matching.
Controlled vocabularies — taken verbatim from the ontology - 3 rows
Transparent baseline; the thing every other method must beat
Low
Fully — path is the reason
Personalised PageRank
Rank graph-near content and similar HCPs
Medium
Yes — via path weight
Node2Vec
Structural HCP and content embeddings
Medium
Weakly
GraphSAGE
Learns from node attributes plus neighbourhood; handles new nodes
High
Limited — neighbour contribution analysis
LightGCN
Collaborative signal propagated over the interaction graph
High
Weakly
Heterogeneous graph transformer
Cross-entity recommendation over typed edges
Highest
Via metapath attention
View 05L1 · Platform
End-to-end architecture
Two representations of one truth — a feature store for point lookups, a graph for traversal — feeding a single linear decision path.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart LR
subgraph SRC["Sources"]
direction TB
s1["Content systems Modular Content Authoring · MLR Workflow Digital Asset Management · Campaign Taxonomy"]
s2["Delivery systems Field CRM (detail + broadcast) Marketing Automation · Third-party media"]
s3["Commercial warehouse Commercial Data Warehouse"]
s4["Consent & suppression Consent Management · Suppression Service"]
end
subgraph LAKE["Lakehouse"]
direction TB
l1["Lake · Raw zone append-only events"]
l2["Lake · Refined zone conformed entities"]
l3["Spark / Delta Hive-partitioned point-in-time joins"]
end
subgraph FEAT["Feature & semantic layer"]
direction TB
f1["Offline feature store time-aware training sets"]
f2["Online store Feast / Redis"]
f3["HCP-360 knowledge graph Graph DB · SPARQL"]
f4["ANN index FAISS / OpenSearch"]
end
subgraph CORE["Decision core"]
direction TB
c1["Candidate generation 5 retrievers"]
c2["Learning-to-rank LambdaMART"]
c3["Sequence rescorer SASRec / BERT4Rec"]
c4["Bandit action selector Thompson / LinUCB"]
c5["Policy & eligibility filter approval · consent · frequency"]
end
subgraph SERVE["Serving"]
direction TB
v1["Recommendation API FastAPI"]
v2["Pre-call copilot Graph-RAG explanation"]
v3["Control centre dashboard"]
end
s1 -->|"~3K assets/qtr"| l1
s2 -->|"~500K events/day"| l1
s3 -->|"nightly, 2.1 TB"| l2
s4 -->|"consent deltas"| l2
l1 --> l3
l2 --> l3
l3 -->|"180M feature rows/night"| f1
l3 -->|"+40K edges/day"| f3
f1 -->|"500K HCP keys"| f2
f1 -->|"40K x 128-d"| f4
f3 --> c1
f4 --> c1
f2 -->|"340 features, 12ms"| c2
c1 -->|"~100 candidates"| c2 --> c3 --> c4 --> c5
c5 -->|"2,000 QPS · P95 180ms"| v1
c5 --> v2
c5 --> v3
v1 -->|"delivered action"| s2
s2 -.->|"engagement events - async, hours to days"| l1
s2 -.->|"real-time stream - 4.5K/s peak"| l3
classDef infra fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.5px,color:#0A464C;
classDef semantic fill:#FAEDDD,stroke:#A85D08,stroke-width:1.5px,color:#6B3B05;
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
class s1,s2,s3,s4,l1,l2 infra;
class f3 semantic;
class l3,f1,f2,f4,c1,c2,c3,c4,c5,v1,v2,v3 model;
Teal = data and serving infrastructure · amber = the semantic layer · violet = learned decision components. The dotted return edge is asynchronous — engagement outcomes land hours to days later.
View 06L2 · Retrieval
Candidate generation — five retrievers, one union
Recall is a ceiling — ranking cannot recover what retrieval missed. Five retrievers with different failure modes emit channel-specific tuples; each is measured on its own Recall@50 so a silently broken retriever is visible rather than absorbed.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12.5px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart LR
hcp["HCP request hcp_id + context"]
r1["Policy-aware baseline coarse eligibility pre-filter segment · brand · indication"]
r2["Collaborative filtering item-item on engagement ALS / implicit"]
r3["Two-tower ANN HCP tower x content tower FAISS top-200"]
r4["Graph neighbours personalised PageRank metapath traversal"]
r5["Segment popularity cold-start fallback EarlyAdopter/GrowthPotential/Established"]
u["Union + dedupe candidate_key = hcp_id + action_type + content_id + channel + send_window"]
cap["Cap to 100 candidates proportional per source"]
hcp --> r1
hcp --> r2
hcp --> r3
hcp --> r4
hcp --> r5
r1 --> u
r2 --> u
r3 --> u
r4 --> u
r5 --> u
u --> cap
cap --> nxt["to ranking"]
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
classDef io fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.5px,color:#0A464C;
class r1,r2,r3,r4,r5 model;
class hcp,u,cap,nxt io;
Retriever
Covers
Fails on
Metric
Policy-aware baseline
Coarse eligibility pre-filter, full transparency
No personalisation signal
Coverage %
Collaborative filtering
Dense HCPs with long engagement history
Cold-start HCPs and new content
Recall@50
Two-tower ANN
Semantic match on content and HCP need
Needs retraining as content churns
Recall@100
Graph neighbours
Affiliation, institution, similar-HCP paths
Sparse subgraphs, orphan nodes
Cold-start Recall@50
Segment popularity
Never returns empty — the floor
Popularity bias, low diversity
Diversity / coverage
View 07Serving · Lifecycle
Serving: request lifecycle and in-session adaptation
One request end to end, and how the features it reads are kept fresh between requests.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
sequenceDiagram
autonumber
participant CRM as Field CRM
participant API as Recommendation API
participant FS as Online store
participant R as Retrieval tier
participant M as Ranking models
participant P as Policy gate
CRM->>API: GET /recommendations?hcp_id
API->>FS: fetch batch + session features
FS-->>API: 340 features · 12 ms
API->>R: fan out to 5 retrievers in parallel
R-->>API: ~100 candidate tuples · 45 ms
API->>M: score tuples
M-->>API: LambdaMART 100 · 18 ms
M-->>API: sequence rescore top 20 · 55 ms
API->>API: utility adjust + bandit select · 5 ms
API->>P: evaluate eligibility
P-->>API: pass or suppress + proof · 9 ms
API-->>CRM: top K + rationale · P95 180 ms
Latency budget — allocated targets per stage
Full latency budget - 11 rows
Stage
P50
P95
Notes
Request parse and auth
1 ms
2 ms
Feature fetch — online store
4 ms
12 ms
Batch + session features in one round trip
Candidate generation — 5 parallel
14 ms
45 ms
Bounded by graph traversal, the slowest retriever
Union, dedupe, proportional cap
1 ms
3 ms
LambdaMART — 100 candidates
7 ms
18 ms
Scales linearly in candidate count
Sequence rescorer — top 20 only
12 ms
55 ms
Dominant cost; bounded by rescoring the head, not all 100
Utility adjustment
<1 ms
1 ms
Arithmetic on business coefficients
Bandit arm selection
2 ms
4 ms
Posterior sample over 7 arms
Policy gate — 6 rules
3 ms
9 ms
Short-circuit; most candidates fail rule 1
Response assembly and rationale
2 ms
5 ms
Total
45 ms
180 ms
Against a 250 ms budget
In-session adaptation - how the features that request reads stay current
The streaming path changes exactly one thing: which features the ranker reads at request time. It retrains nothing.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12.5px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart LR
ev["CRM & web events open · click · dwell · page view"]
k["Kafka topic: hcp.engagement.v1"]
ss["Spark Structured Streaming sessionise by hcp_id 10-min watermark"]
fu["Session feature updater recent topics · channel · time gaps"]
on["Online store Feast / Redis"]
api["Rescoring API"]
hist["Delta history replay + training"]
ev --> k --> ss --> fu --> on --> api
ss --> hist
api -->|"refreshed ranked list"| ui["Rep worklist / web surface"]
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
classDef io fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.5px,color:#0A464C;
class k,ss,fu,on,api model;
class ev,hist,ui io;
Worked example. Initial recommendation: general educational content. New event arrives — the HCP opens an access-related page and searches reimbursement terms. Session features update within the watermark; the next request retrieves access-resource content modules and a field-access follow-up rises to rank 1.
Failure modes and degradation
Component fails
Detected by
Behaviour
User impact
Session feature stream
Consumer lag alert > 60 s
API proceeds on batch features only
Slightly less responsive to in-session intent
ANN index unavailable
Per-source Recall@50 drops to zero
Four remaining retrievers serve
Reduced semantic coverage, list still full
Graph database timeout
Retriever P95 breach
Retriever drops out at 60 ms deadline
Loses relational and cold-start paths
Sequence rescorer
Stage latency breach
Falls back to LambdaMART ordering
Loses short-term intent signal
Bandit service
Health check
Falls back to highest-prior channel
Exploration pauses, exploitation continues
Policy gate
Health check
Request fails closed — no recommendation served
Empty worklist
Online store
Read error rate
Reuse last-known-good scores, then re-run the authoritative gate against current approval, consent and contact state. If policy state is also unavailable, fail closed
Ordering may be stale; eligibility never is
Ranking quality degrades gracefully; eligibility does not degrade at all. A cached score is acceptable, a cached eligibility decision is not — content expires, consent is withdrawn and frequency limits are reached between requests.
View 08L2 · Ranking
Ranking and the policy cascade
Retrieval emits channel-specific tuples, so channel is scored rather than assigned afterwards. Exploration enters as a bonus term on an already-eligible tuple — it never reassigns a component of a scored candidate. The authoritative eligibility gate runs last and is absolute.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12.5px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart TB
cands["~100 channel-specific tuples from retrieval"] --> ltr
ltr["Stage 1 · LightGBM LambdaMART ranks full tuples HCP x action x content x channel meaningful-engagement relevance labels objective: NDCG@K"]
seq["Stage 2 · Sequence rescorer SASRec over recent interaction sequence blends short-term intent"]
util["Stage 3 · Utility and exploration score + exploration bonus − fatigue − opt-out risk − cost"]
ban["Stage 4 · Final ordering argmax over eligible tuples channel is carried, never reassigned"]
ltr --> seq --> util --> ban --> pol
subgraph POL["Stage 5 · Policy gate — absolute, non-overridable"]
direction TB
p1["approval_status = Approved"]
p2["effective_date <= today < expiry_date"]
p3["segment_eligibility contains HCP segment"]
p4["channel permitted for content type"]
p5["consent present · not opted out"]
p6["contact frequency within limit"]
end
pol["eligibility evaluation"] --> p1 --> p2 --> p3 --> p4 --> p5 --> p6
p6 --> out["Final ranked list top K + suppression log"]
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
classDef policy fill:#FAEDDD,stroke:#A85D08,stroke-width:1.6px,color:#6B3B05;
classDef io fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.5px,color:#0A464C;
class ltr,seq,util,ban model;
class p1,p2,p3,p4,p5,p6,pol policy;
class cands,out io;
Every suppression records the full candidate tuple, the rule violated, the policy version and the evaluation timestamp — {recommendation_id, hcp_id, action_type, content_id, channel, send_window, rule_violated, policy_version, evaluated_at}. Suppression compliance is a first-class metric, not an exception path.
View 09L2 · Validation
Validation strategy — four tracks, each against its own baseline
These components solve different problems and are not rungs on one ladder. A retriever cannot beat a ranker: one is measured on recall, the other on ordering. Each track has its own baseline, its own metric, and its own promotion rule.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart LR
subgraph T1["Retrieval track - Recall@50/100, cold-start recall, coverage"]
direction LR
a0["Segment popularity"] --> a1["Collaborative filtering"] --> a2["Two-tower retrieval"] --> a3["Graph-hybrid retrieval"]
end
subgraph T2["Ranking track - NDCG@10, MAP@K, calibration"]
direction LR
b0["Rules score"] --> b1["Propensity model"] --> b2["LambdaMART"] --> b3["Sequence rescoring"]
end
subgraph T3["Online decision track - cumulative reward, regret, opt-out rate"]
direction LR
c0["Fixed channel policy"] --> c1["Epsilon-greedy"] --> c2["LinUCB"] --> c3["Thompson sampling"]
end
subgraph T4["Causal track - Qini, uplift@K, incremental per 1K HCPs"]
direction LR
d0["Propensity targeting"] --> d1["T-learner"] --> d2["X-learner"] --> d3["Causal forest"]
end
classDef io fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.5px,color:#0A464C;
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
class a0,b0,c0,d0 io;
class a1,a2,a3,b1,b2,b3,c1,c2,c3,d1,d2,d3 model;
Teal is the incumbent baseline for each track — what the business does today, or the simplest thing that works. Promotion requires beating that baseline on the track's own metric, on the same temporal split, with bootstrap confidence intervals over HCPs.
Track
Question it answers
Baseline to beat
Primary metric
Promotion rule
Retrieval
Does the right action reach the ranker at all?
Segment popularity
Recall@100 · cold-start Recall@50
Raises the recall ceiling enough to justify an index rebuild
Ranking
Is the ordering right?
Rules score
NDCG@10 · MAP@K
Beats current practice, and calibration does not degrade
Online decision
Which channel, under uncertainty?
Fixed channel policy
Cumulative reward · regret
Lower regret without raising opt-out rate
Causal
Did the action cause the outcome?
Propensity targeting
Qini · uplift@K
Higher incremental engagement per 1,000 HCPs
The rule is "beat the right baseline", not "beat the previous model". Scoped adoption is a valid outcome — if two-tower retrieval wins only on cold-start HCPs, it serves cold-start candidates and nothing more. What does not happen is a global claim from a segment win.
View 10L2 · Online
Online learning — exploration inside the eligible set
Offline learning can only learn from actions that were taken, which leaves channel preference confounded by historical policy. Exploration is the only mechanism that breaks that — and it runs strictly inside the eligible action space.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12.5px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart LR
ctx["Context HCP features · current sequence channel · time · territory content category · contact frequency"]
pol["Policy Thompson sampling posterior over arm reward"]
arms["Arms email content A · email content B rep follow-up · event invitation web resource · no action"]
obs["Observed outcome"]
rew["Reward"]
upd["Posterior update"]
ctx --> pol --> arms --> obs --> rew --> upd --> pol
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
classDef io fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.5px,color:#0A464C;
class pol,arms,upd model;
class ctx,obs,rew io;
Reward function
Outcome
Reward
Why weighted this way
Email open
+0.1
Weak signal, easily inflated by preview panes
Click
+0.3
Intent, but not yet value
Meaningful content engagement
+0.6
Dwell past threshold — the real objective
Follow-up accepted
+1.0
Terminal success for the action
Opt-out
−1.0
Loses the channel permanently
No action taken
0.0
A real arm with a real outcome — restraint is learnable
Exploration is bounded to the eligible set. Arms that would breach approval, consent, channel permission or contact frequency are removed before the policy samples, so a compliance breach is not a low-reward outcome the bandit learns to avoid — it is unreachable. What the policy does learn from is engagement, dwell, follow-up acceptance, fatigue and opt-out.
A propensity model asks who is likely to respond. An uplift model asks who responds because we acted. Targeting the first produces a system that takes credit for engagement that would have happened anyway — and, worse, keeps contacting sleeping dogs.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12.5px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart TB
subgraph DES["Experiment design"]
direction LR
d1["HCP-level randomisation when no rep spillover"]
d2["Cluster RCT by territory when reps serve many HCPs"]
d3["Switchback by time block when policy is global"]
end
DES --> run["Run: personalised policy vs rules / standard segmentation"]
run --> outc["Outcomes engagement · acceptance fatigue · opt-out rate"]
outc --> up["Uplift model T-learner · X-learner · causal forest"]
up --> g1["Persuadables respond only if contacted → TARGET THESE"]
up --> g2["Sure things respond regardless → do not spend contact"]
up --> g3["Lost causes never respond → do not spend contact"]
up --> g4["Sleeping dogs contact harms engagement → actively suppress"]
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
classDef infra fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.8px,color:#0A464C;
classDef policy fill:#FAEDDD,stroke:#A85D08,stroke-width:1.6px,color:#6B3B05;
class d1,d2,d3,run,outc,up model;
class g1 infra;
class g4 policy;
class g2,g3 model;
Reported on Qini curve, uplift@K, and incremental engagement per 1,000 HCPs — never on raw response rate in the treated group.
View 12L2 · GenAI
Compliant pre-call copilot — where the LLM is and is not allowed
This is the single most important boundary in the architecture. The LLM does not rank, does not select, and does not decide eligibility. It receives an already-ranked, already-filtered list plus the approved evidence, and writes the explanation. Saying "I used an LLM to recommend content" describes a system that cannot pass MLR review.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12.5px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart TB
q["Rep question 'best approved follow-up for this HCP given recent access-content activity?'"]
subgraph DET["Deterministic zone — ML and rules decide"]
direction TB
a1["1 · Retrieve HCP history graph traversal"]
a2["2 · Generate candidates five retrievers"]
a3["3 · Rank LTR + sequence rescorer"]
a4["4 · Filter approval · consent · recency · frequency"]
a5["5 · Retrieve supporting evidence hybrid vector + metadata + graph"]
end
subgraph GEN["Generative zone — language only"]
direction TB
b1["6 · Explain the ranked list in natural language"]
b2["7 · Cite key message IDs and the feature signals used"]
end
q --> a1 --> a2 --> a3 --> a4 --> a5 --> b1 --> b2 --> out["Answer with citations"]
note["Boundary rule the LLM never reorders, never adds a candidate, never overrides an eligibility decision"]
GEN -.- note
classDef infra fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.6px,color:#0A464C;
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.6px,color:#2E2470;
classDef policy fill:#FAEDDD,stroke:#A85D08,stroke-width:1.8px,color:#6B3B05;
class a1,a2,a3,a4,a5 infra;
class b1,b2,out model;
class note policy;
Evaluated on factuality, citation correctness, groundedness, constraint satisfaction, and hallucination rate — not on fluency.
View 13L2 · MLOps
Observability
A recommender degrades quietly. Ranking quality decays before anyone reports a problem, and subgroup damage — a specialty or a segment being systematically under-served — never shows up in an aggregate metric.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12.5px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart LR
subgraph SIG["Signals"]
direction TB
x1["Feature drift PSI per feature"]
x2["Prediction drift score distribution"]
x3["NDCG decay rolling window"]
x4["Training/serving skew"]
x5["Subgroup performance by specialty · segment · territory"]
x6["Latency P50 / P95"]
x7["Suppression rate by rule"]
end
reg["Model registry MLflow"]
mon["Monitoring service"]
alert["Alerting"]
act["Actions retrain · rollback · freeze policy"]
SIG --> mon --> alert --> act
reg --> mon
act --> reg
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
classDef io fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.5px,color:#0A464C;
class x1,x2,x3,x4,x5,x6,x7 model;
class reg,mon,alert,act io;
View 14Close
What this demonstrates, what it costs, and what comes next
What the design establishes, the costs it accepts to get there, and the three things I would do first with production traffic.
What the system demonstrates
Capability
How it shows up in the design
A coherent joint decision
One scored tuple - HCP, action, content, channel, window - rather than components reconciled after the fact
Hybrid retrieval
Five retrievers with complementary failure modes, each measured on unique contribution
Ranking under business utility
Model score adjusted by commercially-owned fatigue, opt-out and cost coefficients held outside the learned objective
Deterministic compliance
A zero-error eligibility gate that runs last, emits an audit proof, and fails closed
Bounded exploration
Online learning confined to the eligible action space, so a breach is unreachable rather than penalised
Real-time adaptation
Session features refreshed within minutes on a path that degrades gracefully to batch
Causal measurement
Targeting on incremental rather than predicted response, with randomisation designed around field spillover
Grounded explanation
Generation scoped to language, with citations validated against the retrieved evidence set
Accepted trade-offs
Decision
What it costs
Why it was still the right call
Joint decision record
Pipeline coupling — the channel model cannot be released independently of the ranker
Independent models produce contradictory recommendations, and the reconciliation layer becomes unlearnable
Penalties outside the learned objective
Some loss of joint optimality against a fully-learned objective
Contact policy is a commercial accountability; the business can retune without a model release
Eligibility gate last
Scoring work spent on candidates that are later suppressed
Freshest possible approval check, an auditable suppression log, and no compliance signal leaking into the ranking objective
Two stores over one
Consistency risk between the feature store and the graph
Point lookup and variable-depth traversal are different access patterns; one store serves one of them badly
Generative layer scoped to language
Loses whatever contextual signal a language model could contribute to ordering
Reproducibility is required for audit and for experiment validity; that signal is better distilled into an offline feature
RDF over a property graph
Traversal throughput I have not benchmarked at production scale
Formal ontology governance and interoperability across many source systems
What I would change first
#
Change
Reason
1
Basic observability from first deployment — logging, lineage, model versioning and latency monitoring ship with the first served model; drift and subgroup analysis follow once production outcomes mature
Lightweight logging and latency monitoring should exist from the first served model; only drift and subgroup analysis need a running system to be meaningful
2
Benchmark the graph store against a property-graph alternative
Retrieval latency at production traversal volume is an open question, not a settled one
3
Re-engineer the ANN index and graph rebuilds to be incremental
Both are full recomputations today and are the first components that would break at ten times the volume
4
Calibrate reward and penalty coefficients against realised opt-out rates
The current values encode a defensible ordering but are set a priori rather than fitted
5
Add a learned retrieval blend in place of proportional allocation
Proportional allocation is transparent but almost certainly leaves recall on the table
Open questions I would answer with production data
Does the shared cross-channel encoder actually beat per-channel models on sparse-channel HCPs, or is the aggregate gain driven entirely by the dense channels?
How large is the negative-uplift population in practice — is suppressing it a material effect or a rounding error?
Does in-session feature refresh change decisions often enough to justify the streaming path over hourly batch?
On measurement. Offline ranking quality and business impact are different questions and can disagree. Offline NDCG measures ordering against held-out engagements under the existing logging policy; whether that translates into incremental value is answered only by randomised experiment, reported as Qini and incremental engagement per thousand HCPs.
Appendix
Reference detail - capacity tables, transfer architecture, repository layout and build sequencing. Available for questions rather than presented in flow.
Appendix 1Capacity
Scale, throughput and service objectives
Design capacity and service targets, not recorded benchmarks — these are the figures the system is sized and engineered against. Batch, streaming and request-time paths carry very different load profiles, and each has its own objective — throughput for batch, freshness for streaming, latency for serving.
~85 K concurrent sessions ~4 KB state each · 340 MB working set
Redis, 30-min TTL, keyed by hcp_id
Read latency
P99 read < 5 ms
Serving recommendation API
2,000 QPS peak ~200 K candidate scorings / sec
FastAPI, horizontally scaled · warm model cache
Latency and availability
Target P50 45 ms · P95 180 ms Target availability 99.9%
The event stream is bursty rather than uniform: a broadcast send to a large segment produces hundreds of thousands of impression events in minutes, which is why the streaming tier is sized on peak rather than mean.
Model and index operations
Artefact
Size
Cadence
Build cost
Ranker training set
~85 M labelled rows · 340 features
Weekly
~35 min
Two-tower embeddings
500 K HCP × 128-d · 40 K item × 128-d
Nightly
~50 min GPU
ANN index
40 K vectors · ~40 MB
Nightly full rebuild
~4 min
Sequence model
Max sequence 200 · 6 layers
Weekly
~2.5 h GPU
Graph rebuild
1.2 M nodes · 14 M edges
Nightly, edges incremental
~18 min
Bandit posteriors
7 arms × context dim
Continuous
streaming update
Known scaling limit. The ANN index and graph rebuilds are full recomputations. At roughly ten times current volume both become the binding constraint on nightly window and would need incremental construction — this is the first thing I would re-engineer.
Appendix 2Transfer
Cross-brand and cross-channel transfer
The claim to test: behaviour in one domain improves prediction in another, most visibly for HCPs with thin history in the target domain. The ablation is the experiment — a shared encoder that does not beat per-channel models on sparse users has not earned its complexity.
%%{init:{'theme':'base','themeVariables':{'fontFamily':'ui-monospace,SFMono-Regular,Menlo,monospace','fontSize':'12.5px','lineColor':'#5C6675','primaryTextColor':'#0D1117'}}}%%
flowchart LR
subgraph IN["Per-HCP input"]
direction TB
i1["Profile specialty · segment · territory"]
i2["Interaction sequence all brands, all channels"]
i3["Graph embedding Node2Vec / GraphSAGE"]
end
enc["Shared HCP encoder transformer over unified sequence"]
h1["Email head BROADCAST_EMAIL / REP_EMAIL"]
h2["Field-action head FIELD_DETAIL rep call"]
h3["Web-content head"]
h4["Event head THIRD_PARTY_MEDIA / educational"]
i1 --> enc
i2 --> enc
i3 --> enc
enc --> h1
enc --> h2
enc --> h3
enc --> h4
abl["Ablation ladder channel-only → brand-only → shared → shared+graph → shared+session transformer"]
enc -.-> abl
classDef model fill:#EBE8FA,stroke:#5B4BC4,stroke-width:1.5px,color:#2E2470;
classDef io fill:#E1F0F1,stroke:#0E7C86,stroke-width:1.5px,color:#0A464C;
class enc,h1,h2,h3,h4 model;
class i1,i2,i3,abl io;
Three commercial domains: Brand A clinical education · Brand B access and reimbursement · Brand C scientific events.
Appendix 3Code layout
Repository map
One repository, connected content modules — not twelve disconnected notebooks. Each folder maps to a component in View 03 and each is importable by the ones downstream of it.