A personalized notification router for WhatsApp-style message streams. For every incoming message (text, image, or voice note) it decides whether the user should be interrupted now, shown the message later, or never bothered with it.
Built for the HackerRank Orchestrate (August 2026) challenge.
| Action | Meaning |
|---|---|
notify |
Important enough to interrupt the user now |
digest |
Useful, but can be shown later in a batch |
mute |
Low-value, repetitive, unwanted, suspicious, or unsafe |
Each prediction also carries a message_type, a human-readable reason, a calibrated confidence, and the evidence_message_ids of past messages that support the decision.
Decisions are personalized: the same message can be routed differently for different users based on their relationship with the sender, group or business, their past reactions, quiet hours, and notification load.
messages.csv ──► data_loader ──► for each message:
│
┌────────────────┼─────────────────┐
▼ ▼ ▼
media_pipeline text_signals history_engine
(OCR / voice info) (urgency, payment, (evidence + past
promo, scam, OTP, engagement stats)
prompt injection)
└────────────────┼─────────────────┘
▼
risk_detector ── high risk? ──► mute (scam / spam)
│ no
▼
personalization
(relationship, engagement,
mute state, opt-outs, load)
▼
decision_engine ──► output.csv
Decision order
- Safety first.
risk_detectorscores scam/spam risk (OTP requests, domain mismatch, unverified or new business accounts, heavy reports, suspicious phrasing). Above the threshold, the message is muted regardless of engagement or mentions. - Personalized scoring. Relationship strength, engagement rate, trust, urgency and deadlines, direct mentions, muted groups, business opt-outs, and daily notification load are fused into a routing score.
- Relevance gate. A strong relationship alone does not justify
notify; the message also needs a deadline, mention, actionable content, or an explicit ask. - Classification.
message_typecomes from a transparent keyword/regex rule set. - Evidence. Up to 3 related past messages (same user first) are cited.
All thresholds live in code/config.py, so behavior can be re-tuned without touching logic.
.
├── code/
│ ├── main.py # entry point: routes every message, writes output.csv
│ ├── config.py # paths, thresholds, allowed values
│ ├── data_loader.py # reads all dataset CSVs
│ ├── text_signals.py # lexicon/regex signals
│ ├── risk_detector.py # scam/spam scoring (overrides personalization)
│ ├── personalization.py # per-user relationship and engagement logic
│ ├── history_engine.py # evidence retrieval and engagement stats
│ ├── decision_engine.py # fuses signals into the final prediction
│ ├── media_pipeline.py # dispatches image / voice analysis
│ ├── ocr_processor.py # Tesseract OCR for images
│ ├── audio_processor.py # voice note handling (see limitations)
│ ├── llm_client.py # optional LLM enrichment (off by default)
│ ├── output_writer.py # validates and writes the output CSV
│ ├── models.py, logging_utils.py
│ ├── evaluation/main.py # self-evaluation workflow
│ ├── requirements.txt
│ └── .env.example
├── dataset/ # provided data (not included in code zip)
└── logs/run.log # created on each run
Requires Python 3.9+.
cd code
pip install -r requirements.txtThe core pipeline uses only the Python standard library. The packages are optional:
pytesseract+Pillow: read text from image messages (needs the Tesseract binary)anthropic: only if you enable LLM enrichment
Install the system tools (optional but recommended):
# Ubuntu / Debian
sudo apt-get install tesseract-ocr ffmpeg
# macOS
brew install tesseract ffmpegOn Windows, install the Tesseract binary and FFmpeg and add them to your PATH. Without Tesseract, image messages fall back to a no-OCR heuristic instead of crashing.
cd code
python main.pyThis reads everything in dataset/, routes all messages in dataset/messages.csv, and writes dataset/output.csv.
python main.py --output path/to/output.csv # custom output path
python main.py --verbose # debug loggingmessage_id, action, message_type, reason, confidence, evidence_message_ids
action:notify|digest|mutemessage_type:personal,urgent,event,payment,business_update,promotion,greeting,forward,spam,scam,unknownconfidence: 0 to 1evidence_message_ids: semicolon-separated history IDs, ornone
cd code
python evaluation/main.pyRe-runs the pipeline on the labeled rows in dataset/sample_messages.csv (30 messages) and reports action accuracy, message-type accuracy, and a confusion breakdown, listing every mismatch with its reason.
Latest run:
| Metric | Result |
|---|---|
| Action accuracy | 96.7% |
| Message-type accuracy | 83.3% |
| Both correct | 83.3% |
Caveat: the labeled set is small and was used while developing the rules, so these numbers are optimistic and are a sanity check, not an estimate of hidden-test performance. The official scoring uses hidden labels.
Predictions on the 110 messages in messages.csv split into 26 notify, 40 digest, and 44 mute.
The pipeline is deterministic and needs no API key. To let Claude rewrite templated reasons into more natural prose:
cp .env.example .env
# set ENABLE_LLM_ENRICHMENT=true and ANTHROPIC_API_KEY=...llm_client.py is the only module that touches the network, and only when both are set. Keep keys in .env and never commit it.
- Voice notes are not transcribed. No speech-to-text service is bundled, and the code never fabricates a transcript. Voice messages are routed using duration and sender/conversation context only.
audio_processor.set_asr_provider()is a ready hook for plugging in Whisper or another ASR without changing any other file. - Rule-based classification.
message_typeand risk detection use transparent keyword/regex rules rather than a trained model, to avoid overfitting the small labeled sample and to avoid hardcoding labels. - Image understanding is OCR-only. Visual layout and non-text cues are not analyzed.
- Confidence is heuristic, not statistically calibrated.
- Local or hosted ASR for voice notes
- Vision-language model for posters and screenshots
- LLM second-opinion pass on low-confidence cases
- Calibrate confidence against labeled outcomes
- Learn weights from
message_events.csvreactions
- Reads the provided CSV and media files
- Writes
output.csvin the exact required schema - Includes an evaluation workflow
- No hardcoded test labels or message-specific answers