A newer version of the Streamlit SDK is available: 1.56.0
title: AI Messaging System
emoji: πΆ
colorFrom: blue
colorTo: gray
sdk: streamlit
sdk_version: 1.50.0
python_version: 3.9
app_file: app.py
AI Messaging System v3 β Visualization Tool
A Streamlit tool for reviewing and evaluating AI-generated push notification messages. All data is read from a single output table β no joins. Reviewers filter messages by brand, campaign, and scenario, inspect user context stored at generation time, and flag any message that has issues. Rejections are persisted to Snowflake. No action on a message means it is good.
This tool does not generate messages. It is purely for reviewing messages already produced by the v3 pipeline.
Purpose
- Pre-fetch the most recent 150 messages per campaign on login (single query, no joins)
- Filter instantly by brand, campaign, and scenario β all client-side, no extra DB queries
- View the user context captured at generation time (streaks, profile, previous messages, last interacted content)
- Reject messages with a structured reason β persisted to Snowflake immediately
- Track rejection counts per review session in the sidebar
Directory Structure
visualization/
βββ app.py # Entry point: login + data pre-fetch on authentication
βββ pages/
β βββ 1_Message_Viewer.py # Message cards, filters, user context, rejection UI
βββ utils/
β βββ auth.py # Login / session authentication
β βββ snowflake_client.py # Snowflake reads (single table) and feedback writes
β βββ feedback_manager.py # In-session rejection state + Snowflake persistence
β βββ theme.py # Brand colours, campaign labels, scenario definitions
β βββ __init__.py
βββ requirements.txt
βββ .env # Credentials (not committed)
βββ .env.example
βββ README.md
Getting Started
1. Install dependencies
pip install -r requirements.txt
2. Configure credentials
Copy .env.example to .env and fill in your Snowflake credentials and the app token:
SNOWFLAKE_USER = ...
SNOWFLAKE_PASSWORD = ...
SNOWFLAKE_ACCOUNT = ...
SNOWFLAKE_ROLE = ACCOUNTADMIN
SNOWFLAKE_DATABASE = RECSYS_V3
SNOWFLAKE_WAREHOUSE= COMPUTE_WH
SNOWFLAKE_SCHEMA = PUBLIC
APP_TOKEN = ...
3. Run the app
cd ai_messaging_system_v3/visualization
streamlit run app.py
Authentication
Login requires an email from the authorised list and a shared access token (APP_TOKEN in .env). Authorised emails are defined in utils/auth.py.
Snowflake Tables
Input (read-only) β single table, no joins
All data needed for display and filtering is stored at message generation time in one table.
MESSAGING_SYSTEM_V2.GENERATED_DATA.DAILY_PUSH_MESSAGES
| Column | Type | Description |
|---|---|---|
USER_ID |
NUMBER | Target user |
DETECTED_BRAND |
VARCHAR | Brand: drumeo, pianote, guitareo, singeo, playbass |
BRANCH |
VARCHAR | Campaign routing key (mirrors CAMPAIGN_NAME) |
MESSAGE |
VARCHAR | JSON string {"header": "...", "message": "..."} β parsed into header + body |
PLATFORM |
VARCHAR | Delivery platform (e.g. push) |
CAMPAIGN_NAME |
VARCHAR | dailyPush_dailyStreak, dailyPush_weeklyStreak, dailyPush_noStreak |
TIMESTAMP |
TIMESTAMP | Generation timestamp |
RECOMMENDATION |
VARCHAR | Recommendation type; for_you means no specific recommendation (used for scenario detection) |
RECOMMENDED_CONTENT_ID |
NUMBER | Content ID linked in the message (if any) |
FIRST_NAME |
VARCHAR | User's first name at generation time |
CURRENT_DAILY_STREAK_LENGTH |
NUMBER | Daily streak at generation time |
CURRENT_WEEKLY_STREAK_LENGTH |
NUMBER | Weekly streak at generation time |
USER_PROFILE |
VARCHAR | User profile text used for personalisation |
PREVIOUS_MESSAGES |
VARCHAR | JSON snapshot of prior messages for this user |
LAST_INTERACTED_CONTENT_PROFILE |
VARCHAR | Profile of the last content the user interacted with |
Output (written by this tool)
MESSAGING_SYSTEM_V2.UI.V3_FEEDBACKS β created automatically on first run.
| Column | Description |
|---|---|
BATCH_ID |
UUID generated per review session |
USER_ID |
Reviewed user |
CAMPAIGN_NAME |
Campaign of the reviewed message |
BRAND |
Brand of the reviewed user |
REJECTION_REASON |
Category key (see Rejection Reasons) |
REJECTION_TEXT |
Optional free-text note from the reviewer |
MESSAGE_HEADER |
Full header at time of review |
MESSAGE_BODY |
Full body at time of review |
MESSAGE_TIMESTAMP |
When the message was originally generated |
REVIEWER_EMAIL |
Email of the reviewer |
TIMESTAMP |
When the rejection was submitted |
Rejections are upserted via MERGE β changing the reason on an already-rejected message updates the existing row.
Users with any existing rejection in V3_FEEDBACKS are excluded from the pre-fetch query, so the same message is never shown twice across sessions.
Filters
All filters are applied instantly client-side on the pre-loaded DataFrame β no Snowflake query is triggered on filter change.
| Filter | Source column | Notes |
|---|---|---|
| Brand | DETECTED_BRAND |
All, Drumeo, Pianote, Guitareo, Singeo, Playbass |
| Campaign | CAMPAIGN_NAME |
All, Daily Streak, Weekly Streak, No Streak |
| Scenario | RECOMMENDATION |
Weekly Streak campaign only (see Scenarios) |
| Only users with previous messages | HAS_PREVIOUS |
Pre-computed at fetch time via COUNT(*) > 1 |
Campaigns and Scenarios
| Campaign | Scenario key | Display label | Detection |
|---|---|---|---|
dailyPush_dailyStreak |
β | β | No sub-scenarios |
dailyPush_weeklyStreak |
no_practice_this_week |
No Practice This Week | RECOMMENDATION == 'for_you' |
dailyPush_weeklyStreak |
practiced_this_week |
Practiced This Week | RECOMMENDATION != 'for_you' |
dailyPush_noStreak |
β | β | No sub-scenarios |
Rejection Reason Categories
| Key | Label |
|---|---|
poor_header |
Poor Header |
poor_body |
Poor Body / Content |
grammar_issues |
Grammar Issues |
emoji_problems |
Emoji Problems |
recommendation_issues |
Recommendation Issues |
wrong_information |
Wrong / Inaccurate Information |
tone_issues |
Tone Issues |
similarity |
Similar To Previous Messages |
other |
Other |
Supported Brands
| Brand | Colour |
|---|---|
| Drumeo | Red #E84545 |
| Pianote | Blue #4A90D9 |
| Guitareo | Amber #F5A623 |
| Singeo | Purple #7B68EE |
| Playbass | Green #3DAA5C |
UI Flow
app.py (Login form)
β authenticated
app.py (Landing)
β pre-fetches top 150 messages per campaign from DAILY_PUSH_MESSAGES
β stores result in session_state["all_messages"]
β shows per-campaign counts
β navigate to Message Viewer
pages/1_Message_Viewer.py
Sidebar (instant, no Apply button):
Brand / Campaign / Scenario / "Only users with prev messages"
Reload button β clears cache, re-fetches
Sidebar:
Session stats: showing Β· rejected Β· reject %
Main area β paginated cards (20 per page):
Each card:
ββ Brand badge Β· Campaign Β· User #ID Β· Generated timestamp βββ
β β
β Push Notification Preview β
β [Header text] β
β [Body text] β
β Header: N chars Β· Body: N chars β
β β
β βΌ User Context (collapsible) β
β First name Β· Daily streak Β· Weekly streak Β· Profile β
β β
β βΌ Previous Messages (collapsible) β
β Parsed from PREVIOUS_MESSAGES column β no extra query β
β β
β βΌ Last Interacted Content Profile (collapsible) β
β β
β Feedback column: β
β [β Reject] β reason dropdown + optional note β Submit β
β Already rejected: shows reason Β· [βοΈ Change] [Clear] β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Rejections saved to Snowflake on Submit
β No action = message is considered good
Module Responsibilities
| Module | Responsibility |
|---|---|
auth.py |
Session authentication, email allowlist, token verification |
snowflake_client.py |
Single-table fetch from DAILY_PUSH_MESSAGES; feedback MERGE/DELETE |
feedback_manager.py |
In-session rejection dict (fast cache), delegates writes to snowflake_client |
theme.py |
Brand colours, campaign labels, scenario definitions and detect_scenario() |