Delete visualization
Browse files- visualization/.env.example +0 -38
- visualization/.gitignore +0 -57
- visualization/README.md +0 -1130
- visualization/app.py +0 -384
- visualization/data/UI_users/drumeo_users.csv +0 -101
- visualization/data/UI_users/guitareo_users.csv +0 -101
- visualization/data/UI_users/pianote_users.csv +0 -101
- visualization/data/UI_users/singeo_users.csv +0 -101
- visualization/pages/1_Campaign_Builder.py +0 -1100
- visualization/pages/2_Message_Viewer.py +0 -939
- visualization/pages/4_Analytics.py +0 -886
- visualization/pages/5_Historical_Analytics.py +0 -1043
- visualization/requirements.txt +0 -23
- visualization/utils/__init__.py +0 -28
- visualization/utils/auth.py +0 -101
- visualization/utils/config_manager.py +0 -361
- visualization/utils/data_loader.py +0 -230
- visualization/utils/db_manager.py +0 -700
- visualization/utils/experiment_runner.py +0 -276
- visualization/utils/feedback_manager.py +0 -414
- visualization/utils/session_feedback_manager.py +0 -310
- visualization/utils/theme.py +0 -335
visualization/.env.example
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
# AI Messaging System Visualization Tool - Environment Variables
|
| 2 |
-
# Copy this file to .env and fill in your actual values
|
| 3 |
-
|
| 4 |
-
# ============================================================================
|
| 5 |
-
# Snowflake Configuration
|
| 6 |
-
# ============================================================================
|
| 7 |
-
SNOWFLAKE_USER=your_snowflake_username
|
| 8 |
-
SNOWFLAKE_PASSWORD=your_snowflake_password
|
| 9 |
-
SNOWFLAKE_ACCOUNT=your_account_identifier
|
| 10 |
-
SNOWFLAKE_ROLE=your_role_name
|
| 11 |
-
SNOWFLAKE_DATABASE=your_database_name
|
| 12 |
-
SNOWFLAKE_WAREHOUSE=your_warehouse_name
|
| 13 |
-
SNOWFLAKE_SCHEMA=your_schema_name
|
| 14 |
-
|
| 15 |
-
# ============================================================================
|
| 16 |
-
# AI Provider API Keys
|
| 17 |
-
# ============================================================================
|
| 18 |
-
# OpenAI API Key (for GPT models)
|
| 19 |
-
OPENAI_API_KEY=sk-your-openai-api-key-here
|
| 20 |
-
|
| 21 |
-
# Google AI API Key (for Gemini models)
|
| 22 |
-
GOOGLE_API_KEY=your-google-api-key-here
|
| 23 |
-
|
| 24 |
-
# ============================================================================
|
| 25 |
-
# Application Access Token
|
| 26 |
-
# ============================================================================
|
| 27 |
-
# This token is used for authentication in the web UI
|
| 28 |
-
# Set this to a secure random string
|
| 29 |
-
# Example: you can generate one using: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
| 30 |
-
APP_TOKEN=your-secure-access-token-here
|
| 31 |
-
|
| 32 |
-
# ============================================================================
|
| 33 |
-
# Notes:
|
| 34 |
-
# ============================================================================
|
| 35 |
-
# 1. Never commit the actual .env file to version control
|
| 36 |
-
# 2. Keep your credentials secure
|
| 37 |
-
# 3. Use strong, unique values for APP_TOKEN
|
| 38 |
-
# 4. You can also use the parent directory's .env file if it contains all these variables
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/.gitignore
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
# Python
|
| 2 |
-
__pycache__/
|
| 3 |
-
*.py[cod]
|
| 4 |
-
*$py.class
|
| 5 |
-
*.so
|
| 6 |
-
.Python
|
| 7 |
-
env/
|
| 8 |
-
venv/
|
| 9 |
-
ENV/
|
| 10 |
-
build/
|
| 11 |
-
develop-eggs/
|
| 12 |
-
dist/
|
| 13 |
-
downloads/
|
| 14 |
-
eggs/
|
| 15 |
-
.eggs/
|
| 16 |
-
lib/
|
| 17 |
-
lib64/
|
| 18 |
-
parts/
|
| 19 |
-
sdist/
|
| 20 |
-
var/
|
| 21 |
-
wheels/
|
| 22 |
-
*.egg-info/
|
| 23 |
-
.installed.cfg
|
| 24 |
-
*.egg
|
| 25 |
-
|
| 26 |
-
# Data files (local storage)
|
| 27 |
-
data/configs/*.json
|
| 28 |
-
data/feedback/*.csv
|
| 29 |
-
data/experiments/*.json
|
| 30 |
-
data/users/*.csv
|
| 31 |
-
|
| 32 |
-
# Keep directory structure but ignore contents
|
| 33 |
-
!data/configs/.gitkeep
|
| 34 |
-
!data/feedback/.gitkeep
|
| 35 |
-
!data/experiments/.gitkeep
|
| 36 |
-
!data/users/.gitkeep
|
| 37 |
-
|
| 38 |
-
# Environment variables
|
| 39 |
-
.env
|
| 40 |
-
.env.local
|
| 41 |
-
|
| 42 |
-
# IDEs
|
| 43 |
-
.vscode/
|
| 44 |
-
.idea/
|
| 45 |
-
*.swp
|
| 46 |
-
*.swo
|
| 47 |
-
*~
|
| 48 |
-
|
| 49 |
-
# OS
|
| 50 |
-
.DS_Store
|
| 51 |
-
Thumbs.db
|
| 52 |
-
|
| 53 |
-
# Streamlit
|
| 54 |
-
.streamlit/secrets.toml
|
| 55 |
-
|
| 56 |
-
# Logs
|
| 57 |
-
*.log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/README.md
DELETED
|
@@ -1,1130 +0,0 @@
|
|
| 1 |
-
# AI Messaging System - Visualization Tool
|
| 2 |
-
|
| 3 |
-
An interactive Streamlit-based visualization and experimentation tool for the AI Messaging System v2. This tool enables non-technical users to generate, visualize, analyze, and improve personalized message campaigns with integrated A/B testing capabilities and comprehensive historical tracking.
|
| 4 |
-
|
| 5 |
-
## 🎯 Purpose
|
| 6 |
-
|
| 7 |
-
- Generate personalized messages with various configurations
|
| 8 |
-
- Run A/B tests with parallel processing and side-by-side comparison
|
| 9 |
-
- Visualize and analyze messages in real-time across all campaign stages
|
| 10 |
-
- Provide detailed feedback with message header/body tracking
|
| 11 |
-
- Track improvements and trends across historical experiments
|
| 12 |
-
- Cloud-native architecture with Snowflake integration
|
| 13 |
-
- Ready for deployment on HuggingFace Spaces
|
| 14 |
-
|
| 15 |
-
---
|
| 16 |
-
|
| 17 |
-
## 🏗️ Architecture Overview
|
| 18 |
-
|
| 19 |
-
### Core Philosophy: **In-Memory + Cloud Persistence**
|
| 20 |
-
|
| 21 |
-
The system operates on a hybrid architecture:
|
| 22 |
-
- **In-Memory Operations**: All active experiments run in `session_state` (no local files)
|
| 23 |
-
- **Cloud Persistence**: Data stored in Snowflake for long-term analytics
|
| 24 |
-
- **On-Demand Loading**: Historical data loaded from Snowflake when needed
|
| 25 |
-
- **One-Click Storage**: Results persisted with a single button click
|
| 26 |
-
|
| 27 |
-
### Why This Architecture?
|
| 28 |
-
|
| 29 |
-
1. **HuggingFace Ready**: No local file dependencies
|
| 30 |
-
2. **Fast Operations**: In-memory processing for real-time feedback
|
| 31 |
-
3. **Scalable**: Snowflake handles unlimited historical data
|
| 32 |
-
4. **Clean Separation**: Current experiments vs. historical data
|
| 33 |
-
5. **Versioned Configs**: Automatic configuration versioning in Snowflake
|
| 34 |
-
|
| 35 |
-
---
|
| 36 |
-
|
| 37 |
-
## 📁 Directory Structure
|
| 38 |
-
|
| 39 |
-
```
|
| 40 |
-
visualization/
|
| 41 |
-
├── app.py # Main entry point with authentication & brand selection
|
| 42 |
-
├── pages/ # Multi-page Streamlit application
|
| 43 |
-
│ ├── 1_Campaign_Builder.py # Campaign configuration & generation (with A/B testing)
|
| 44 |
-
│ ├── 2_Message_Viewer.py # Message browsing & feedback (A/B aware)
|
| 45 |
-
│ ├── 4_Analytics.py # Performance metrics for CURRENT experiment
|
| 46 |
-
│ └── 5_Historical_Analytics.py # Historical experiments from Snowflake
|
| 47 |
-
├── utils/ # Utility modules
|
| 48 |
-
│ ├── __init__.py
|
| 49 |
-
│ ├── auth.py # Authentication logic
|
| 50 |
-
│ ├── config_manager.py # Configuration loading from Snowflake
|
| 51 |
-
│ ├── db_manager.py # Snowflake database operations (NEW)
|
| 52 |
-
│ ├── experiment_runner.py # Parallel experiment execution (NEW)
|
| 53 |
-
│ ├── session_feedback_manager.py # In-memory feedback management (NEW)
|
| 54 |
-
│ └── theme.py # Brand-specific theming
|
| 55 |
-
├── data/ # Local data storage (configs cached here)
|
| 56 |
-
│ └── UI_users/ # Pre-loaded user lists (100 users per brand)
|
| 57 |
-
│ ├── drumeo_users.csv
|
| 58 |
-
│ ├── pianote_users.csv
|
| 59 |
-
│ ├── guitareo_users.csv
|
| 60 |
-
│ └── singeo_users.csv
|
| 61 |
-
├── requirements.txt # Python dependencies
|
| 62 |
-
├── README.md # This file
|
| 63 |
-
├── IMPLEMENTATION_COMPLETE.md # Refactoring details & progress
|
| 64 |
-
└── ARCHITECTURE_REFACTOR_GUIDE.md # Technical refactoring guide
|
| 65 |
-
```
|
| 66 |
-
|
| 67 |
-
### Deprecated Files (No Longer Used)
|
| 68 |
-
|
| 69 |
-
These files are legacy and no longer part of the active codebase:
|
| 70 |
-
- ~~`utils/data_loader.py`~~ - Replaced by session_state loading
|
| 71 |
-
- ~~`utils/feedback_manager.py`~~ - Replaced by SessionFeedbackManager
|
| 72 |
-
- ~~`data/configs/`~~ - Configs now cached locally but loaded from Snowflake
|
| 73 |
-
- ~~`data/feedback/`~~ - Feedback now in session_state → Snowflake
|
| 74 |
-
- ~~`ai_messaging_system_v2/Data/ui_output/`~~ - No more file outputs
|
| 75 |
-
|
| 76 |
-
---
|
| 77 |
-
|
| 78 |
-
## 🗄️ Snowflake Database Schema
|
| 79 |
-
|
| 80 |
-
### Tables
|
| 81 |
-
|
| 82 |
-
**1. MESSAGING_SYSTEM_V2.UI.CONFIGS**
|
| 83 |
-
```sql
|
| 84 |
-
CONFIG_NAME VARCHAR -- Configuration identifier
|
| 85 |
-
CONFIG_FILE VARIANT -- JSON configuration
|
| 86 |
-
CONFIG_VERSION INTEGER -- Auto-incrementing version
|
| 87 |
-
BRAND VARCHAR -- Brand name (drumeo, pianote, etc.)
|
| 88 |
-
CREATED_AT TIMESTAMP -- Creation timestamp
|
| 89 |
-
```
|
| 90 |
-
|
| 91 |
-
**2. MESSAGING_SYSTEM_V2.UI.EXPERIMENT_METADATA**
|
| 92 |
-
```sql
|
| 93 |
-
EXPERIMENT_ID VARCHAR -- Unique experiment identifier
|
| 94 |
-
CONFIG_NAME VARCHAR -- Configuration used
|
| 95 |
-
BRAND VARCHAR -- Brand name
|
| 96 |
-
CAMPAIGN_NAME VARCHAR -- Campaign identifier
|
| 97 |
-
STAGE INTEGER -- Stage number (1-11)
|
| 98 |
-
LLM_MODEL VARCHAR -- Model used (gpt-4o-mini, gemini-2.5-flash-lite, etc.)
|
| 99 |
-
TOTAL_MESSAGES INTEGER -- Messages generated in this stage
|
| 100 |
-
TOTAL_USERS INTEGER -- Unique users in this stage
|
| 101 |
-
PLATFORM VARCHAR -- Platform (push, email, etc.)
|
| 102 |
-
PERSONALIZATION BOOLEAN -- Personalization enabled
|
| 103 |
-
INVOLVE_RECSYS BOOLEAN -- Recommendations enabled
|
| 104 |
-
RECSYS_CONTENTS ARRAY -- Recommendation types
|
| 105 |
-
SEGMENT_INFO VARCHAR -- Segment description
|
| 106 |
-
CAMPAIGN_INSTRUCTIONS VARCHAR -- Campaign-wide instructions
|
| 107 |
-
PER_MESSAGE_INSTRUCTIONS VARCHAR -- Stage-specific instructions
|
| 108 |
-
START_TIME TIMESTAMP -- Experiment start time
|
| 109 |
-
END_TIME TIMESTAMP -- Experiment end time (optional)
|
| 110 |
-
```
|
| 111 |
-
|
| 112 |
-
**3. MESSAGING_SYSTEM_V2.UI.FEEDBACKS**
|
| 113 |
-
```sql
|
| 114 |
-
EXPERIMENT_ID VARCHAR -- Links to EXPERIMENT_METADATA
|
| 115 |
-
USER_ID INTEGER -- User who received the message
|
| 116 |
-
STAGE INTEGER -- Stage number
|
| 117 |
-
FEEDBACK_TYPE VARCHAR -- 'reject' (only type currently)
|
| 118 |
-
REJECTION_REASON VARCHAR -- Reason category key
|
| 119 |
-
REJECTION_TEXT VARCHAR -- Custom text explanation
|
| 120 |
-
MESSAGE_HEADER VARCHAR -- Full message header
|
| 121 |
-
MESSAGE_BODY VARCHAR -- Full message body
|
| 122 |
-
CAMPAIGN_NAME VARCHAR -- Campaign identifier
|
| 123 |
-
BRAND VARCHAR -- Brand name
|
| 124 |
-
CONFIG_NAME VARCHAR -- Configuration used
|
| 125 |
-
TIMESTAMP TIMESTAMP -- Feedback submission time
|
| 126 |
-
```
|
| 127 |
-
|
| 128 |
-
---
|
| 129 |
-
|
| 130 |
-
## 🚀 Getting Started
|
| 131 |
-
|
| 132 |
-
### Prerequisites
|
| 133 |
-
|
| 134 |
-
- Python 3.9+
|
| 135 |
-
- Snowflake account and credentials
|
| 136 |
-
- OpenAI/Google AI API keys
|
| 137 |
-
- AI Messaging System v2 installed in parent directory
|
| 138 |
-
- Plotly for data visualization
|
| 139 |
-
|
| 140 |
-
### Installation
|
| 141 |
-
|
| 142 |
-
```bash
|
| 143 |
-
# Install dependencies
|
| 144 |
-
pip install -r requirements.txt
|
| 145 |
-
|
| 146 |
-
# Set up environment variables (.env file)
|
| 147 |
-
SNOWFLAKE_USER=your_user
|
| 148 |
-
SNOWFLAKE_PASSWORD=your_password
|
| 149 |
-
SNOWFLAKE_ACCOUNT=your_account
|
| 150 |
-
SNOWFLAKE_ROLE=your_role
|
| 151 |
-
SNOWFLAKE_DATABASE=MESSAGING_SYSTEM_V2
|
| 152 |
-
SNOWFLAKE_WAREHOUSE=your_warehouse
|
| 153 |
-
SNOWFLAKE_SCHEMA=UI
|
| 154 |
-
|
| 155 |
-
# LLM API keys
|
| 156 |
-
OPENAI_API_KEY=your_key
|
| 157 |
-
GOOGLE_API_KEY=your_key
|
| 158 |
-
```
|
| 159 |
-
|
| 160 |
-
### Running the Application
|
| 161 |
-
|
| 162 |
-
```bash
|
| 163 |
-
# From the visualization directory
|
| 164 |
-
cd visualization
|
| 165 |
-
streamlit run app.py
|
| 166 |
-
```
|
| 167 |
-
|
| 168 |
-
### First-Time Setup
|
| 169 |
-
|
| 170 |
-
1. Ensure `.env` file exists with Snowflake credentials
|
| 171 |
-
2. Verify user CSV files exist in `data/UI_users/` for each brand
|
| 172 |
-
3. Create Snowflake tables using schema above (or let system auto-create)
|
| 173 |
-
4. Upload initial configurations to Snowflake (optional - can create in UI)
|
| 174 |
-
5. Login with authorized email and access token
|
| 175 |
-
|
| 176 |
-
---
|
| 177 |
-
|
| 178 |
-
## 📊 Complete Data Flow
|
| 179 |
-
|
| 180 |
-
### **Session State Architecture**
|
| 181 |
-
|
| 182 |
-
All active data lives in Streamlit's `session_state`:
|
| 183 |
-
|
| 184 |
-
```python
|
| 185 |
-
# Single Experiment Mode
|
| 186 |
-
st.session_state.ui_log_data # DataFrame: Generated messages
|
| 187 |
-
st.session_state.current_experiment_id # String: Experiment identifier
|
| 188 |
-
st.session_state.current_experiment_metadata # List[Dict]: Metadata per stage
|
| 189 |
-
st.session_state.current_feedbacks # List[Dict]: Feedback records
|
| 190 |
-
|
| 191 |
-
# AB Testing Mode
|
| 192 |
-
st.session_state.ui_log_data_a # DataFrame: Experiment A messages
|
| 193 |
-
st.session_state.ui_log_data_b # DataFrame: Experiment B messages
|
| 194 |
-
st.session_state.experiment_a_id # String: Experiment A identifier
|
| 195 |
-
st.session_state.experiment_b_id # String: Experiment B identifier
|
| 196 |
-
st.session_state.experiment_a_metadata # List[Dict]: A's metadata
|
| 197 |
-
st.session_state.experiment_b_metadata # List[Dict]: B's metadata
|
| 198 |
-
st.session_state.feedbacks_a # List[Dict]: A's feedback
|
| 199 |
-
st.session_state.feedbacks_b # List[Dict]: B's feedback
|
| 200 |
-
|
| 201 |
-
# Configuration
|
| 202 |
-
st.session_state.campaign_config # Dict: Single mode config
|
| 203 |
-
st.session_state.campaign_config_a # Dict: AB mode config A
|
| 204 |
-
st.session_state.campaign_config_b # Dict: AB mode config B
|
| 205 |
-
st.session_state.configs_cache # Dict: Cached configs from Snowflake
|
| 206 |
-
```
|
| 207 |
-
|
| 208 |
-
### **End-to-End Workflow**
|
| 209 |
-
|
| 210 |
-
```
|
| 211 |
-
┌─────────────────────────────────────────────────────────────────┐
|
| 212 |
-
│ 1. CAMPAIGN BUILDER │
|
| 213 |
-
├─────────────────────────────────────────────────────────────────┤
|
| 214 |
-
│ • Load configs from Snowflake (cached in session_state) │
|
| 215 |
-
│ • User selects/modifies configuration │
|
| 216 |
-
│ • Sample users from CSV files │
|
| 217 |
-
│ • Generate messages (stored in session_state.ui_log_data) │
|
| 218 |
-
│ • Track metadata (session_state.current_experiment_metadata) │
|
| 219 |
-
└─────────────────────────────────────────────────────────────────┘
|
| 220 |
-
↓
|
| 221 |
-
┌─────────────────────────────────────────────────────────────────┐
|
| 222 |
-
│ 2. MESSAGE VIEWER │
|
| 223 |
-
├─────────────────────────────────────────────────────────────────┤
|
| 224 |
-
│ • Load messages from session_state.ui_log_data │
|
| 225 |
-
│ • Display in user-centric or stage-centric views │
|
| 226 |
-
│ • User provides feedback (reject messages) │
|
| 227 |
-
│ • Feedback stored in session_state.current_feedbacks │
|
| 228 |
-
│ • [BUTTON] Store Results to Snowflake │
|
| 229 |
-
│ → Writes metadata to EXPERIMENT_METADATA table │
|
| 230 |
-
│ → Writes feedback to FEEDBACKS table │
|
| 231 |
-
└─────────────────────────────────────────────────────────────────┘
|
| 232 |
-
↓
|
| 233 |
-
┌─────────────────────────────────────────────────────────────────┐
|
| 234 |
-
│ 3. ANALYTICS │
|
| 235 |
-
├─────────────────────────────────────────────────────────────────┤
|
| 236 |
-
│ • Load current experiment from session_state │
|
| 237 |
-
│ • Calculate metrics using SessionFeedbackManager │
|
| 238 |
-
│ • Show overall performance, stage analysis, rejection reasons │
|
| 239 |
-
│ • Support AB testing side-by-side comparison │
|
| 240 |
-
└─────────────────────────────────────────────────────────────────┘
|
| 241 |
-
↓
|
| 242 |
-
┌─────────────────────────────────────────────────────────────────┐
|
| 243 |
-
│ 4. HISTORICAL ANALYTICS │
|
| 244 |
-
├─────────────────────────────────────────────────────────────────┤
|
| 245 |
-
│ • [BUTTON] Load Historical Data from Snowflake │
|
| 246 |
-
│ • Query EXPERIMENT_METADATA + FEEDBACKS tables │
|
| 247 |
-
│ • Calculate aggregate metrics across all experiments │
|
| 248 |
-
│ • Show trends: rejection rates over time │
|
| 249 |
-
│ • Compare configurations: which performs best │
|
| 250 |
-
│ • Filter by date range │
|
| 251 |
-
└─────────────────────────────────────────────────────────────────┘
|
| 252 |
-
```
|
| 253 |
-
|
| 254 |
-
---
|
| 255 |
-
|
| 256 |
-
## 📄 Pages Overview
|
| 257 |
-
|
| 258 |
-
### Page 0: Home & Authentication (app.py)
|
| 259 |
-
|
| 260 |
-
**Purpose**: Login, brand selection, config loading, and navigation hub
|
| 261 |
-
|
| 262 |
-
**Features**:
|
| 263 |
-
- Email and token-based authentication
|
| 264 |
-
- Brand selection (Drumeo, Pianote, Guitareo, Singeo)
|
| 265 |
-
- **Config loading from Snowflake on startup**
|
| 266 |
-
- Brand-specific theming applied throughout app
|
| 267 |
-
- Navigation guide and quick start instructions
|
| 268 |
-
- Current experiment status overview
|
| 269 |
-
|
| 270 |
-
**Technical Details**:
|
| 271 |
-
- Session-based authentication
|
| 272 |
-
- Brand selection persists via `st.session_state.selected_brand`
|
| 273 |
-
- Configs cached in `st.session_state.configs_cache` on first load
|
| 274 |
-
- Dynamic theming using `utils/theme.py`
|
| 275 |
-
- Loads environment variables from `.env` file
|
| 276 |
-
|
| 277 |
-
**Key Functions**:
|
| 278 |
-
```python
|
| 279 |
-
def load_configs_from_snowflake(brand):
|
| 280 |
-
"""Load all configs for brand from Snowflake, cache in session_state."""
|
| 281 |
-
session = create_snowflake_session()
|
| 282 |
-
config_manager = ConfigManager(session)
|
| 283 |
-
configs = config_manager.load_configs_from_snowflake(brand)
|
| 284 |
-
st.session_state.configs_cache = configs
|
| 285 |
-
session.close()
|
| 286 |
-
```
|
| 287 |
-
|
| 288 |
-
---
|
| 289 |
-
|
| 290 |
-
### Page 1: Campaign Builder
|
| 291 |
-
|
| 292 |
-
**Purpose**: Create and run message generation campaigns with integrated A/B testing
|
| 293 |
-
|
| 294 |
-
**Architecture**: In-memory experiment execution with parallel AB testing
|
| 295 |
-
|
| 296 |
-
#### Key Features
|
| 297 |
-
|
| 298 |
-
**Configuration Management**:
|
| 299 |
-
- Loads configs from cached `session_state.configs_cache`
|
| 300 |
-
- Real-time config editing in UI
|
| 301 |
-
- Save configs to Snowflake with auto-versioning
|
| 302 |
-
- Quick save or save-as-new options
|
| 303 |
-
|
| 304 |
-
**A/B Testing Toggle**:
|
| 305 |
-
- Single Experiment Mode (default): One campaign
|
| 306 |
-
- A/B Testing Mode: Two parallel experiments for comparison
|
| 307 |
-
|
| 308 |
-
**User Sampling**:
|
| 309 |
-
- Random sampling from brand-specific CSV files
|
| 310 |
-
- 1-25 users selectable
|
| 311 |
-
- Same users used for both AB experiments (fair comparison)
|
| 312 |
-
|
| 313 |
-
**Parallel Execution**:
|
| 314 |
-
- Uses `ExperimentRunner.run_ab_test_parallel()`
|
| 315 |
-
- Threading for simultaneous A/B generation
|
| 316 |
-
- Separate Snowflake sessions per experiment (no conflicts)
|
| 317 |
-
- Console logging for thread progress
|
| 318 |
-
- Results stored directly in session_state
|
| 319 |
-
|
| 320 |
-
**Technical Details**:
|
| 321 |
-
|
| 322 |
-
**Single Mode Generation**:
|
| 323 |
-
```python
|
| 324 |
-
# ExperimentRunner handles all generation logic
|
| 325 |
-
runner = ExperimentRunner(brand=brand, system_config=config)
|
| 326 |
-
|
| 327 |
-
success, ui_log_data, metadata = runner.run_single_experiment(
|
| 328 |
-
config=campaign_config,
|
| 329 |
-
sampled_users_df=users_df,
|
| 330 |
-
experiment_id=experiment_id,
|
| 331 |
-
create_session_func=create_snowflake_session,
|
| 332 |
-
progress_container=st.container()
|
| 333 |
-
)
|
| 334 |
-
|
| 335 |
-
# Store in session_state
|
| 336 |
-
st.session_state.ui_log_data = ui_log_data
|
| 337 |
-
st.session_state.current_experiment_metadata = metadata
|
| 338 |
-
st.session_state.current_experiment_id = experiment_id
|
| 339 |
-
```
|
| 340 |
-
|
| 341 |
-
**AB Mode Generation**:
|
| 342 |
-
```python
|
| 343 |
-
# Parallel execution in threads
|
| 344 |
-
results = runner.run_ab_test_parallel(
|
| 345 |
-
config_a=campaign_config_a,
|
| 346 |
-
config_b=campaign_config_b,
|
| 347 |
-
sampled_users_df=users_df,
|
| 348 |
-
experiment_a_id=experiment_a_id,
|
| 349 |
-
experiment_b_id=experiment_b_id,
|
| 350 |
-
create_session_func=create_snowflake_session
|
| 351 |
-
)
|
| 352 |
-
|
| 353 |
-
# Store both results
|
| 354 |
-
st.session_state.ui_log_data_a = results['a']['ui_log_data']
|
| 355 |
-
st.session_state.ui_log_data_b = results['b']['ui_log_data']
|
| 356 |
-
st.session_state.experiment_a_metadata = results['a']['metadata']
|
| 357 |
-
st.session_state.experiment_b_metadata = results['b']['metadata']
|
| 358 |
-
```
|
| 359 |
-
|
| 360 |
-
**Configuration Saving to Snowflake**:
|
| 361 |
-
```python
|
| 362 |
-
# Save with auto-versioning
|
| 363 |
-
session = create_snowflake_session()
|
| 364 |
-
db_manager = UIDatabaseManager(session)
|
| 365 |
-
success = db_manager.save_config(
|
| 366 |
-
config_name=config_name,
|
| 367 |
-
config_data=campaign_config,
|
| 368 |
-
brand=brand
|
| 369 |
-
)
|
| 370 |
-
session.close()
|
| 371 |
-
```
|
| 372 |
-
|
| 373 |
-
**Thread-Safe Progress Handling**:
|
| 374 |
-
- Main thread: Full Streamlit UI updates
|
| 375 |
-
- Worker threads: Console logging only
|
| 376 |
-
- Dummy progress bars prevent Streamlit errors in threads
|
| 377 |
-
|
| 378 |
-
---
|
| 379 |
-
|
| 380 |
-
### Page 2: Message Viewer
|
| 381 |
-
|
| 382 |
-
**Purpose**: Browse, search, and evaluate generated messages with A/B testing awareness
|
| 383 |
-
|
| 384 |
-
**Architecture**: Loads from session_state, feedback stored in-memory
|
| 385 |
-
|
| 386 |
-
#### Key Features
|
| 387 |
-
|
| 388 |
-
**Automatic AB Detection**:
|
| 389 |
-
```python
|
| 390 |
-
def detect_ab_testing_mode():
|
| 391 |
-
"""Detect AB mode from session_state."""
|
| 392 |
-
return (
|
| 393 |
-
'ui_log_data_a' in st.session_state and
|
| 394 |
-
'ui_log_data_b' in st.session_state and
|
| 395 |
-
st.session_state.ui_log_data_a is not None and
|
| 396 |
-
st.session_state.ui_log_data_b is not None
|
| 397 |
-
)
|
| 398 |
-
```
|
| 399 |
-
|
| 400 |
-
**Message Loading**:
|
| 401 |
-
```python
|
| 402 |
-
# Single mode
|
| 403 |
-
def get_single_experiment_data():
|
| 404 |
-
if 'ui_log_data' in st.session_state:
|
| 405 |
-
return st.session_state.ui_log_data
|
| 406 |
-
return None
|
| 407 |
-
|
| 408 |
-
# AB mode - loads both dataframes
|
| 409 |
-
messages_a_df = st.session_state.ui_log_data_a
|
| 410 |
-
messages_b_df = st.session_state.ui_log_data_b
|
| 411 |
-
```
|
| 412 |
-
|
| 413 |
-
**Feedback System**:
|
| 414 |
-
- In-memory storage using `SessionFeedbackManager`
|
| 415 |
-
- Rejection categories: Poor header, Poor body, Grammar, Emoji, Recommendation issues, Similar to previous, etc.
|
| 416 |
-
- Stores full message header and body with feedback
|
| 417 |
-
- Undo rejection capability
|
| 418 |
-
|
| 419 |
-
```python
|
| 420 |
-
# Add feedback
|
| 421 |
-
SessionFeedbackManager.add_feedback(
|
| 422 |
-
experiment_id=experiment_id,
|
| 423 |
-
user_id=user_id,
|
| 424 |
-
stage=stage,
|
| 425 |
-
feedback_type="reject",
|
| 426 |
-
rejection_reason="poor_header",
|
| 427 |
-
rejection_text="Too generic",
|
| 428 |
-
message_header=header,
|
| 429 |
-
message_body=body,
|
| 430 |
-
campaign_name=campaign_name,
|
| 431 |
-
brand=brand,
|
| 432 |
-
config_name=config_name,
|
| 433 |
-
feedback_list_key="current_feedbacks" # or "feedbacks_a", "feedbacks_b"
|
| 434 |
-
)
|
| 435 |
-
```
|
| 436 |
-
|
| 437 |
-
**Store Results to Snowflake** (CRITICAL FEATURE):
|
| 438 |
-
|
| 439 |
-
Located after message viewing section. Button appears prominently.
|
| 440 |
-
|
| 441 |
-
```python
|
| 442 |
-
if st.button("💾 Store Results to Snowflake"):
|
| 443 |
-
session = create_snowflake_session()
|
| 444 |
-
db_manager = UIDatabaseManager(session)
|
| 445 |
-
|
| 446 |
-
# Single mode
|
| 447 |
-
if not ab_mode:
|
| 448 |
-
# Store metadata
|
| 449 |
-
for meta in st.session_state.current_experiment_metadata:
|
| 450 |
-
db_manager.store_experiment_metadata(meta)
|
| 451 |
-
|
| 452 |
-
# Store feedback
|
| 453 |
-
for feedback in st.session_state.current_feedbacks:
|
| 454 |
-
db_manager.store_feedback(feedback)
|
| 455 |
-
|
| 456 |
-
# AB mode
|
| 457 |
-
else:
|
| 458 |
-
# Store both experiments
|
| 459 |
-
for meta in st.session_state.experiment_a_metadata:
|
| 460 |
-
db_manager.store_experiment_metadata(meta)
|
| 461 |
-
for meta in st.session_state.experiment_b_metadata:
|
| 462 |
-
db_manager.store_experiment_metadata(meta)
|
| 463 |
-
|
| 464 |
-
for feedback in st.session_state.feedbacks_a:
|
| 465 |
-
db_manager.store_feedback(feedback)
|
| 466 |
-
for feedback in st.session_state.feedbacks_b:
|
| 467 |
-
db_manager.store_feedback(feedback)
|
| 468 |
-
|
| 469 |
-
session.close()
|
| 470 |
-
st.success("✅ Results stored to Snowflake successfully!")
|
| 471 |
-
st.balloons()
|
| 472 |
-
```
|
| 473 |
-
|
| 474 |
-
**View Modes**:
|
| 475 |
-
- User-Centric: All stages for each user
|
| 476 |
-
- Stage-Centric: All users for each stage
|
| 477 |
-
- Filters: Stage selection, keyword search, pagination
|
| 478 |
-
|
| 479 |
-
---
|
| 480 |
-
|
| 481 |
-
### Page 3: Analytics Dashboard
|
| 482 |
-
|
| 483 |
-
**Purpose**: Visualize performance metrics for CURRENT experiment only
|
| 484 |
-
|
| 485 |
-
**Architecture**: Loads from session_state, uses SessionFeedbackManager
|
| 486 |
-
|
| 487 |
-
#### Key Features
|
| 488 |
-
|
| 489 |
-
**Data Loading**:
|
| 490 |
-
```python
|
| 491 |
-
# Single mode
|
| 492 |
-
def get_single_experiment_data():
|
| 493 |
-
return st.session_state.ui_log_data
|
| 494 |
-
|
| 495 |
-
# AB mode
|
| 496 |
-
detect_ab_testing_mode() # Returns True if AB data exists
|
| 497 |
-
messages_a_df = st.session_state.ui_log_data_a
|
| 498 |
-
messages_b_df = st.session_state.ui_log_data_b
|
| 499 |
-
```
|
| 500 |
-
|
| 501 |
-
**Feedback Stats Calculation**:
|
| 502 |
-
```python
|
| 503 |
-
# Single mode
|
| 504 |
-
feedback_stats = SessionFeedbackManager.get_feedback_stats(
|
| 505 |
-
experiment_id=experiment_id,
|
| 506 |
-
total_messages=len(messages_df),
|
| 507 |
-
feedback_list_key="current_feedbacks"
|
| 508 |
-
)
|
| 509 |
-
|
| 510 |
-
# AB mode
|
| 511 |
-
feedback_stats_a = SessionFeedbackManager.get_feedback_stats(
|
| 512 |
-
experiment_a_id,
|
| 513 |
-
total_messages=len(messages_a_df),
|
| 514 |
-
feedback_list_key="feedbacks_a"
|
| 515 |
-
)
|
| 516 |
-
feedback_stats_b = SessionFeedbackManager.get_feedback_stats(
|
| 517 |
-
experiment_b_id,
|
| 518 |
-
total_messages=len(messages_b_df),
|
| 519 |
-
feedback_list_key="feedbacks_b"
|
| 520 |
-
)
|
| 521 |
-
```
|
| 522 |
-
|
| 523 |
-
**Metrics Displayed**:
|
| 524 |
-
- Overall: Total messages, rejection rate, feedback count
|
| 525 |
-
- Stage-by-Stage: Performance breakdown per stage
|
| 526 |
-
- Rejection Reasons: Pie charts and bar charts
|
| 527 |
-
- AB Comparison: Side-by-side metrics with winner determination
|
| 528 |
-
|
| 529 |
-
**Export Options**:
|
| 530 |
-
- Export current messages to CSV
|
| 531 |
-
- Export current feedback to CSV
|
| 532 |
-
- Export analytics summary to CSV
|
| 533 |
-
|
| 534 |
-
**Important**: Analytics page shows ONLY the current in-memory experiment. For historical data, use Historical Analytics.
|
| 535 |
-
|
| 536 |
-
---
|
| 537 |
-
|
| 538 |
-
### Page 4: Historical Analytics
|
| 539 |
-
|
| 540 |
-
**Purpose**: Track all past experiments and analyze trends from Snowflake
|
| 541 |
-
|
| 542 |
-
**Architecture**: Button-triggered Snowflake queries
|
| 543 |
-
|
| 544 |
-
#### Key Features
|
| 545 |
-
|
| 546 |
-
**Load Button**:
|
| 547 |
-
```python
|
| 548 |
-
if st.button("📊 Load Historical Data from Snowflake"):
|
| 549 |
-
session = create_snowflake_session()
|
| 550 |
-
db_manager = UIDatabaseManager(session)
|
| 551 |
-
|
| 552 |
-
# Load experiment summary with JOIN
|
| 553 |
-
experiments_df = db_manager.get_experiment_summary(brand=brand)
|
| 554 |
-
|
| 555 |
-
st.session_state['historical_experiments'] = experiments_df
|
| 556 |
-
st.session_state['historical_data_loaded'] = True
|
| 557 |
-
|
| 558 |
-
session.close()
|
| 559 |
-
```
|
| 560 |
-
|
| 561 |
-
**SQL Query Example**:
|
| 562 |
-
```sql
|
| 563 |
-
SELECT
|
| 564 |
-
m.EXPERIMENT_ID,
|
| 565 |
-
m.CONFIG_NAME,
|
| 566 |
-
m.CAMPAIGN_NAME,
|
| 567 |
-
m.BRAND,
|
| 568 |
-
MIN(m.START_TIME) as start_time,
|
| 569 |
-
SUM(m.TOTAL_MESSAGES) as total_messages,
|
| 570 |
-
MAX(m.TOTAL_USERS) as total_users,
|
| 571 |
-
COUNT(DISTINCT m.STAGE) as total_stages,
|
| 572 |
-
COUNT(f.FEEDBACK_TYPE) as total_rejects,
|
| 573 |
-
(COUNT(f.FEEDBACK_TYPE) * 100.0 / NULLIF(SUM(m.TOTAL_MESSAGES), 0)) as rejection_rate
|
| 574 |
-
FROM MESSAGING_SYSTEM_V2.UI.EXPERIMENT_METADATA m
|
| 575 |
-
LEFT JOIN MESSAGING_SYSTEM_V2.UI.FEEDBACKS f
|
| 576 |
-
ON m.EXPERIMENT_ID = f.EXPERIMENT_ID
|
| 577 |
-
WHERE m.BRAND = :brand
|
| 578 |
-
GROUP BY m.EXPERIMENT_ID, m.CONFIG_NAME, m.CAMPAIGN_NAME, m.BRAND
|
| 579 |
-
ORDER BY start_time DESC
|
| 580 |
-
```
|
| 581 |
-
|
| 582 |
-
**Visualizations**:
|
| 583 |
-
- Experiments summary table
|
| 584 |
-
- Rejection rate trend over time (line chart)
|
| 585 |
-
- Performance comparison by configuration (bar chart)
|
| 586 |
-
- Best/worst performing configs
|
| 587 |
-
|
| 588 |
-
**Filters**:
|
| 589 |
-
- Date range filtering
|
| 590 |
-
- Automatic refresh button
|
| 591 |
-
|
| 592 |
-
**Export**:
|
| 593 |
-
- Export summary to CSV
|
| 594 |
-
- Note: Detailed feedback export coming soon (use SQL queries for now)
|
| 595 |
-
|
| 596 |
-
---
|
| 597 |
-
|
| 598 |
-
## 🔧 Utility Modules
|
| 599 |
-
|
| 600 |
-
### utils/db_manager.py - UIDatabaseManager
|
| 601 |
-
|
| 602 |
-
**Purpose**: All Snowflake database operations
|
| 603 |
-
|
| 604 |
-
**Key Methods**:
|
| 605 |
-
```python
|
| 606 |
-
class UIDatabaseManager:
|
| 607 |
-
def __init__(self, session: Session):
|
| 608 |
-
"""Initialize with Snowflake session."""
|
| 609 |
-
|
| 610 |
-
def save_config(self, config_name, config_data, brand):
|
| 611 |
-
"""Save config with auto-versioning."""
|
| 612 |
-
|
| 613 |
-
def load_config(self, config_name, brand, version=None):
|
| 614 |
-
"""Load specific config version."""
|
| 615 |
-
|
| 616 |
-
def store_experiment_metadata(self, metadata: dict):
|
| 617 |
-
"""Insert metadata record."""
|
| 618 |
-
|
| 619 |
-
def store_feedback(self, feedback: dict):
|
| 620 |
-
"""Insert feedback record."""
|
| 621 |
-
|
| 622 |
-
def get_experiment_summary(self, brand=None, start_date=None, end_date=None):
|
| 623 |
-
"""Get aggregated experiment metrics with JOIN."""
|
| 624 |
-
|
| 625 |
-
def close(self):
|
| 626 |
-
"""Close Snowflake session."""
|
| 627 |
-
```
|
| 628 |
-
|
| 629 |
-
**Usage Pattern**:
|
| 630 |
-
```python
|
| 631 |
-
# Always use context-like pattern
|
| 632 |
-
session = create_snowflake_session()
|
| 633 |
-
db_manager = UIDatabaseManager(session)
|
| 634 |
-
|
| 635 |
-
try:
|
| 636 |
-
# Do operations
|
| 637 |
-
db_manager.save_config(...)
|
| 638 |
-
db_manager.store_feedback(...)
|
| 639 |
-
finally:
|
| 640 |
-
db_manager.close() # or session.close()
|
| 641 |
-
```
|
| 642 |
-
|
| 643 |
-
---
|
| 644 |
-
|
| 645 |
-
### utils/config_manager.py - ConfigManager
|
| 646 |
-
|
| 647 |
-
**Purpose**: Configuration loading and caching
|
| 648 |
-
|
| 649 |
-
**Key Methods**:
|
| 650 |
-
```python
|
| 651 |
-
class ConfigManager:
|
| 652 |
-
def __init__(self, session: Session):
|
| 653 |
-
"""Initialize with Snowflake session."""
|
| 654 |
-
|
| 655 |
-
def load_configs_from_snowflake(self, brand: str) -> Dict:
|
| 656 |
-
"""Load all configs for brand, returns {name: config_data}."""
|
| 657 |
-
|
| 658 |
-
def get_latest_version(self, config_name: str, brand: str) -> int:
|
| 659 |
-
"""Get latest version number."""
|
| 660 |
-
```
|
| 661 |
-
|
| 662 |
-
**Caching Strategy**:
|
| 663 |
-
- Configs loaded once on app startup
|
| 664 |
-
- Cached in `st.session_state.configs_cache`
|
| 665 |
-
- Format: `{"config_name": {...config_data...}, ...}`
|
| 666 |
-
- No re-querying Snowflake during session
|
| 667 |
-
|
| 668 |
-
---
|
| 669 |
-
|
| 670 |
-
### utils/experiment_runner.py - ExperimentRunner
|
| 671 |
-
|
| 672 |
-
**Purpose**: Execute experiments with proper session management
|
| 673 |
-
|
| 674 |
-
**Key Methods**:
|
| 675 |
-
```python
|
| 676 |
-
class ExperimentRunner:
|
| 677 |
-
def run_single_experiment(
|
| 678 |
-
self, config, sampled_users_df, experiment_id,
|
| 679 |
-
create_session_func, progress_container
|
| 680 |
-
):
|
| 681 |
-
"""Run one experiment, all stages sequentially."""
|
| 682 |
-
|
| 683 |
-
def run_ab_test_parallel(
|
| 684 |
-
self, config_a, config_b, sampled_users_df,
|
| 685 |
-
experiment_a_id, experiment_b_id, create_session_func
|
| 686 |
-
):
|
| 687 |
-
"""Run two experiments in parallel threads."""
|
| 688 |
-
```
|
| 689 |
-
|
| 690 |
-
**Thread-Safe Design**:
|
| 691 |
-
- Each thread gets own Snowflake session
|
| 692 |
-
- Progress updates handled safely:
|
| 693 |
-
- Main thread: Full Streamlit UI
|
| 694 |
-
- Worker threads: Console logging with dummy UI objects
|
| 695 |
-
- No Streamlit context errors
|
| 696 |
-
|
| 697 |
-
**Implementation**:
|
| 698 |
-
```python
|
| 699 |
-
# Thread function
|
| 700 |
-
def run_experiment(exp_key, config, exp_id):
|
| 701 |
-
try:
|
| 702 |
-
success, data, metadata = self.run_single_experiment(
|
| 703 |
-
config=config,
|
| 704 |
-
sampled_users_df=users_df,
|
| 705 |
-
experiment_id=exp_id,
|
| 706 |
-
create_session_func=create_session_func,
|
| 707 |
-
progress_container=None # None = threaded mode
|
| 708 |
-
)
|
| 709 |
-
results[exp_key] = {'success': success, 'ui_log_data': data, 'metadata': metadata}
|
| 710 |
-
except Exception as e:
|
| 711 |
-
results[exp_key] = {'success': False, 'error': str(e)}
|
| 712 |
-
|
| 713 |
-
# Start threads
|
| 714 |
-
thread_a = threading.Thread(target=run_experiment, args=('a', config_a, exp_a_id))
|
| 715 |
-
thread_b = threading.Thread(target=run_experiment, args=('b', config_b, exp_b_id))
|
| 716 |
-
thread_a.start()
|
| 717 |
-
thread_b.start()
|
| 718 |
-
thread_a.join()
|
| 719 |
-
thread_b.join()
|
| 720 |
-
```
|
| 721 |
-
|
| 722 |
-
---
|
| 723 |
-
|
| 724 |
-
### utils/session_feedback_manager.py - SessionFeedbackManager
|
| 725 |
-
|
| 726 |
-
**Purpose**: In-memory feedback management
|
| 727 |
-
|
| 728 |
-
**Static Methods** (no instance needed):
|
| 729 |
-
```python
|
| 730 |
-
@staticmethod
|
| 731 |
-
def add_feedback(experiment_id, user_id, stage, feedback_type,
|
| 732 |
-
rejection_reason, rejection_text, message_header,
|
| 733 |
-
message_body, campaign_name, brand, config_name,
|
| 734 |
-
feedback_list_key):
|
| 735 |
-
"""Add feedback to session_state list."""
|
| 736 |
-
|
| 737 |
-
@staticmethod
|
| 738 |
-
def get_feedback_stats(experiment_id, total_messages, feedback_list_key):
|
| 739 |
-
"""Calculate aggregate stats from feedback list."""
|
| 740 |
-
|
| 741 |
-
@staticmethod
|
| 742 |
-
def get_stage_feedback_stats(experiment_id, messages_df, feedback_list_key):
|
| 743 |
-
"""Calculate per-stage stats."""
|
| 744 |
-
|
| 745 |
-
@staticmethod
|
| 746 |
-
def get_rejection_reason_label(reason_key):
|
| 747 |
-
"""Map reason key to display label."""
|
| 748 |
-
```
|
| 749 |
-
|
| 750 |
-
**Feedback List Keys**:
|
| 751 |
-
- `"current_feedbacks"` - Single mode
|
| 752 |
-
- `"feedbacks_a"` - AB mode experiment A
|
| 753 |
-
- `"feedbacks_b"` - AB mode experiment B
|
| 754 |
-
|
| 755 |
-
**Usage**:
|
| 756 |
-
```python
|
| 757 |
-
# Add feedback
|
| 758 |
-
SessionFeedbackManager.add_feedback(
|
| 759 |
-
experiment_id="drumeo_20260114_1234",
|
| 760 |
-
user_id=12345,
|
| 761 |
-
stage=1,
|
| 762 |
-
feedback_type="reject",
|
| 763 |
-
rejection_reason="poor_header",
|
| 764 |
-
rejection_text="Too generic",
|
| 765 |
-
message_header="Your next lesson 👇",
|
| 766 |
-
message_body="Check it out...",
|
| 767 |
-
campaign_name="Re-engagement",
|
| 768 |
-
brand="drumeo",
|
| 769 |
-
config_name="drumeo_re_engagement_test",
|
| 770 |
-
feedback_list_key="current_feedbacks"
|
| 771 |
-
)
|
| 772 |
-
|
| 773 |
-
# Get stats
|
| 774 |
-
stats = SessionFeedbackManager.get_feedback_stats(
|
| 775 |
-
experiment_id="drumeo_20260114_1234",
|
| 776 |
-
total_messages=100,
|
| 777 |
-
feedback_list_key="current_feedbacks"
|
| 778 |
-
)
|
| 779 |
-
# Returns: {'total_feedback': 10, 'total_rejects': 10,
|
| 780 |
-
# 'reject_rate': 10.0, 'rejection_reasons': {...}}
|
| 781 |
-
```
|
| 782 |
-
|
| 783 |
-
---
|
| 784 |
-
|
| 785 |
-
## 🎨 Brand Theming
|
| 786 |
-
|
| 787 |
-
Theming automatically adjusts based on selected brand:
|
| 788 |
-
|
| 789 |
-
| Brand | Primary Color | Sidebar BG | Accent | Emoji |
|
| 790 |
-
|----------|---------------|------------|----------|-------|
|
| 791 |
-
| Base | Gold | Dark Gold | Gold | 🎵 |
|
| 792 |
-
| Drumeo | Light Blue | Dark Blue | Blue | 🥁 |
|
| 793 |
-
| Pianote | Light Red | Dark Red | Red | 🎹 |
|
| 794 |
-
| Guitareo | Light Green | Dark Green | Green | 🎸 |
|
| 795 |
-
| Singeo | Light Purple | Dark Purple| Purple | 🎤 |
|
| 796 |
-
|
| 797 |
-
**Implementation**: `utils/theme.py`
|
| 798 |
-
|
| 799 |
-
```python
|
| 800 |
-
def get_brand_theme(brand):
|
| 801 |
-
"""Returns theme dictionary."""
|
| 802 |
-
|
| 803 |
-
def apply_theme(brand):
|
| 804 |
-
"""Applies CSS via st.markdown."""
|
| 805 |
-
|
| 806 |
-
def get_brand_emoji(brand):
|
| 807 |
-
"""Returns brand emoji."""
|
| 808 |
-
```
|
| 809 |
-
|
| 810 |
-
---
|
| 811 |
-
|
| 812 |
-
## 🔍 Key Implementation Details
|
| 813 |
-
|
| 814 |
-
### Configuration File Structure
|
| 815 |
-
|
| 816 |
-
```json
|
| 817 |
-
{
|
| 818 |
-
"brand": "drumeo",
|
| 819 |
-
"campaign_type": "re_engagement",
|
| 820 |
-
"campaign_name": "UI-Test-Campaign",
|
| 821 |
-
"campaign_instructions": "Keep messages encouraging and motivational.",
|
| 822 |
-
"1": {
|
| 823 |
-
"stage": 1,
|
| 824 |
-
"model": "gemini-2.5-flash-lite",
|
| 825 |
-
"personalization": true,
|
| 826 |
-
"involve_recsys_result": true,
|
| 827 |
-
"recsys_contents": ["workout", "course", "quick_tips"],
|
| 828 |
-
"specific_content_id": null,
|
| 829 |
-
"segment_info": "Students inactive for 3+ days",
|
| 830 |
-
"instructions": "",
|
| 831 |
-
"sample_examples": "Header: Your next lesson 👇\nMessage: Check it out!",
|
| 832 |
-
"identifier_column": "user_id",
|
| 833 |
-
"platform": "push"
|
| 834 |
-
},
|
| 835 |
-
"2": {
|
| 836 |
-
"stage": 2,
|
| 837 |
-
"model": "gpt-4o-mini",
|
| 838 |
-
"personalization": true,
|
| 839 |
-
"involve_recsys_result": true,
|
| 840 |
-
"recsys_contents": ["song"],
|
| 841 |
-
"specific_content_id": 12345,
|
| 842 |
-
"segment_info": "Students inactive for 7+ days",
|
| 843 |
-
"instructions": "Focus on easy songs",
|
| 844 |
-
"sample_examples": "Header: Let's jam! 🎸\nMessage: Try this song!",
|
| 845 |
-
"identifier_column": "user_id",
|
| 846 |
-
"platform": "push"
|
| 847 |
-
}
|
| 848 |
-
}
|
| 849 |
-
```
|
| 850 |
-
|
| 851 |
-
### Rejection Reason Categories
|
| 852 |
-
|
| 853 |
-
```python
|
| 854 |
-
REJECTION_REASONS = {
|
| 855 |
-
"poor_header": "Poor Header",
|
| 856 |
-
"poor_body": "Poor Body/Content",
|
| 857 |
-
"grammar_issues": "Grammar Issues",
|
| 858 |
-
"emoji_problems": "Emoji Problems",
|
| 859 |
-
"recommendation_issues": "Recommendation Issues",
|
| 860 |
-
"wrong_information": "Wrong/Inaccurate Information",
|
| 861 |
-
"tone_issues": "Tone Issues",
|
| 862 |
-
"similarity": "Similar To Previous Header/Messages",
|
| 863 |
-
"other": "Other"
|
| 864 |
-
}
|
| 865 |
-
```
|
| 866 |
-
|
| 867 |
-
### Environment Variables Required
|
| 868 |
-
|
| 869 |
-
```bash
|
| 870 |
-
# Snowflake
|
| 871 |
-
SNOWFLAKE_USER=your_user
|
| 872 |
-
SNOWFLAKE_PASSWORD=your_password
|
| 873 |
-
SNOWFLAKE_ACCOUNT=your_account
|
| 874 |
-
SNOWFLAKE_ROLE=your_role
|
| 875 |
-
SNOWFLAKE_DATABASE=MESSAGING_SYSTEM_V2
|
| 876 |
-
SNOWFLAKE_WAREHOUSE=your_warehouse
|
| 877 |
-
SNOWFLAKE_SCHEMA=UI
|
| 878 |
-
|
| 879 |
-
# LLM APIs
|
| 880 |
-
OPENAI_API_KEY=sk-...
|
| 881 |
-
GOOGLE_API_KEY=AIza...
|
| 882 |
-
```
|
| 883 |
-
|
| 884 |
-
---
|
| 885 |
-
|
| 886 |
-
## 🛠️ Development Guide
|
| 887 |
-
|
| 888 |
-
### Adding a New Page
|
| 889 |
-
|
| 890 |
-
1. Create `pages/{number}_{Name}.py`
|
| 891 |
-
2. Follow standard structure:
|
| 892 |
-
```python
|
| 893 |
-
import streamlit as st
|
| 894 |
-
import os
|
| 895 |
-
from pathlib import Path
|
| 896 |
-
from dotenv import load_dotenv
|
| 897 |
-
|
| 898 |
-
# Load .env
|
| 899 |
-
env_path = Path(__file__).parent.parent.parent / '.env'
|
| 900 |
-
if env_path.exists():
|
| 901 |
-
load_dotenv(env_path)
|
| 902 |
-
|
| 903 |
-
from utils.auth import check_authentication
|
| 904 |
-
from utils.theme import apply_theme, get_brand_emoji
|
| 905 |
-
|
| 906 |
-
st.set_page_config(page_title="New Page", page_icon="🆕", layout="wide")
|
| 907 |
-
|
| 908 |
-
if not check_authentication():
|
| 909 |
-
st.error("🔒 Please login first")
|
| 910 |
-
st.stop()
|
| 911 |
-
|
| 912 |
-
if "selected_brand" not in st.session_state:
|
| 913 |
-
st.error("⚠️ Please select a brand first")
|
| 914 |
-
st.stop()
|
| 915 |
-
|
| 916 |
-
brand = st.session_state.selected_brand
|
| 917 |
-
apply_theme(brand)
|
| 918 |
-
|
| 919 |
-
# Your content here
|
| 920 |
-
```
|
| 921 |
-
3. Update `app.py` navigation
|
| 922 |
-
4. Test with all brands
|
| 923 |
-
|
| 924 |
-
### Adding a New Feedback Category
|
| 925 |
-
|
| 926 |
-
1. Update `utils/session_feedback_manager.py`:
|
| 927 |
-
```python
|
| 928 |
-
REJECTION_REASONS = {
|
| 929 |
-
# ... existing
|
| 930 |
-
"new_reason": "New Reason Label"
|
| 931 |
-
}
|
| 932 |
-
```
|
| 933 |
-
2. Automatically appears in UI
|
| 934 |
-
|
| 935 |
-
### Adding a New Brand
|
| 936 |
-
|
| 937 |
-
1. Create `data/UI_users/{brand}_users.csv` (100 users, must have `USER_ID` column)
|
| 938 |
-
2. Add to `utils/theme.py`:
|
| 939 |
-
```python
|
| 940 |
-
BRAND_THEMES["newbrand"] = {
|
| 941 |
-
"primary": "#COLOR",
|
| 942 |
-
"accent": "#COLOR",
|
| 943 |
-
"sidebar_bg": "#DARK_COLOR",
|
| 944 |
-
"text": "#FFFFFF"
|
| 945 |
-
}
|
| 946 |
-
BRAND_EMOJIS["newbrand"] = "🆕"
|
| 947 |
-
```
|
| 948 |
-
3. Update `app.py`:
|
| 949 |
-
```python
|
| 950 |
-
brands = ["drumeo", "pianote", "guitareo", "singeo", "newbrand"]
|
| 951 |
-
brand_labels["newbrand"] = "🆕 New Brand"
|
| 952 |
-
```
|
| 953 |
-
4. Create default config in Snowflake or via UI
|
| 954 |
-
|
| 955 |
-
### Debugging Tips
|
| 956 |
-
|
| 957 |
-
**Common Issues**:
|
| 958 |
-
|
| 959 |
-
1. **"No messages found"** in Message Viewer:
|
| 960 |
-
- Check `st.session_state.ui_log_data` exists
|
| 961 |
-
- Verify generation completed in Campaign Builder
|
| 962 |
-
- Look for errors in terminal
|
| 963 |
-
|
| 964 |
-
2. **Snowflake connection errors**:
|
| 965 |
-
- Verify `.env` file exists and is loaded
|
| 966 |
-
- Check credentials are correct
|
| 967 |
-
- Test connection: `create_snowflake_session()`
|
| 968 |
-
|
| 969 |
-
3. **AB test AttributeError**:
|
| 970 |
-
- Fixed in latest version
|
| 971 |
-
- Ensure ExperimentRunner uses thread-safe progress handling
|
| 972 |
-
|
| 973 |
-
4. **Config save errors with quotes**:
|
| 974 |
-
- Fixed: Now uses `write_pandas()` instead of raw SQL
|
| 975 |
-
- Handles JSON with apostrophes correctly
|
| 976 |
-
|
| 977 |
-
5. **Feedback not in Analytics**:
|
| 978 |
-
- Check `st.session_state.current_feedbacks` has data
|
| 979 |
-
- Verify correct feedback_list_key used
|
| 980 |
-
- Check experiment_id matches
|
| 981 |
-
|
| 982 |
-
**Debugging Code**:
|
| 983 |
-
```python
|
| 984 |
-
# Debug session state
|
| 985 |
-
with st.expander("Debug Info"):
|
| 986 |
-
st.write("Session State Keys:", list(st.session_state.keys()))
|
| 987 |
-
if 'ui_log_data' in st.session_state:
|
| 988 |
-
st.write("Messages shape:", st.session_state.ui_log_data.shape)
|
| 989 |
-
if 'current_feedbacks' in st.session_state:
|
| 990 |
-
st.write("Feedback count:", len(st.session_state.current_feedbacks))
|
| 991 |
-
```
|
| 992 |
-
|
| 993 |
-
---
|
| 994 |
-
|
| 995 |
-
## 🎯 Key Design Decisions
|
| 996 |
-
|
| 997 |
-
### 1. In-Memory + Cloud Hybrid
|
| 998 |
-
**Decision**: Use session_state for active data, Snowflake for persistence
|
| 999 |
-
**Rationale**:
|
| 1000 |
-
- Fast in-memory operations
|
| 1001 |
-
- No local file dependencies (HuggingFace ready)
|
| 1002 |
-
- Scalable historical storage
|
| 1003 |
-
- Clean separation: current vs. historical
|
| 1004 |
-
|
| 1005 |
-
### 2. One-Click Storage
|
| 1006 |
-
**Decision**: Single "Store Results" button to persist everything
|
| 1007 |
-
**Rationale**:
|
| 1008 |
-
- Simple user workflow
|
| 1009 |
-
- Explicit persistence action
|
| 1010 |
-
- User controls when data is saved
|
| 1011 |
-
- No auto-save surprises
|
| 1012 |
-
|
| 1013 |
-
### 3. Config Caching
|
| 1014 |
-
**Decision**: Load all configs once, cache in session_state
|
| 1015 |
-
**Rationale**:
|
| 1016 |
-
- Reduces Snowflake queries
|
| 1017 |
-
- Faster config switching
|
| 1018 |
-
- Session-scoped cache (fresh on page load)
|
| 1019 |
-
- No stale data issues
|
| 1020 |
-
|
| 1021 |
-
### 4. Thread-Safe AB Testing
|
| 1022 |
-
**Decision**: Separate Snowflake sessions per thread, console logging
|
| 1023 |
-
**Rationale**:
|
| 1024 |
-
- Prevents session conflicts
|
| 1025 |
-
- Streamlit UI only in main thread
|
| 1026 |
-
- Clean error handling
|
| 1027 |
-
- Production-ready parallel execution
|
| 1028 |
-
|
| 1029 |
-
### 5. Versioned Configurations
|
| 1030 |
-
**Decision**: Auto-increment version on every config save
|
| 1031 |
-
**Rationale**:
|
| 1032 |
-
- Full audit trail
|
| 1033 |
-
- Can rollback to previous versions
|
| 1034 |
-
- Supports experimentation
|
| 1035 |
-
- No data loss
|
| 1036 |
-
|
| 1037 |
-
### 6. Button-Triggered Historical Loading
|
| 1038 |
-
**Decision**: Historical Analytics loads on button click, not auto
|
| 1039 |
-
**Rationale**:
|
| 1040 |
-
- User controls when to query Snowflake
|
| 1041 |
-
- Avoids unnecessary queries
|
| 1042 |
-
- Faster page load
|
| 1043 |
-
- Clear user action
|
| 1044 |
-
|
| 1045 |
-
### 7. SessionFeedbackManager Static Methods
|
| 1046 |
-
**Decision**: All methods static, no instance needed
|
| 1047 |
-
**Rationale**:
|
| 1048 |
-
- Simpler API
|
| 1049 |
-
- Works directly with session_state
|
| 1050 |
-
- No state to manage
|
| 1051 |
-
- Cleaner code
|
| 1052 |
-
|
| 1053 |
-
---
|
| 1054 |
-
|
| 1055 |
-
## 🚀 Deployment Guide
|
| 1056 |
-
|
| 1057 |
-
### HuggingFace Spaces Deployment
|
| 1058 |
-
|
| 1059 |
-
**Requirements**:
|
| 1060 |
-
- No local file dependencies ✅
|
| 1061 |
-
- Environment variables for secrets ✅
|
| 1062 |
-
- Snowflake connectivity ✅
|
| 1063 |
-
- CSV files in repo (data/UI_users/) ✅
|
| 1064 |
-
|
| 1065 |
-
**Steps**:
|
| 1066 |
-
1. Push code to GitHub/HuggingFace repo
|
| 1067 |
-
2. Include `data/UI_users/` CSV files
|
| 1068 |
-
3. Set environment variables in Space settings:
|
| 1069 |
-
- All SNOWFLAKE_* variables
|
| 1070 |
-
- All API keys
|
| 1071 |
-
4. Run: `streamlit run app.py`
|
| 1072 |
-
5. Verify Snowflake connection works
|
| 1073 |
-
|
| 1074 |
-
**Files to Exclude**:
|
| 1075 |
-
- `.env` (use Space secrets instead)
|
| 1076 |
-
- Local cache directories
|
| 1077 |
-
- Test data
|
| 1078 |
-
|
| 1079 |
-
---
|
| 1080 |
-
|
| 1081 |
-
## 📞 Support & Resources
|
| 1082 |
-
|
| 1083 |
-
**Contact**:
|
| 1084 |
-
- Technical Support: danial@musora.com
|
| 1085 |
-
|
| 1086 |
-
**Related Documentation**:
|
| 1087 |
-
- Main System: `ai_messaging_system_v2/README.md`
|
| 1088 |
-
- UI Mode Guide: `ai_messaging_system_v2/UI_MODE_GUIDE.md`
|
| 1089 |
-
- Implementation Details: `visualization/IMPLEMENTATION_COMPLETE.md`
|
| 1090 |
-
- Refactoring Guide: `visualization/ARCHITECTURE_REFACTOR_GUIDE.md`
|
| 1091 |
-
|
| 1092 |
-
**Useful Links**:
|
| 1093 |
-
- Streamlit Documentation: https://docs.streamlit.io
|
| 1094 |
-
- Snowflake Python Connector: https://docs.snowflake.com/en/developer-guide/python-connector/python-connector
|
| 1095 |
-
- Plotly Charts: https://plotly.com/python/
|
| 1096 |
-
|
| 1097 |
-
---
|
| 1098 |
-
|
| 1099 |
-
## 📊 System Status
|
| 1100 |
-
|
| 1101 |
-
**Completion**: 100% ✅
|
| 1102 |
-
|
| 1103 |
-
**Completed Components**:
|
| 1104 |
-
1. ✅ Database Layer (db_manager.py)
|
| 1105 |
-
2. ✅ Config Manager (config_manager.py)
|
| 1106 |
-
3. ✅ Session Feedback Manager (session_feedback_manager.py)
|
| 1107 |
-
4. ✅ Experiment Runner (experiment_runner.py)
|
| 1108 |
-
5. ✅ App.py - Authentication & config loading
|
| 1109 |
-
6. ✅ Campaign Builder - Generation & AB testing
|
| 1110 |
-
7. ✅ Message Viewer - Viewing & feedback
|
| 1111 |
-
8. ✅ Analytics - Current experiment metrics
|
| 1112 |
-
9. ✅ Historical Analytics - Snowflake integration
|
| 1113 |
-
|
| 1114 |
-
**Recent Fixes**:
|
| 1115 |
-
- ✅ Configuration save error (JSON escaping) - Fixed with `write_pandas()`
|
| 1116 |
-
- ✅ AB testing `AttributeError: enter` - Fixed with thread-safe design
|
| 1117 |
-
- ✅ Historical Analytics Snowflake connection - Fixed to use `.env`
|
| 1118 |
-
|
| 1119 |
-
**Ready For**:
|
| 1120 |
-
- ✅ Production use
|
| 1121 |
-
- ✅ HuggingFace deployment
|
| 1122 |
-
- ✅ End-to-end testing
|
| 1123 |
-
- ✅ Team onboarding
|
| 1124 |
-
|
| 1125 |
-
---
|
| 1126 |
-
|
| 1127 |
-
**Built with ❤️ for the Musora team**
|
| 1128 |
-
|
| 1129 |
-
**Last Updated**: 2026-01-14
|
| 1130 |
-
**Version**: 2.0 (Refactored Architecture)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/app.py
DELETED
|
@@ -1,384 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
AI Messaging System - Visualization Tool
|
| 3 |
-
Main entry point with authentication and brand selection.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import streamlit as st
|
| 7 |
-
import sys
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
from dotenv import load_dotenv
|
| 10 |
-
|
| 11 |
-
# Load environment variables from .env file FIRST
|
| 12 |
-
env_path = Path(__file__).parent / '.env'
|
| 13 |
-
if env_path.exists():
|
| 14 |
-
load_dotenv(env_path)
|
| 15 |
-
else:
|
| 16 |
-
# Try parent directory .env as fallback
|
| 17 |
-
parent_env_path = Path(__file__).parent.parent / '.env'
|
| 18 |
-
if parent_env_path.exists():
|
| 19 |
-
load_dotenv(parent_env_path)
|
| 20 |
-
|
| 21 |
-
# Add parent directory to path
|
| 22 |
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 23 |
-
|
| 24 |
-
from utils.auth import verify_login, check_authentication, get_current_user, logout
|
| 25 |
-
from utils.theme import apply_theme, get_brand_emoji, get_brand_theme
|
| 26 |
-
from utils.data_loader import DataLoader
|
| 27 |
-
from utils.config_manager import ConfigManager
|
| 28 |
-
from snowflake.snowpark import Session
|
| 29 |
-
import os
|
| 30 |
-
|
| 31 |
-
# Page configuration
|
| 32 |
-
st.set_page_config(
|
| 33 |
-
page_title="AI Messaging System - Visualization",
|
| 34 |
-
page_icon="🎵",
|
| 35 |
-
layout="wide",
|
| 36 |
-
initial_sidebar_state="expanded"
|
| 37 |
-
)
|
| 38 |
-
|
| 39 |
-
# Helper function to create Snowflake session
|
| 40 |
-
def create_snowflake_session() -> Session:
|
| 41 |
-
"""Create a Snowflake session using environment variables."""
|
| 42 |
-
conn_params = {
|
| 43 |
-
"user": os.getenv("SNOWFLAKE_USER"),
|
| 44 |
-
"password": os.getenv("SNOWFLAKE_PASSWORD"),
|
| 45 |
-
"account": os.getenv("SNOWFLAKE_ACCOUNT"),
|
| 46 |
-
"role": os.getenv("SNOWFLAKE_ROLE"),
|
| 47 |
-
"database": os.getenv("SNOWFLAKE_DATABASE"),
|
| 48 |
-
"warehouse": os.getenv("SNOWFLAKE_WAREHOUSE"),
|
| 49 |
-
"schema": os.getenv("SNOWFLAKE_SCHEMA"),
|
| 50 |
-
}
|
| 51 |
-
return Session.builder.configs(conn_params).create()
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
# Initialize session state
|
| 55 |
-
def init_session_state():
|
| 56 |
-
"""Initialize session state variables."""
|
| 57 |
-
defaults = {
|
| 58 |
-
# Authentication
|
| 59 |
-
"authenticated": False,
|
| 60 |
-
"user_email": "",
|
| 61 |
-
|
| 62 |
-
# Brand and configs
|
| 63 |
-
"selected_brand": None,
|
| 64 |
-
"cached_configs": {}, # Cache configs per brand: {brand: {config_name: config_data}}
|
| 65 |
-
"configs_loaded": False,
|
| 66 |
-
|
| 67 |
-
# Current experiment data (in-memory)
|
| 68 |
-
"current_experiment_id": None,
|
| 69 |
-
"ui_log_data": None, # Accumulated dataframe for multi-stage messages
|
| 70 |
-
"current_experiment_metadata": [], # List of metadata dicts per stage
|
| 71 |
-
"current_feedbacks": [], # List of feedback dicts
|
| 72 |
-
|
| 73 |
-
# AB Testing data
|
| 74 |
-
"ab_testing_mode": False,
|
| 75 |
-
"ui_log_data_a": None,
|
| 76 |
-
"ui_log_data_b": None,
|
| 77 |
-
"experiment_a_id": None,
|
| 78 |
-
"experiment_b_id": None,
|
| 79 |
-
"experiment_a_metadata": [],
|
| 80 |
-
"experiment_b_metadata": [],
|
| 81 |
-
"feedbacks_a": [],
|
| 82 |
-
"feedbacks_b": [],
|
| 83 |
-
}
|
| 84 |
-
for key, value in defaults.items():
|
| 85 |
-
if key not in st.session_state:
|
| 86 |
-
st.session_state[key] = value
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
init_session_state()
|
| 90 |
-
|
| 91 |
-
# Authentication check
|
| 92 |
-
if not check_authentication():
|
| 93 |
-
# Show login page
|
| 94 |
-
st.title("🔐 AI Messaging System - Login")
|
| 95 |
-
|
| 96 |
-
st.markdown("""
|
| 97 |
-
Welcome to the **AI Messaging System Visualization Tool**!
|
| 98 |
-
|
| 99 |
-
This tool enables you to:
|
| 100 |
-
- 🏗️ Build and configure message campaigns
|
| 101 |
-
- 👀 Visualize generated messages across all stages
|
| 102 |
-
- 📊 Analyze performance and provide feedback
|
| 103 |
-
- 🧪 Run A/B tests to compare different approaches
|
| 104 |
-
|
| 105 |
-
---
|
| 106 |
-
|
| 107 |
-
**Access is restricted to authorized team members only.**
|
| 108 |
-
Please enter your credentials below.
|
| 109 |
-
""")
|
| 110 |
-
|
| 111 |
-
with st.form("login_form"):
|
| 112 |
-
email = st.text_input("📧 Email Address", placeholder="your.name@musora.com")
|
| 113 |
-
token = st.text_input("🔑 Access Token", type="password", placeholder="Enter your access token")
|
| 114 |
-
submit = st.form_submit_button("🚀 Login", use_container_width=True)
|
| 115 |
-
|
| 116 |
-
if submit:
|
| 117 |
-
if verify_login(email, token):
|
| 118 |
-
st.session_state.authenticated = True
|
| 119 |
-
st.session_state.user_email = email
|
| 120 |
-
st.success("✅ Login successful! Redirecting...")
|
| 121 |
-
st.rerun()
|
| 122 |
-
else:
|
| 123 |
-
st.error("❌ Invalid email or access token. Please try again.")
|
| 124 |
-
|
| 125 |
-
st.stop()
|
| 126 |
-
|
| 127 |
-
# User is authenticated - show main app
|
| 128 |
-
# Apply base theme initially
|
| 129 |
-
apply_theme("base")
|
| 130 |
-
|
| 131 |
-
# Sidebar - Brand Selection
|
| 132 |
-
with st.sidebar:
|
| 133 |
-
st.title("🎵 AI Messaging System")
|
| 134 |
-
|
| 135 |
-
st.markdown(f"**Logged in as:** {get_current_user()}")
|
| 136 |
-
|
| 137 |
-
if st.button("🚪 Logout", use_container_width=True):
|
| 138 |
-
logout()
|
| 139 |
-
st.rerun()
|
| 140 |
-
|
| 141 |
-
st.markdown("---")
|
| 142 |
-
|
| 143 |
-
# Brand Selection
|
| 144 |
-
st.subheader("🎨 Select Brand")
|
| 145 |
-
|
| 146 |
-
brands = ["drumeo", "pianote", "guitareo", "singeo"]
|
| 147 |
-
brand_labels = {
|
| 148 |
-
"drumeo": "🥁 Drumeo",
|
| 149 |
-
"pianote": "🎹 Pianote",
|
| 150 |
-
"guitareo": "🎸 Guitareo",
|
| 151 |
-
"singeo": "🎤 Singeo"
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
# Get current brand from session state if exists
|
| 155 |
-
current_brand = st.session_state.get("selected_brand", brands[0])
|
| 156 |
-
|
| 157 |
-
selected_brand = st.selectbox(
|
| 158 |
-
"Brand",
|
| 159 |
-
brands,
|
| 160 |
-
index=brands.index(current_brand) if current_brand in brands else 0,
|
| 161 |
-
format_func=lambda x: brand_labels[x],
|
| 162 |
-
key="brand_selector",
|
| 163 |
-
label_visibility="collapsed"
|
| 164 |
-
)
|
| 165 |
-
|
| 166 |
-
# Check if brand changed
|
| 167 |
-
brand_changed = (st.session_state.selected_brand != selected_brand)
|
| 168 |
-
|
| 169 |
-
# Update session state with selected brand
|
| 170 |
-
st.session_state.selected_brand = selected_brand
|
| 171 |
-
|
| 172 |
-
# Load configs from Snowflake if brand changed or not loaded yet
|
| 173 |
-
if brand_changed or not st.session_state.get("configs_loaded", False):
|
| 174 |
-
if selected_brand:
|
| 175 |
-
with st.spinner(f"Loading configurations for {selected_brand}..."):
|
| 176 |
-
try:
|
| 177 |
-
# Create Snowflake session for config loading
|
| 178 |
-
session = create_snowflake_session()
|
| 179 |
-
|
| 180 |
-
# Initialize config manager with session
|
| 181 |
-
config_manager = ConfigManager(session)
|
| 182 |
-
|
| 183 |
-
# Load all configs for this brand
|
| 184 |
-
configs = config_manager.get_all_configs(selected_brand)
|
| 185 |
-
|
| 186 |
-
# Cache in session state
|
| 187 |
-
if selected_brand not in st.session_state.cached_configs:
|
| 188 |
-
st.session_state.cached_configs[selected_brand] = {}
|
| 189 |
-
st.session_state.cached_configs[selected_brand] = configs
|
| 190 |
-
st.session_state.configs_loaded = True
|
| 191 |
-
|
| 192 |
-
# Close session after loading
|
| 193 |
-
session.close()
|
| 194 |
-
|
| 195 |
-
st.success(f"✅ Loaded {len(configs)} configurations from Snowflake")
|
| 196 |
-
|
| 197 |
-
except Exception as e:
|
| 198 |
-
st.error(f"❌ Error loading configs from Snowflake: {e}")
|
| 199 |
-
st.info("💡 Make sure Snowflake credentials are set in .env file")
|
| 200 |
-
|
| 201 |
-
# Apply brand theme
|
| 202 |
-
if selected_brand:
|
| 203 |
-
apply_theme(selected_brand)
|
| 204 |
-
|
| 205 |
-
st.markdown("---")
|
| 206 |
-
|
| 207 |
-
# Navigation info
|
| 208 |
-
st.subheader("📖 Navigation")
|
| 209 |
-
st.markdown("""
|
| 210 |
-
Use the pages in the sidebar to navigate:
|
| 211 |
-
- **Campaign Builder**: Create campaigns & A/B tests
|
| 212 |
-
- **Message Viewer**: Review messages
|
| 213 |
-
- **Analytics**: View current performance
|
| 214 |
-
- **Historical Analytics**: Track improvements
|
| 215 |
-
""")
|
| 216 |
-
|
| 217 |
-
# Main Content Area
|
| 218 |
-
if not selected_brand:
|
| 219 |
-
st.title("🎵 Welcome to AI Messaging System")
|
| 220 |
-
|
| 221 |
-
st.markdown("""
|
| 222 |
-
### Get Started
|
| 223 |
-
|
| 224 |
-
**Please select a brand from the sidebar to begin.**
|
| 225 |
-
|
| 226 |
-
Once you've selected a brand, you can:
|
| 227 |
-
|
| 228 |
-
1. **Build Campaigns** - Configure and generate personalized messages
|
| 229 |
-
2. **View Messages** - Browse and provide feedback on generated messages
|
| 230 |
-
3. **Run A/B Tests** - Compare different configurations side-by-side
|
| 231 |
-
4. **Analyze Results** - Review performance metrics and insights
|
| 232 |
-
""")
|
| 233 |
-
|
| 234 |
-
# Show feature cards
|
| 235 |
-
col1, col2 = st.columns(2)
|
| 236 |
-
|
| 237 |
-
with col1:
|
| 238 |
-
st.markdown("""
|
| 239 |
-
#### 🏗️ Campaign Builder
|
| 240 |
-
- Configure multi-stage campaigns
|
| 241 |
-
- Built-in A/B testing mode
|
| 242 |
-
- Parallel experiment processing
|
| 243 |
-
- Automatic experiment archiving
|
| 244 |
-
- Save custom configurations
|
| 245 |
-
""")
|
| 246 |
-
|
| 247 |
-
st.markdown("""
|
| 248 |
-
#### 👀 Message Viewer
|
| 249 |
-
- View all generated messages
|
| 250 |
-
- Side-by-side A/B comparison
|
| 251 |
-
- User-centric or stage-centric views
|
| 252 |
-
- Search and filter messages
|
| 253 |
-
- Provide detailed feedback
|
| 254 |
-
""")
|
| 255 |
-
|
| 256 |
-
with col2:
|
| 257 |
-
st.markdown("""
|
| 258 |
-
#### 📊 Analytics Dashboard
|
| 259 |
-
- Real-time performance metrics
|
| 260 |
-
- A/B test winner determination
|
| 261 |
-
- Rejection reason analysis
|
| 262 |
-
- Stage-by-stage performance
|
| 263 |
-
- Export capabilities
|
| 264 |
-
""")
|
| 265 |
-
|
| 266 |
-
st.markdown("""
|
| 267 |
-
#### 📚 Historical Analytics
|
| 268 |
-
- Track all past experiments
|
| 269 |
-
- Rejection rate trends over time
|
| 270 |
-
- Compare historical A/B tests
|
| 271 |
-
- Export historical data
|
| 272 |
-
""")
|
| 273 |
-
|
| 274 |
-
else:
|
| 275 |
-
# Brand is selected - show brand-specific homepage
|
| 276 |
-
emoji = get_brand_emoji(selected_brand)
|
| 277 |
-
theme = get_brand_theme(selected_brand)
|
| 278 |
-
|
| 279 |
-
st.title(f"{emoji} {selected_brand.title()} - AI Messaging System")
|
| 280 |
-
|
| 281 |
-
st.markdown(f"""
|
| 282 |
-
### Welcome to {selected_brand.title()} Message Generation!
|
| 283 |
-
|
| 284 |
-
You're all set to create personalized messages for {selected_brand.title()} users.
|
| 285 |
-
""")
|
| 286 |
-
|
| 287 |
-
# Quick stats
|
| 288 |
-
data_loader = DataLoader()
|
| 289 |
-
|
| 290 |
-
# Check if we have brand users
|
| 291 |
-
has_users = data_loader.has_brand_users(selected_brand)
|
| 292 |
-
|
| 293 |
-
# Check if we have generated messages
|
| 294 |
-
messages_df = data_loader.load_generated_messages()
|
| 295 |
-
has_messages = messages_df is not None and len(messages_df) > 0
|
| 296 |
-
|
| 297 |
-
st.markdown("---")
|
| 298 |
-
|
| 299 |
-
# Status cards
|
| 300 |
-
col1, col2, col3 = st.columns(3)
|
| 301 |
-
|
| 302 |
-
with col1:
|
| 303 |
-
st.markdown("### 👥 Available Users")
|
| 304 |
-
if has_users:
|
| 305 |
-
user_count = data_loader.get_brand_user_count(selected_brand)
|
| 306 |
-
st.metric("Users Available", user_count)
|
| 307 |
-
st.success(f"✅ {user_count} users ready")
|
| 308 |
-
else:
|
| 309 |
-
st.metric("Users Available", 0)
|
| 310 |
-
st.info("ℹ️ No users available")
|
| 311 |
-
|
| 312 |
-
with col2:
|
| 313 |
-
st.markdown("### 📨 Generated Messages")
|
| 314 |
-
if has_messages:
|
| 315 |
-
stats = data_loader.get_message_stats()
|
| 316 |
-
st.metric("Total Messages", stats['total_messages'])
|
| 317 |
-
st.success(f"✅ {stats['total_stages']} stages")
|
| 318 |
-
else:
|
| 319 |
-
st.metric("Total Messages", 0)
|
| 320 |
-
st.info("ℹ️ No messages generated yet")
|
| 321 |
-
|
| 322 |
-
with col3:
|
| 323 |
-
st.markdown("### 🎯 Quick Actions")
|
| 324 |
-
st.markdown("") # spacing
|
| 325 |
-
if st.button("🏗️ Build Campaign", use_container_width=True):
|
| 326 |
-
st.switch_page("pages/1_Campaign_Builder.py")
|
| 327 |
-
if has_messages:
|
| 328 |
-
if st.button("👀 View Messages", use_container_width=True):
|
| 329 |
-
st.switch_page("pages/2_Message_Viewer.py")
|
| 330 |
-
|
| 331 |
-
st.markdown("---")
|
| 332 |
-
|
| 333 |
-
# Getting started guide
|
| 334 |
-
st.markdown("### 🚀 Quick Start Guide")
|
| 335 |
-
|
| 336 |
-
st.markdown("""
|
| 337 |
-
#### Step 1: Build a Campaign
|
| 338 |
-
Navigate to **Campaign Builder** to:
|
| 339 |
-
- Toggle A/B testing mode on/off as needed
|
| 340 |
-
- Select number of users to experiment with (1-25)
|
| 341 |
-
- Configure campaign stages (or two experiments side-by-side)
|
| 342 |
-
- Set LLM models and instructions
|
| 343 |
-
- Generate personalized messages with automatic archiving
|
| 344 |
-
|
| 345 |
-
#### Step 2: Review Messages
|
| 346 |
-
Go to **Message Viewer** to:
|
| 347 |
-
- Browse all generated messages
|
| 348 |
-
- View A/B tests side-by-side automatically
|
| 349 |
-
- Switch between user-centric and stage-centric views
|
| 350 |
-
- Provide detailed feedback with rejection reasons
|
| 351 |
-
- Track message headers and content
|
| 352 |
-
|
| 353 |
-
#### Step 3: Analyze Performance
|
| 354 |
-
Check **Analytics Dashboard** for:
|
| 355 |
-
- Real-time approval and rejection rates
|
| 356 |
-
- A/B test comparisons with winner determination
|
| 357 |
-
- Common rejection reasons with visual charts
|
| 358 |
-
- Stage-by-stage performance insights
|
| 359 |
-
- Export analytics data
|
| 360 |
-
|
| 361 |
-
#### Step 4: Track Improvements
|
| 362 |
-
View **Historical Analytics** to:
|
| 363 |
-
- See all past experiments over time
|
| 364 |
-
- Identify rejection rate trends
|
| 365 |
-
- Compare historical A/B tests
|
| 366 |
-
- Export comprehensive historical data
|
| 367 |
-
""")
|
| 368 |
-
|
| 369 |
-
st.markdown("---")
|
| 370 |
-
|
| 371 |
-
# Tips
|
| 372 |
-
with st.expander("💡 Tips & Best Practices"):
|
| 373 |
-
st.markdown("""
|
| 374 |
-
- **User Selection**: Select 1-25 users for quick experimentation
|
| 375 |
-
- **Configuration Templates**: Start with default configurations and customize as needed
|
| 376 |
-
- **A/B Testing**: Enable A/B mode in Campaign Builder to compare two configurations in parallel
|
| 377 |
-
- **Feedback**: Reject poor messages with specific categories including the new 'Similar To Previous' option
|
| 378 |
-
- **Historical Tracking**: Check Historical Analytics regularly to track improvements over time
|
| 379 |
-
- **Stage Design**: Each stage should build upon previous stages with varied messaging approaches
|
| 380 |
-
- **Automatic Archiving**: Previous experiments are automatically archived when you start a new one
|
| 381 |
-
""")
|
| 382 |
-
|
| 383 |
-
st.markdown("---")
|
| 384 |
-
st.markdown("**Built with ❤️ for the Musora team**")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/data/UI_users/drumeo_users.csv
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
USER_ID
|
| 2 |
-
668594
|
| 3 |
-
268920
|
| 4 |
-
223559
|
| 5 |
-
830729
|
| 6 |
-
356828
|
| 7 |
-
703749
|
| 8 |
-
840761
|
| 9 |
-
773120
|
| 10 |
-
847327
|
| 11 |
-
566765
|
| 12 |
-
843750
|
| 13 |
-
825806
|
| 14 |
-
551310
|
| 15 |
-
924846
|
| 16 |
-
655113
|
| 17 |
-
576751
|
| 18 |
-
334867
|
| 19 |
-
691463
|
| 20 |
-
680767
|
| 21 |
-
812160
|
| 22 |
-
164216
|
| 23 |
-
881978
|
| 24 |
-
539894
|
| 25 |
-
566222
|
| 26 |
-
660036
|
| 27 |
-
531101
|
| 28 |
-
591381
|
| 29 |
-
876151
|
| 30 |
-
596577
|
| 31 |
-
910512
|
| 32 |
-
395956
|
| 33 |
-
173257
|
| 34 |
-
792254
|
| 35 |
-
911568
|
| 36 |
-
914399
|
| 37 |
-
903638
|
| 38 |
-
736051
|
| 39 |
-
675660
|
| 40 |
-
892433
|
| 41 |
-
346547
|
| 42 |
-
824636
|
| 43 |
-
774712
|
| 44 |
-
488145
|
| 45 |
-
688315
|
| 46 |
-
546326
|
| 47 |
-
610623
|
| 48 |
-
560107
|
| 49 |
-
356255
|
| 50 |
-
757824
|
| 51 |
-
826319
|
| 52 |
-
272269
|
| 53 |
-
919799
|
| 54 |
-
670740
|
| 55 |
-
919348
|
| 56 |
-
324493
|
| 57 |
-
284107
|
| 58 |
-
772112
|
| 59 |
-
821339
|
| 60 |
-
433101
|
| 61 |
-
705894
|
| 62 |
-
775898
|
| 63 |
-
922718
|
| 64 |
-
627434
|
| 65 |
-
811134
|
| 66 |
-
922432
|
| 67 |
-
922696
|
| 68 |
-
934924
|
| 69 |
-
158386
|
| 70 |
-
488116
|
| 71 |
-
167704
|
| 72 |
-
721421
|
| 73 |
-
785019
|
| 74 |
-
831724
|
| 75 |
-
928899
|
| 76 |
-
911985
|
| 77 |
-
911041
|
| 78 |
-
552685
|
| 79 |
-
391892
|
| 80 |
-
265614
|
| 81 |
-
916134
|
| 82 |
-
402573
|
| 83 |
-
900149
|
| 84 |
-
717562
|
| 85 |
-
161665
|
| 86 |
-
389376
|
| 87 |
-
509974
|
| 88 |
-
913237
|
| 89 |
-
760712
|
| 90 |
-
415475
|
| 91 |
-
346652
|
| 92 |
-
604378
|
| 93 |
-
817870
|
| 94 |
-
698502
|
| 95 |
-
904044
|
| 96 |
-
931916
|
| 97 |
-
662869
|
| 98 |
-
837947
|
| 99 |
-
666152
|
| 100 |
-
367181
|
| 101 |
-
510852
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/data/UI_users/guitareo_users.csv
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
USER_ID
|
| 2 |
-
157715
|
| 3 |
-
911553
|
| 4 |
-
156907
|
| 5 |
-
694766
|
| 6 |
-
914407
|
| 7 |
-
158017
|
| 8 |
-
157707
|
| 9 |
-
931216
|
| 10 |
-
157275
|
| 11 |
-
157049
|
| 12 |
-
762379
|
| 13 |
-
922989
|
| 14 |
-
894939
|
| 15 |
-
156233
|
| 16 |
-
445299
|
| 17 |
-
893526
|
| 18 |
-
502852
|
| 19 |
-
412696
|
| 20 |
-
524379
|
| 21 |
-
934803
|
| 22 |
-
533843
|
| 23 |
-
918486
|
| 24 |
-
326346
|
| 25 |
-
825138
|
| 26 |
-
510105
|
| 27 |
-
836199
|
| 28 |
-
903763
|
| 29 |
-
847337
|
| 30 |
-
920186
|
| 31 |
-
367515
|
| 32 |
-
464147
|
| 33 |
-
876953
|
| 34 |
-
820530
|
| 35 |
-
483932
|
| 36 |
-
804270
|
| 37 |
-
652888
|
| 38 |
-
611387
|
| 39 |
-
901114
|
| 40 |
-
161990
|
| 41 |
-
858049
|
| 42 |
-
504479
|
| 43 |
-
793688
|
| 44 |
-
596030
|
| 45 |
-
856499
|
| 46 |
-
830288
|
| 47 |
-
905807
|
| 48 |
-
737373
|
| 49 |
-
801123
|
| 50 |
-
936207
|
| 51 |
-
161806
|
| 52 |
-
844885
|
| 53 |
-
156673
|
| 54 |
-
156417
|
| 55 |
-
166828
|
| 56 |
-
157214
|
| 57 |
-
163682
|
| 58 |
-
157637
|
| 59 |
-
815647
|
| 60 |
-
445428
|
| 61 |
-
848726
|
| 62 |
-
345886
|
| 63 |
-
645329
|
| 64 |
-
908931
|
| 65 |
-
157569
|
| 66 |
-
900579
|
| 67 |
-
161677
|
| 68 |
-
898048
|
| 69 |
-
429111
|
| 70 |
-
902004
|
| 71 |
-
161816
|
| 72 |
-
676947
|
| 73 |
-
160755
|
| 74 |
-
921572
|
| 75 |
-
453858
|
| 76 |
-
548274
|
| 77 |
-
922381
|
| 78 |
-
825297
|
| 79 |
-
720840
|
| 80 |
-
449753
|
| 81 |
-
504742
|
| 82 |
-
157789
|
| 83 |
-
162667
|
| 84 |
-
920329
|
| 85 |
-
157258
|
| 86 |
-
739976
|
| 87 |
-
641480
|
| 88 |
-
157176
|
| 89 |
-
565814
|
| 90 |
-
870217
|
| 91 |
-
574774
|
| 92 |
-
420668
|
| 93 |
-
344602
|
| 94 |
-
918695
|
| 95 |
-
174797
|
| 96 |
-
270977
|
| 97 |
-
903413
|
| 98 |
-
157324
|
| 99 |
-
158234
|
| 100 |
-
156592
|
| 101 |
-
491409
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/data/UI_users/pianote_users.csv
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
USER_ID
|
| 2 |
-
901462
|
| 3 |
-
663534
|
| 4 |
-
925578
|
| 5 |
-
808019
|
| 6 |
-
727360
|
| 7 |
-
907785
|
| 8 |
-
903125
|
| 9 |
-
358505
|
| 10 |
-
903293
|
| 11 |
-
926435
|
| 12 |
-
577983
|
| 13 |
-
640044
|
| 14 |
-
871545
|
| 15 |
-
689297
|
| 16 |
-
839797
|
| 17 |
-
838978
|
| 18 |
-
894404
|
| 19 |
-
388259
|
| 20 |
-
742545
|
| 21 |
-
875618
|
| 22 |
-
906682
|
| 23 |
-
156360
|
| 24 |
-
615274
|
| 25 |
-
496647
|
| 26 |
-
837649
|
| 27 |
-
819147
|
| 28 |
-
762360
|
| 29 |
-
667066
|
| 30 |
-
922040
|
| 31 |
-
165396
|
| 32 |
-
491498
|
| 33 |
-
643639
|
| 34 |
-
388015
|
| 35 |
-
876565
|
| 36 |
-
391879
|
| 37 |
-
451960
|
| 38 |
-
508244
|
| 39 |
-
426511
|
| 40 |
-
401073
|
| 41 |
-
869259
|
| 42 |
-
522076
|
| 43 |
-
631399
|
| 44 |
-
428081
|
| 45 |
-
867206
|
| 46 |
-
815870
|
| 47 |
-
458726
|
| 48 |
-
547732
|
| 49 |
-
554044
|
| 50 |
-
364525
|
| 51 |
-
825266
|
| 52 |
-
844418
|
| 53 |
-
818026
|
| 54 |
-
751982
|
| 55 |
-
356018
|
| 56 |
-
345635
|
| 57 |
-
427868
|
| 58 |
-
586083
|
| 59 |
-
166330
|
| 60 |
-
825075
|
| 61 |
-
343531
|
| 62 |
-
843298
|
| 63 |
-
558798
|
| 64 |
-
352221
|
| 65 |
-
558176
|
| 66 |
-
813053
|
| 67 |
-
386914
|
| 68 |
-
154237
|
| 69 |
-
362024
|
| 70 |
-
855589
|
| 71 |
-
807289
|
| 72 |
-
307320
|
| 73 |
-
623466
|
| 74 |
-
825321
|
| 75 |
-
605229
|
| 76 |
-
348870
|
| 77 |
-
388261
|
| 78 |
-
519602
|
| 79 |
-
153096
|
| 80 |
-
586282
|
| 81 |
-
716526
|
| 82 |
-
401891
|
| 83 |
-
874652
|
| 84 |
-
834192
|
| 85 |
-
732481
|
| 86 |
-
508825
|
| 87 |
-
859490
|
| 88 |
-
519051
|
| 89 |
-
482569
|
| 90 |
-
518170
|
| 91 |
-
541203
|
| 92 |
-
421310
|
| 93 |
-
550180
|
| 94 |
-
406176
|
| 95 |
-
520979
|
| 96 |
-
593825
|
| 97 |
-
549999
|
| 98 |
-
483788
|
| 99 |
-
841717
|
| 100 |
-
703563
|
| 101 |
-
364690
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/data/UI_users/singeo_users.csv
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
USER_ID
|
| 2 |
-
397959
|
| 3 |
-
807871
|
| 4 |
-
532351
|
| 5 |
-
344867
|
| 6 |
-
491564
|
| 7 |
-
687686
|
| 8 |
-
912997
|
| 9 |
-
757896
|
| 10 |
-
475186
|
| 11 |
-
559116
|
| 12 |
-
476436
|
| 13 |
-
430839
|
| 14 |
-
929828
|
| 15 |
-
546602
|
| 16 |
-
660403
|
| 17 |
-
809863
|
| 18 |
-
930638
|
| 19 |
-
308771
|
| 20 |
-
156181
|
| 21 |
-
832580
|
| 22 |
-
642142
|
| 23 |
-
484262
|
| 24 |
-
672837
|
| 25 |
-
723502
|
| 26 |
-
735251
|
| 27 |
-
440691
|
| 28 |
-
676900
|
| 29 |
-
891419
|
| 30 |
-
912102
|
| 31 |
-
605067
|
| 32 |
-
727808
|
| 33 |
-
449959
|
| 34 |
-
295615
|
| 35 |
-
934452
|
| 36 |
-
932834
|
| 37 |
-
631715
|
| 38 |
-
933185
|
| 39 |
-
247877
|
| 40 |
-
550753
|
| 41 |
-
841927
|
| 42 |
-
795745
|
| 43 |
-
555274
|
| 44 |
-
679834
|
| 45 |
-
675272
|
| 46 |
-
911688
|
| 47 |
-
689022
|
| 48 |
-
414410
|
| 49 |
-
502875
|
| 50 |
-
649091
|
| 51 |
-
540841
|
| 52 |
-
616322
|
| 53 |
-
761625
|
| 54 |
-
534677
|
| 55 |
-
830290
|
| 56 |
-
870293
|
| 57 |
-
510636
|
| 58 |
-
932700
|
| 59 |
-
533364
|
| 60 |
-
540891
|
| 61 |
-
584369
|
| 62 |
-
739290
|
| 63 |
-
914907
|
| 64 |
-
934815
|
| 65 |
-
760925
|
| 66 |
-
411509
|
| 67 |
-
420037
|
| 68 |
-
566575
|
| 69 |
-
667578
|
| 70 |
-
483362
|
| 71 |
-
787773
|
| 72 |
-
164176
|
| 73 |
-
876127
|
| 74 |
-
344023
|
| 75 |
-
634693
|
| 76 |
-
365543
|
| 77 |
-
160559
|
| 78 |
-
474363
|
| 79 |
-
675851
|
| 80 |
-
934109
|
| 81 |
-
563890
|
| 82 |
-
875101
|
| 83 |
-
768715
|
| 84 |
-
405549
|
| 85 |
-
509855
|
| 86 |
-
615509
|
| 87 |
-
800896
|
| 88 |
-
901230
|
| 89 |
-
444234
|
| 90 |
-
152323
|
| 91 |
-
610485
|
| 92 |
-
628039
|
| 93 |
-
854271
|
| 94 |
-
503894
|
| 95 |
-
339113
|
| 96 |
-
441986
|
| 97 |
-
721231
|
| 98 |
-
486789
|
| 99 |
-
807279
|
| 100 |
-
749298
|
| 101 |
-
923577
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/pages/1_Campaign_Builder.py
DELETED
|
@@ -1,1100 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Page 1: Campaign Builder
|
| 3 |
-
Configure and run message generation campaigns with A/B testing support.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import streamlit as st
|
| 7 |
-
import sys
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
import json
|
| 11 |
-
import threading
|
| 12 |
-
import shutil
|
| 13 |
-
|
| 14 |
-
# Add parent directories to path
|
| 15 |
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 16 |
-
|
| 17 |
-
from utils.auth import check_authentication
|
| 18 |
-
from utils.theme import apply_theme, get_brand_emoji
|
| 19 |
-
from utils.data_loader import DataLoader
|
| 20 |
-
from utils.config_manager import ConfigManager
|
| 21 |
-
from utils.db_manager import UIDatabaseManager
|
| 22 |
-
from utils.experiment_runner import ExperimentRunner
|
| 23 |
-
|
| 24 |
-
from ai_messaging_system_v2.Messaging_system.Permes import Permes
|
| 25 |
-
from ai_messaging_system_v2.configs.config_loader import get_system_config
|
| 26 |
-
from snowflake.snowpark import Session
|
| 27 |
-
from dotenv import load_dotenv
|
| 28 |
-
import os
|
| 29 |
-
|
| 30 |
-
# Load environment variables
|
| 31 |
-
env_path = Path(__file__).parent.parent / '.env'
|
| 32 |
-
if env_path.exists():
|
| 33 |
-
load_dotenv(env_path)
|
| 34 |
-
else:
|
| 35 |
-
parent_env_path = Path(__file__).parent.parent.parent / '.env'
|
| 36 |
-
if parent_env_path.exists():
|
| 37 |
-
load_dotenv(parent_env_path)
|
| 38 |
-
|
| 39 |
-
# Page configuration
|
| 40 |
-
st.set_page_config(
|
| 41 |
-
page_title="Campaign Builder",
|
| 42 |
-
page_icon="🏗️",
|
| 43 |
-
layout="wide"
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
# Check authentication
|
| 47 |
-
if not check_authentication():
|
| 48 |
-
st.error("🔒 Please login first")
|
| 49 |
-
st.stop()
|
| 50 |
-
|
| 51 |
-
# Initialize utilities
|
| 52 |
-
data_loader = DataLoader()
|
| 53 |
-
# Initialize config_manager for utility methods (create_config_from_ui, validate_config)
|
| 54 |
-
# These methods don't require a database session
|
| 55 |
-
config_manager = ConfigManager(session=None)
|
| 56 |
-
# Note: For saving configs, we'll create a new ConfigManager with a Snowflake session
|
| 57 |
-
# For loading, we use cached configs from session_state
|
| 58 |
-
|
| 59 |
-
# Helper function to create Snowflake session
|
| 60 |
-
def create_snowflake_session() -> Session:
|
| 61 |
-
"""Create a Snowflake session using environment variables."""
|
| 62 |
-
conn_params = {
|
| 63 |
-
"user": os.getenv("SNOWFLAKE_USER"),
|
| 64 |
-
"password": os.getenv("SNOWFLAKE_PASSWORD"),
|
| 65 |
-
"account": os.getenv("SNOWFLAKE_ACCOUNT"),
|
| 66 |
-
"role": os.getenv("SNOWFLAKE_ROLE"),
|
| 67 |
-
"database": os.getenv("SNOWFLAKE_DATABASE"),
|
| 68 |
-
"warehouse": os.getenv("SNOWFLAKE_WAREHOUSE"),
|
| 69 |
-
"schema": os.getenv("SNOWFLAKE_SCHEMA"),
|
| 70 |
-
}
|
| 71 |
-
return Session.builder.configs(conn_params).create()
|
| 72 |
-
|
| 73 |
-
# Check if brand is selected
|
| 74 |
-
if "selected_brand" not in st.session_state or not st.session_state.selected_brand:
|
| 75 |
-
st.error("⚠️ Please select a brand from the home page first")
|
| 76 |
-
st.stop()
|
| 77 |
-
|
| 78 |
-
brand = st.session_state.selected_brand
|
| 79 |
-
apply_theme(brand)
|
| 80 |
-
|
| 81 |
-
# Initialize session state
|
| 82 |
-
if "ab_testing_mode" not in st.session_state:
|
| 83 |
-
st.session_state.ab_testing_mode = False
|
| 84 |
-
if "campaign_config" not in st.session_state:
|
| 85 |
-
st.session_state.campaign_config = None
|
| 86 |
-
if "campaign_config_a" not in st.session_state:
|
| 87 |
-
st.session_state.campaign_config_a = None
|
| 88 |
-
if "campaign_config_b" not in st.session_state:
|
| 89 |
-
st.session_state.campaign_config_b = None
|
| 90 |
-
if "selected_user_count" not in st.session_state:
|
| 91 |
-
st.session_state.selected_user_count = 5
|
| 92 |
-
if "generation_complete" not in st.session_state:
|
| 93 |
-
st.session_state.generation_complete = False
|
| 94 |
-
if "show_next_steps" not in st.session_state:
|
| 95 |
-
st.session_state.show_next_steps = False
|
| 96 |
-
|
| 97 |
-
# Page Header
|
| 98 |
-
emoji = get_brand_emoji(brand)
|
| 99 |
-
st.title(f"🏗️ Campaign Builder - {emoji} {brand.title()}")
|
| 100 |
-
st.markdown("**Configure and generate personalized message campaigns**")
|
| 101 |
-
|
| 102 |
-
st.markdown("---")
|
| 103 |
-
|
| 104 |
-
# ============================================================================
|
| 105 |
-
# A/B TESTING TOGGLE
|
| 106 |
-
# ============================================================================
|
| 107 |
-
ab_col1, ab_col2 = st.columns([3, 1])
|
| 108 |
-
|
| 109 |
-
with ab_col1:
|
| 110 |
-
st.subheader("🧪 Experiment Mode")
|
| 111 |
-
st.markdown("Toggle A/B testing to run two experiments in parallel with different configurations.")
|
| 112 |
-
|
| 113 |
-
with ab_col2:
|
| 114 |
-
ab_testing_enabled = st.toggle(
|
| 115 |
-
"Enable A/B Testing",
|
| 116 |
-
value=st.session_state.ab_testing_mode,
|
| 117 |
-
help="Enable to run two experiments (A and B) in parallel"
|
| 118 |
-
)
|
| 119 |
-
st.session_state.ab_testing_mode = ab_testing_enabled
|
| 120 |
-
|
| 121 |
-
st.markdown("---")
|
| 122 |
-
|
| 123 |
-
# ============================================================================
|
| 124 |
-
# HELPER FUNCTIONS FOR CONFIGURATION SECTIONS
|
| 125 |
-
# ============================================================================
|
| 126 |
-
|
| 127 |
-
def render_configuration_section(prefix: str, col_container, initial_config=None):
|
| 128 |
-
"""
|
| 129 |
-
Render a configuration section (used for both single mode and A/B mode).
|
| 130 |
-
|
| 131 |
-
Args:
|
| 132 |
-
prefix: Prefix for session state keys (e.g., "single", "a", "b")
|
| 133 |
-
col_container: Streamlit container/column to render in
|
| 134 |
-
initial_config: Initial configuration to load
|
| 135 |
-
|
| 136 |
-
Returns:
|
| 137 |
-
Complete configuration dictionary
|
| 138 |
-
"""
|
| 139 |
-
with col_container:
|
| 140 |
-
# Configuration template selection
|
| 141 |
-
st.subheader("📋 Configuration Template")
|
| 142 |
-
|
| 143 |
-
# Get available configurations from cached session_state
|
| 144 |
-
brand_configs = st.session_state.cached_configs.get(brand, {})
|
| 145 |
-
all_configs = brand_configs
|
| 146 |
-
config_names = list(all_configs.keys())
|
| 147 |
-
|
| 148 |
-
if len(config_names) == 0:
|
| 149 |
-
st.warning("No configurations available. Please check your setup.")
|
| 150 |
-
return None
|
| 151 |
-
|
| 152 |
-
selected_config_name = st.selectbox(
|
| 153 |
-
"Template",
|
| 154 |
-
config_names,
|
| 155 |
-
index=0 if "re_engagement_test" in config_names else 0,
|
| 156 |
-
help="Select a configuration template to start with",
|
| 157 |
-
key=f"{prefix}_config_select"
|
| 158 |
-
)
|
| 159 |
-
|
| 160 |
-
# Load selected configuration from cached configs
|
| 161 |
-
loaded_config = all_configs.get(selected_config_name, {})
|
| 162 |
-
|
| 163 |
-
if loaded_config:
|
| 164 |
-
st.success(f"✅ Loaded: **{selected_config_name}**")
|
| 165 |
-
|
| 166 |
-
# Show config preview
|
| 167 |
-
with st.expander("📄 View Configuration Details"):
|
| 168 |
-
st.json(loaded_config)
|
| 169 |
-
|
| 170 |
-
st.markdown("---")
|
| 171 |
-
|
| 172 |
-
# Campaign settings
|
| 173 |
-
st.subheader("⚙️ Campaign Settings")
|
| 174 |
-
|
| 175 |
-
campaign_name = st.text_input(
|
| 176 |
-
"Campaign Name",
|
| 177 |
-
value=loaded_config.get("campaign_name", f"UI-Campaign-{brand}"),
|
| 178 |
-
help="Name for this campaign",
|
| 179 |
-
key=f"{prefix}_campaign_name"
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
col1, col2 = st.columns(2)
|
| 183 |
-
|
| 184 |
-
with col1:
|
| 185 |
-
campaign_type = st.selectbox(
|
| 186 |
-
"Campaign Type",
|
| 187 |
-
["re_engagement"],
|
| 188 |
-
help="Type of campaign",
|
| 189 |
-
key=f"{prefix}_campaign_type"
|
| 190 |
-
)
|
| 191 |
-
|
| 192 |
-
with col2:
|
| 193 |
-
num_stages = st.number_input(
|
| 194 |
-
"Number of Stages",
|
| 195 |
-
min_value=1,
|
| 196 |
-
max_value=11,
|
| 197 |
-
value=min(3, len([k for k in loaded_config.keys() if k.isdigit()])),
|
| 198 |
-
help="Number of message stages (1-11)",
|
| 199 |
-
key=f"{prefix}_num_stages"
|
| 200 |
-
)
|
| 201 |
-
|
| 202 |
-
campaign_instructions = st.text_area(
|
| 203 |
-
"Campaign-Wide Instructions (Optional)",
|
| 204 |
-
value=loaded_config.get("campaign_instructions", ""),
|
| 205 |
-
height=80,
|
| 206 |
-
help="Instructions applied to all stages",
|
| 207 |
-
key=f"{prefix}_campaign_instructions"
|
| 208 |
-
)
|
| 209 |
-
|
| 210 |
-
st.markdown("---")
|
| 211 |
-
|
| 212 |
-
# Stage configuration
|
| 213 |
-
st.subheader("📝 Stage Configuration")
|
| 214 |
-
|
| 215 |
-
# Load system config for model options
|
| 216 |
-
system_config = get_system_config()
|
| 217 |
-
available_models = system_config.get("openai_models", []) + system_config.get("google_models", [])
|
| 218 |
-
|
| 219 |
-
stages_config = {}
|
| 220 |
-
|
| 221 |
-
for stage_num in range(1, num_stages + 1):
|
| 222 |
-
with st.expander(f"Stage {stage_num} Configuration", expanded=(stage_num == 1)):
|
| 223 |
-
# Load existing stage config if available
|
| 224 |
-
existing_stage = loaded_config.get(str(stage_num), {})
|
| 225 |
-
|
| 226 |
-
col1, col2 = st.columns(2)
|
| 227 |
-
|
| 228 |
-
with col1:
|
| 229 |
-
model = st.selectbox(
|
| 230 |
-
"LLM Model",
|
| 231 |
-
available_models,
|
| 232 |
-
index=available_models.index(existing_stage.get("model", available_models[0])) if existing_stage.get("model") in available_models else 0,
|
| 233 |
-
key=f"{prefix}_model_{stage_num}",
|
| 234 |
-
help="Language model to use for generation"
|
| 235 |
-
)
|
| 236 |
-
|
| 237 |
-
personalization = st.checkbox(
|
| 238 |
-
"Enable Personalization",
|
| 239 |
-
value=existing_stage.get("personalization", True),
|
| 240 |
-
key=f"{prefix}_personalization_{stage_num}",
|
| 241 |
-
help="Personalize messages based on user profile"
|
| 242 |
-
)
|
| 243 |
-
|
| 244 |
-
involve_recsys = st.checkbox(
|
| 245 |
-
"Include Content Recommendation",
|
| 246 |
-
value=existing_stage.get("involve_recsys_result", True),
|
| 247 |
-
key=f"{prefix}_involve_recsys_{stage_num}",
|
| 248 |
-
help="Include content recommendations in messages"
|
| 249 |
-
)
|
| 250 |
-
|
| 251 |
-
with col2:
|
| 252 |
-
recsys_contents = st.multiselect(
|
| 253 |
-
"Recommendation Types",
|
| 254 |
-
["workout", "course", "quick_tips", "song"],
|
| 255 |
-
default=existing_stage.get("recsys_contents", ["workout", "course"]),
|
| 256 |
-
key=f"{prefix}_recsys_contents_{stage_num}",
|
| 257 |
-
help="Types of content to recommend",
|
| 258 |
-
disabled=not involve_recsys
|
| 259 |
-
)
|
| 260 |
-
|
| 261 |
-
specific_content_id = st.number_input(
|
| 262 |
-
"Specific Content ID (Optional)",
|
| 263 |
-
min_value=0,
|
| 264 |
-
value=existing_stage.get("specific_content_id", 0) or 0,
|
| 265 |
-
key=f"{prefix}_specific_content_id_{stage_num}",
|
| 266 |
-
help="Force specific content for all users (leave 0 for AI recommendations)"
|
| 267 |
-
)
|
| 268 |
-
specific_content_id = specific_content_id if specific_content_id > 0 else None
|
| 269 |
-
|
| 270 |
-
segment_info = st.text_area(
|
| 271 |
-
"Segment Description",
|
| 272 |
-
value=existing_stage.get("segment_info", f"Users eligible for stage {stage_num}"),
|
| 273 |
-
key=f"{prefix}_segment_info_{stage_num}",
|
| 274 |
-
height=68,
|
| 275 |
-
help="Description of the user segment"
|
| 276 |
-
)
|
| 277 |
-
|
| 278 |
-
instructions = st.text_area(
|
| 279 |
-
"Stage-Specific Instructions (Optional)",
|
| 280 |
-
value=existing_stage.get("instructions", ""),
|
| 281 |
-
key=f"{prefix}_instructions_{stage_num}",
|
| 282 |
-
height=80,
|
| 283 |
-
help="Instructions specific to this stage"
|
| 284 |
-
)
|
| 285 |
-
|
| 286 |
-
sample_example = st.text_area(
|
| 287 |
-
"Example Messages",
|
| 288 |
-
value=existing_stage.get("sample_examples", "Header: Hi!\nMessage: Check this out!"),
|
| 289 |
-
key=f"{prefix}_sample_example_{stage_num}",
|
| 290 |
-
height=80,
|
| 291 |
-
help="Example messages for the LLM to learn from"
|
| 292 |
-
)
|
| 293 |
-
|
| 294 |
-
# Store stage configuration
|
| 295 |
-
stages_config[stage_num] = {
|
| 296 |
-
"stage": stage_num,
|
| 297 |
-
"model": model,
|
| 298 |
-
"personalization": personalization,
|
| 299 |
-
"involve_recsys_result": involve_recsys,
|
| 300 |
-
"recsys_contents": recsys_contents if involve_recsys else [],
|
| 301 |
-
"specific_content_id": specific_content_id,
|
| 302 |
-
"segment_info": segment_info,
|
| 303 |
-
"instructions": instructions,
|
| 304 |
-
"sample_examples": sample_example,
|
| 305 |
-
"identifier_column": "user_id",
|
| 306 |
-
"platform": "push"
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
# Create complete configuration
|
| 310 |
-
complete_config = config_manager.create_config_from_ui(
|
| 311 |
-
brand=brand,
|
| 312 |
-
campaign_name=campaign_name,
|
| 313 |
-
campaign_type=campaign_type,
|
| 314 |
-
num_stages=num_stages,
|
| 315 |
-
campaign_instructions=campaign_instructions,
|
| 316 |
-
stages_config=stages_config
|
| 317 |
-
)
|
| 318 |
-
|
| 319 |
-
# Validation
|
| 320 |
-
is_valid, error_msg = config_manager.validate_config(complete_config)
|
| 321 |
-
|
| 322 |
-
if not is_valid:
|
| 323 |
-
st.error(f"❌ Configuration Error: {error_msg}")
|
| 324 |
-
else:
|
| 325 |
-
st.success("✅ Configuration is valid")
|
| 326 |
-
|
| 327 |
-
return complete_config
|
| 328 |
-
|
| 329 |
-
def render_save_config_section(prefix: str, col_container, config):
|
| 330 |
-
"""Render save configuration section."""
|
| 331 |
-
with col_container:
|
| 332 |
-
st.subheader("💾 Save Configuration")
|
| 333 |
-
|
| 334 |
-
with st.form(f"{prefix}_save_config_form"):
|
| 335 |
-
new_config_name = st.text_input(
|
| 336 |
-
"Configuration Name",
|
| 337 |
-
placeholder="my-custom-config",
|
| 338 |
-
help="Enter a name to save this configuration"
|
| 339 |
-
)
|
| 340 |
-
|
| 341 |
-
save_button = st.form_submit_button("💾 Save Configuration", use_container_width=True)
|
| 342 |
-
|
| 343 |
-
if save_button and new_config_name:
|
| 344 |
-
if config:
|
| 345 |
-
try:
|
| 346 |
-
# Create Snowflake session for saving
|
| 347 |
-
session = create_snowflake_session()
|
| 348 |
-
|
| 349 |
-
# Initialize config manager with session
|
| 350 |
-
config_manager = ConfigManager(session)
|
| 351 |
-
|
| 352 |
-
# Check if config already exists
|
| 353 |
-
brand_configs = st.session_state.cached_configs.get(brand, {})
|
| 354 |
-
if new_config_name in brand_configs:
|
| 355 |
-
st.info(f"ℹ️ Configuration '{new_config_name}' exists. Creating new version...")
|
| 356 |
-
|
| 357 |
-
# Save configuration (auto-versioning)
|
| 358 |
-
if config_manager.save_custom_config(brand, new_config_name, config):
|
| 359 |
-
# Reload configs and update cache
|
| 360 |
-
updated_configs = config_manager.get_all_configs(brand)
|
| 361 |
-
st.session_state.cached_configs[brand] = updated_configs
|
| 362 |
-
st.success(f"✅ Configuration saved as '{new_config_name}'")
|
| 363 |
-
else:
|
| 364 |
-
st.error("❌ Failed to save configuration")
|
| 365 |
-
|
| 366 |
-
# Close session
|
| 367 |
-
session.close()
|
| 368 |
-
|
| 369 |
-
except Exception as e:
|
| 370 |
-
st.error(f"❌ Error saving configuration: {e}")
|
| 371 |
-
else:
|
| 372 |
-
st.error("❌ No configuration loaded")
|
| 373 |
-
|
| 374 |
-
# ============================================================================
|
| 375 |
-
# RENDER CONFIGURATION SECTIONS BASED ON MODE
|
| 376 |
-
# ============================================================================
|
| 377 |
-
|
| 378 |
-
if st.session_state.ab_testing_mode:
|
| 379 |
-
# A/B Testing Mode - Two Columns
|
| 380 |
-
st.header("🧪 A/B Testing Configuration")
|
| 381 |
-
st.info("Configure two different experiments (A and B) to run in parallel with the same users.")
|
| 382 |
-
|
| 383 |
-
col_a, col_b = st.columns(2)
|
| 384 |
-
|
| 385 |
-
# Initialize A/B configs with defaults if None
|
| 386 |
-
if st.session_state.campaign_config_a is None:
|
| 387 |
-
brand_configs = st.session_state.cached_configs.get(brand, {})
|
| 388 |
-
default_name = f"{brand}_re_engagement_test"
|
| 389 |
-
st.session_state.campaign_config_a = brand_configs.get(default_name, {})
|
| 390 |
-
if st.session_state.campaign_config_b is None:
|
| 391 |
-
brand_configs = st.session_state.cached_configs.get(brand, {})
|
| 392 |
-
default_name = f"{brand}_re_engagement_test"
|
| 393 |
-
st.session_state.campaign_config_b = brand_configs.get(default_name, {})
|
| 394 |
-
|
| 395 |
-
# Column A
|
| 396 |
-
with col_a:
|
| 397 |
-
st.markdown("### 🅰️ Experiment A")
|
| 398 |
-
with st.expander("Configuration A", expanded=True):
|
| 399 |
-
config_a = render_configuration_section("a", st.container(), st.session_state.campaign_config_a)
|
| 400 |
-
if config_a is not None:
|
| 401 |
-
st.session_state.campaign_config_a = config_a
|
| 402 |
-
|
| 403 |
-
with st.expander("Save Configuration A"):
|
| 404 |
-
if st.session_state.campaign_config_a:
|
| 405 |
-
render_save_config_section("a", st.container(), st.session_state.campaign_config_a)
|
| 406 |
-
|
| 407 |
-
# Column B
|
| 408 |
-
with col_b:
|
| 409 |
-
st.markdown("### 🅱️ Experiment B")
|
| 410 |
-
with st.expander("Configuration B", expanded=True):
|
| 411 |
-
config_b = render_configuration_section("b", st.container(), st.session_state.campaign_config_b)
|
| 412 |
-
if config_b is not None:
|
| 413 |
-
st.session_state.campaign_config_b = config_b
|
| 414 |
-
|
| 415 |
-
with st.expander("Save Configuration B"):
|
| 416 |
-
if st.session_state.campaign_config_b:
|
| 417 |
-
render_save_config_section("b", st.container(), st.session_state.campaign_config_b)
|
| 418 |
-
|
| 419 |
-
else:
|
| 420 |
-
# Single Mode
|
| 421 |
-
# Initialize config with default if None
|
| 422 |
-
if st.session_state.campaign_config is None:
|
| 423 |
-
brand_configs = st.session_state.cached_configs.get(brand, {})
|
| 424 |
-
default_name = f"{brand}_re_engagement_test"
|
| 425 |
-
st.session_state.campaign_config = brand_configs.get(default_name, {})
|
| 426 |
-
|
| 427 |
-
with st.expander("📋 Configuration", expanded=True):
|
| 428 |
-
config = render_configuration_section("single", st.container(), st.session_state.campaign_config)
|
| 429 |
-
if config is not None:
|
| 430 |
-
st.session_state.campaign_config = config
|
| 431 |
-
|
| 432 |
-
# Save section outside the main config expander
|
| 433 |
-
with st.expander("💾 Save Configuration"):
|
| 434 |
-
if st.session_state.campaign_config:
|
| 435 |
-
render_save_config_section("single", st.container(), st.session_state.campaign_config)
|
| 436 |
-
|
| 437 |
-
st.markdown("---")
|
| 438 |
-
|
| 439 |
-
# ============================================================================
|
| 440 |
-
# USER SELECTION SECTION
|
| 441 |
-
# ============================================================================
|
| 442 |
-
|
| 443 |
-
with st.expander("👥 User Selection", expanded=True):
|
| 444 |
-
st.subheader("👥 Select Number of Users")
|
| 445 |
-
|
| 446 |
-
# Check if brand users file exists
|
| 447 |
-
has_brand_users = data_loader.has_brand_users(brand)
|
| 448 |
-
|
| 449 |
-
if not has_brand_users:
|
| 450 |
-
st.error(f"❌ No user file found for {brand}. Please ensure {brand}_users.csv exists in visualization/data/UI_users/")
|
| 451 |
-
else:
|
| 452 |
-
# Get total available users
|
| 453 |
-
total_users = data_loader.get_brand_user_count(brand)
|
| 454 |
-
|
| 455 |
-
st.info(f"ℹ️ {total_users} users available for {brand}")
|
| 456 |
-
|
| 457 |
-
st.markdown("""
|
| 458 |
-
Choose how many users you want to experiment with. Users will be **randomly sampled** each time you generate messages.
|
| 459 |
-
|
| 460 |
-
For quick experimentation, we recommend **5-10 users**.
|
| 461 |
-
""")
|
| 462 |
-
|
| 463 |
-
col1, col2 = st.columns([2, 3])
|
| 464 |
-
|
| 465 |
-
with col1:
|
| 466 |
-
user_count = st.number_input(
|
| 467 |
-
"Number of Users",
|
| 468 |
-
min_value=1,
|
| 469 |
-
max_value=min(25, total_users),
|
| 470 |
-
value=st.session_state.selected_user_count,
|
| 471 |
-
help="Select 1-25 users for experimentation"
|
| 472 |
-
)
|
| 473 |
-
|
| 474 |
-
# Update session state
|
| 475 |
-
st.session_state.selected_user_count = user_count
|
| 476 |
-
|
| 477 |
-
with col2:
|
| 478 |
-
st.markdown("")
|
| 479 |
-
st.markdown("")
|
| 480 |
-
st.markdown(f"**Selected:** {user_count} user{'s' if user_count != 1 else ''}")
|
| 481 |
-
st.markdown(f"**Available:** {total_users} total users")
|
| 482 |
-
|
| 483 |
-
st.markdown("---")
|
| 484 |
-
|
| 485 |
-
# Preview sample button
|
| 486 |
-
if st.button("👀 Preview Random Sample", use_container_width=False):
|
| 487 |
-
sample_df = data_loader.sample_users_randomly(brand, user_count)
|
| 488 |
-
if sample_df is not None:
|
| 489 |
-
st.success(f"✅ Sampled {len(sample_df)} random users")
|
| 490 |
-
st.dataframe(sample_df, use_container_width=True)
|
| 491 |
-
|
| 492 |
-
st.markdown("---")
|
| 493 |
-
|
| 494 |
-
# ============================================================================
|
| 495 |
-
# GENERATION SECTION
|
| 496 |
-
# ============================================================================
|
| 497 |
-
|
| 498 |
-
def archive_existing_messages(campaign_name: str):
|
| 499 |
-
"""Archive existing messages.csv to a timestamped file."""
|
| 500 |
-
ui_output_path = data_loader.get_ui_output_path()
|
| 501 |
-
messages_file = ui_output_path / "messages.csv"
|
| 502 |
-
|
| 503 |
-
if messages_file.exists():
|
| 504 |
-
timestamp = datetime.now().strftime('%Y%m%d_%H%M')
|
| 505 |
-
archive_name = f"messages_{campaign_name}_{timestamp}.csv"
|
| 506 |
-
archive_path = ui_output_path / archive_name
|
| 507 |
-
|
| 508 |
-
try:
|
| 509 |
-
shutil.copy2(messages_file, archive_path)
|
| 510 |
-
st.info(f"📦 Archived existing messages to: {archive_name}")
|
| 511 |
-
return True
|
| 512 |
-
except Exception as e:
|
| 513 |
-
st.warning(f"⚠️ Could not archive existing messages: {e}")
|
| 514 |
-
return False
|
| 515 |
-
return True
|
| 516 |
-
|
| 517 |
-
def generate_messages_for_experiment(
|
| 518 |
-
experiment_name: str,
|
| 519 |
-
config: dict,
|
| 520 |
-
sampled_users_df,
|
| 521 |
-
output_suffix: str = None,
|
| 522 |
-
progress_container=None
|
| 523 |
-
):
|
| 524 |
-
"""
|
| 525 |
-
Generate messages for a single experiment.
|
| 526 |
-
|
| 527 |
-
Args:
|
| 528 |
-
experiment_name: Name of the experiment (e.g., "A" or "B")
|
| 529 |
-
config: Configuration dictionary
|
| 530 |
-
sampled_users_df: DataFrame of sampled users
|
| 531 |
-
output_suffix: Optional suffix for output filename (e.g., "_a" or "_b")
|
| 532 |
-
progress_container: Streamlit container for progress updates
|
| 533 |
-
|
| 534 |
-
Returns:
|
| 535 |
-
tuple: (success, total_messages_generated)
|
| 536 |
-
"""
|
| 537 |
-
if progress_container is None:
|
| 538 |
-
progress_container = st
|
| 539 |
-
|
| 540 |
-
with progress_container:
|
| 541 |
-
st.markdown(f"### 📊 Experiment {experiment_name} Progress")
|
| 542 |
-
|
| 543 |
-
system_config = get_system_config()
|
| 544 |
-
|
| 545 |
-
# Setup Permes for message generation
|
| 546 |
-
permes = Permes()
|
| 547 |
-
original_output_file = permes.UI_OUTPUT_FILE
|
| 548 |
-
ui_experiment_id = None
|
| 549 |
-
|
| 550 |
-
if output_suffix:
|
| 551 |
-
# Generate experiment ID for timestamping and naming
|
| 552 |
-
experiment_timestamp = datetime.now().strftime('%Y%m%d_%H%M')
|
| 553 |
-
ui_experiment_id = f"messages_{output_suffix}_{brand}_{experiment_timestamp}"
|
| 554 |
-
# Keep this for backward compatibility (though it won't be used anymore)
|
| 555 |
-
permes.UI_OUTPUT_FILE = f"{ui_experiment_id}.csv"
|
| 556 |
-
st.info(f"💾 Results will be saved to: {ui_experiment_id}.csv")
|
| 557 |
-
|
| 558 |
-
# Get campaign-level configurations
|
| 559 |
-
campaign_name = config.get("campaign_name", "UI-Campaign")
|
| 560 |
-
campaign_instructions = config.get("campaign_instructions", None)
|
| 561 |
-
num_stages = len([k for k in config.keys() if k.isdigit()])
|
| 562 |
-
|
| 563 |
-
# Generate messages for each stage
|
| 564 |
-
total_messages_generated = 0
|
| 565 |
-
generation_successful = True
|
| 566 |
-
|
| 567 |
-
for stage in range(1, num_stages + 1):
|
| 568 |
-
st.markdown(f"#### Stage {stage} of {num_stages}")
|
| 569 |
-
|
| 570 |
-
progress_bar = st.progress(0)
|
| 571 |
-
status_text = st.empty()
|
| 572 |
-
|
| 573 |
-
status_text.text(f"Generating messages for stage {stage}...")
|
| 574 |
-
|
| 575 |
-
session = None
|
| 576 |
-
try:
|
| 577 |
-
session = create_snowflake_session()
|
| 578 |
-
progress_bar.progress(10)
|
| 579 |
-
|
| 580 |
-
# Get stage configuration
|
| 581 |
-
stage_config = config.get(str(stage), {})
|
| 582 |
-
|
| 583 |
-
if not stage_config:
|
| 584 |
-
status_text.error(f"❌ Stage {stage}: Configuration not found")
|
| 585 |
-
generation_successful = False
|
| 586 |
-
if session:
|
| 587 |
-
try:
|
| 588 |
-
session.close()
|
| 589 |
-
except:
|
| 590 |
-
pass
|
| 591 |
-
continue
|
| 592 |
-
|
| 593 |
-
# Extract stage parameters
|
| 594 |
-
model = stage_config.get("model", "gemini-2.5-flash-lite")
|
| 595 |
-
segment_info = stage_config.get("segment_info", "")
|
| 596 |
-
recsys_contents = stage_config.get("recsys_contents", [])
|
| 597 |
-
involve_recsys_result = stage_config.get("involve_recsys_result", True)
|
| 598 |
-
personalization = stage_config.get("personalization", True)
|
| 599 |
-
sample_example = stage_config.get("sample_examples", "")
|
| 600 |
-
per_message_instructions = stage_config.get("instructions", None)
|
| 601 |
-
specific_content_id = stage_config.get("specific_content_id", None)
|
| 602 |
-
|
| 603 |
-
progress_bar.progress(20)
|
| 604 |
-
|
| 605 |
-
# Call Permes to generate messages
|
| 606 |
-
users_message = permes.create_personalize_messages(
|
| 607 |
-
session=session,
|
| 608 |
-
model=model,
|
| 609 |
-
users=sampled_users_df.copy(),
|
| 610 |
-
brand=brand,
|
| 611 |
-
config_file=system_config,
|
| 612 |
-
segment_info=segment_info,
|
| 613 |
-
involve_recsys_result=involve_recsys_result,
|
| 614 |
-
identifier_column="user_id",
|
| 615 |
-
recsys_contents=recsys_contents,
|
| 616 |
-
sample_example=sample_example,
|
| 617 |
-
campaign_name=campaign_name,
|
| 618 |
-
personalization=personalization,
|
| 619 |
-
stage=stage,
|
| 620 |
-
test_mode=False,
|
| 621 |
-
mode="ui",
|
| 622 |
-
campaign_instructions=campaign_instructions,
|
| 623 |
-
per_message_instructions=per_message_instructions,
|
| 624 |
-
specific_content_id=specific_content_id,
|
| 625 |
-
ui_experiment_id=ui_experiment_id
|
| 626 |
-
)
|
| 627 |
-
|
| 628 |
-
progress_bar.progress(100)
|
| 629 |
-
|
| 630 |
-
if users_message is not None and len(users_message) > 0:
|
| 631 |
-
total_messages_generated += len(users_message)
|
| 632 |
-
status_text.success(
|
| 633 |
-
f"✅ Stage {stage}: Generated {len(users_message)} messages"
|
| 634 |
-
)
|
| 635 |
-
else:
|
| 636 |
-
status_text.warning(f"⚠️ Stage {stage}: No messages generated")
|
| 637 |
-
generation_successful = False
|
| 638 |
-
|
| 639 |
-
except Exception as e:
|
| 640 |
-
progress_bar.progress(0)
|
| 641 |
-
status_text.error(f"❌ Stage {stage}: Error - {str(e)}")
|
| 642 |
-
generation_successful = False
|
| 643 |
-
|
| 644 |
-
finally:
|
| 645 |
-
if session:
|
| 646 |
-
try:
|
| 647 |
-
session.close()
|
| 648 |
-
except:
|
| 649 |
-
pass
|
| 650 |
-
|
| 651 |
-
# Restore original output file name
|
| 652 |
-
permes.UI_OUTPUT_FILE = original_output_file
|
| 653 |
-
|
| 654 |
-
return generation_successful, total_messages_generated
|
| 655 |
-
|
| 656 |
-
def generate_messages_threaded(
|
| 657 |
-
experiment_name: str,
|
| 658 |
-
config: dict,
|
| 659 |
-
sampled_users_df,
|
| 660 |
-
output_suffix: str,
|
| 661 |
-
brand: str,
|
| 662 |
-
experiment_timestamp: str = None
|
| 663 |
-
):
|
| 664 |
-
"""
|
| 665 |
-
Generate messages for threading (no UI updates).
|
| 666 |
-
|
| 667 |
-
Args:
|
| 668 |
-
experiment_name: Name of the experiment (e.g., "A" or "B")
|
| 669 |
-
config: Configuration dictionary
|
| 670 |
-
sampled_users_df: DataFrame of sampled users
|
| 671 |
-
output_suffix: Suffix for output filename (e.g., "a" or "b")
|
| 672 |
-
brand: Brand name
|
| 673 |
-
experiment_timestamp: Shared timestamp for A/B test pairs
|
| 674 |
-
|
| 675 |
-
Returns:
|
| 676 |
-
dict: Results with 'success', 'total_messages', 'error', 'stages_completed'
|
| 677 |
-
"""
|
| 678 |
-
result = {
|
| 679 |
-
'success': False,
|
| 680 |
-
'total_messages': 0,
|
| 681 |
-
'error': None,
|
| 682 |
-
'stages_completed': 0,
|
| 683 |
-
'stage_details': []
|
| 684 |
-
}
|
| 685 |
-
|
| 686 |
-
try:
|
| 687 |
-
system_config = get_system_config()
|
| 688 |
-
|
| 689 |
-
# Setup Permes for message generation
|
| 690 |
-
permes = Permes()
|
| 691 |
-
original_output_file = permes.UI_OUTPUT_FILE
|
| 692 |
-
|
| 693 |
-
# Use provided timestamp or generate new one
|
| 694 |
-
if experiment_timestamp is None:
|
| 695 |
-
experiment_timestamp = datetime.now().strftime('%Y%m%d_%H%M')
|
| 696 |
-
|
| 697 |
-
# Create experiment ID for UI mode
|
| 698 |
-
ui_experiment_id = f"messages_{output_suffix}_{brand}_{experiment_timestamp}"
|
| 699 |
-
# Keep this for backward compatibility (though it won't be used anymore)
|
| 700 |
-
permes.UI_OUTPUT_FILE = f"{ui_experiment_id}.csv"
|
| 701 |
-
|
| 702 |
-
# Get campaign-level configurations
|
| 703 |
-
campaign_name = config.get("campaign_name", "UI-Campaign")
|
| 704 |
-
campaign_instructions = config.get("campaign_instructions", None)
|
| 705 |
-
num_stages = len([k for k in config.keys() if k.isdigit()])
|
| 706 |
-
|
| 707 |
-
# Generate messages for each stage
|
| 708 |
-
for stage in range(1, num_stages + 1):
|
| 709 |
-
session = None
|
| 710 |
-
try:
|
| 711 |
-
session = create_snowflake_session()
|
| 712 |
-
|
| 713 |
-
# Get stage configuration
|
| 714 |
-
stage_config = config.get(str(stage), {})
|
| 715 |
-
|
| 716 |
-
if not stage_config:
|
| 717 |
-
result['stage_details'].append({
|
| 718 |
-
'stage': stage,
|
| 719 |
-
'status': 'error',
|
| 720 |
-
'message': 'Configuration not found',
|
| 721 |
-
'count': 0
|
| 722 |
-
})
|
| 723 |
-
continue
|
| 724 |
-
|
| 725 |
-
# Extract stage parameters
|
| 726 |
-
model = stage_config.get("model", "gemini-2.5-flash-lite")
|
| 727 |
-
segment_info = stage_config.get("segment_info", "")
|
| 728 |
-
recsys_contents = stage_config.get("recsys_contents", [])
|
| 729 |
-
involve_recsys_result = stage_config.get("involve_recsys_result", True)
|
| 730 |
-
personalization = stage_config.get("personalization", True)
|
| 731 |
-
sample_example = stage_config.get("sample_examples", "")
|
| 732 |
-
per_message_instructions = stage_config.get("instructions", None)
|
| 733 |
-
specific_content_id = stage_config.get("specific_content_id", None)
|
| 734 |
-
|
| 735 |
-
# Call Permes to generate messages
|
| 736 |
-
users_message = permes.create_personalize_messages(
|
| 737 |
-
session=session,
|
| 738 |
-
model=model,
|
| 739 |
-
users=sampled_users_df.copy(),
|
| 740 |
-
brand=brand,
|
| 741 |
-
config_file=system_config,
|
| 742 |
-
segment_info=segment_info,
|
| 743 |
-
involve_recsys_result=involve_recsys_result,
|
| 744 |
-
identifier_column="user_id",
|
| 745 |
-
recsys_contents=recsys_contents,
|
| 746 |
-
sample_example=sample_example,
|
| 747 |
-
campaign_name=campaign_name,
|
| 748 |
-
personalization=personalization,
|
| 749 |
-
stage=stage,
|
| 750 |
-
test_mode=False,
|
| 751 |
-
mode="ui",
|
| 752 |
-
campaign_instructions=campaign_instructions,
|
| 753 |
-
per_message_instructions=per_message_instructions,
|
| 754 |
-
specific_content_id=specific_content_id,
|
| 755 |
-
ui_experiment_id=ui_experiment_id
|
| 756 |
-
)
|
| 757 |
-
|
| 758 |
-
if users_message is not None and len(users_message) > 0:
|
| 759 |
-
result['total_messages'] += len(users_message)
|
| 760 |
-
result['stages_completed'] += 1
|
| 761 |
-
result['stage_details'].append({
|
| 762 |
-
'stage': stage,
|
| 763 |
-
'status': 'success',
|
| 764 |
-
'message': f'Generated {len(users_message)} messages',
|
| 765 |
-
'count': len(users_message)
|
| 766 |
-
})
|
| 767 |
-
else:
|
| 768 |
-
result['stage_details'].append({
|
| 769 |
-
'stage': stage,
|
| 770 |
-
'status': 'warning',
|
| 771 |
-
'message': 'No messages generated',
|
| 772 |
-
'count': 0
|
| 773 |
-
})
|
| 774 |
-
|
| 775 |
-
except Exception as e:
|
| 776 |
-
result['stage_details'].append({
|
| 777 |
-
'stage': stage,
|
| 778 |
-
'status': 'error',
|
| 779 |
-
'message': str(e),
|
| 780 |
-
'count': 0
|
| 781 |
-
})
|
| 782 |
-
|
| 783 |
-
finally:
|
| 784 |
-
if session:
|
| 785 |
-
try:
|
| 786 |
-
session.close()
|
| 787 |
-
except:
|
| 788 |
-
pass
|
| 789 |
-
|
| 790 |
-
# Restore original output file name
|
| 791 |
-
permes.UI_OUTPUT_FILE = original_output_file
|
| 792 |
-
|
| 793 |
-
# Mark as successful if at least one stage completed
|
| 794 |
-
result['success'] = result['stages_completed'] > 0
|
| 795 |
-
|
| 796 |
-
except Exception as e:
|
| 797 |
-
result['error'] = str(e)
|
| 798 |
-
result['success'] = False
|
| 799 |
-
|
| 800 |
-
return result
|
| 801 |
-
|
| 802 |
-
with st.expander("🚀 Generate Messages", expanded=True):
|
| 803 |
-
st.subheader("🚀 Generate Messages")
|
| 804 |
-
|
| 805 |
-
# Check prerequisites
|
| 806 |
-
if st.session_state.ab_testing_mode:
|
| 807 |
-
config_ready = (st.session_state.campaign_config_a is not None and
|
| 808 |
-
st.session_state.campaign_config_b is not None)
|
| 809 |
-
else:
|
| 810 |
-
config_ready = st.session_state.campaign_config is not None
|
| 811 |
-
|
| 812 |
-
users_ready = data_loader.has_brand_users(brand)
|
| 813 |
-
user_count_selected = st.session_state.get("selected_user_count", 5)
|
| 814 |
-
|
| 815 |
-
# Status checks
|
| 816 |
-
col1, col2, col3 = st.columns(3)
|
| 817 |
-
|
| 818 |
-
with col1:
|
| 819 |
-
if config_ready:
|
| 820 |
-
st.success("✅ Configuration ready")
|
| 821 |
-
else:
|
| 822 |
-
st.error("❌ Configuration not ready")
|
| 823 |
-
|
| 824 |
-
with col2:
|
| 825 |
-
if users_ready:
|
| 826 |
-
st.success(f"✅ {user_count_selected} users selected")
|
| 827 |
-
else:
|
| 828 |
-
st.error("❌ No users available")
|
| 829 |
-
|
| 830 |
-
with col3:
|
| 831 |
-
if st.session_state.ab_testing_mode:
|
| 832 |
-
st.metric("Mode", "A/B Testing")
|
| 833 |
-
else:
|
| 834 |
-
st.metric("Mode", "Single")
|
| 835 |
-
|
| 836 |
-
st.markdown("---")
|
| 837 |
-
|
| 838 |
-
if not config_ready:
|
| 839 |
-
st.info("👆 Please complete the configuration first")
|
| 840 |
-
elif not users_ready:
|
| 841 |
-
st.error("❌ No users available. Please check that user files exist.")
|
| 842 |
-
else:
|
| 843 |
-
# Generation settings
|
| 844 |
-
st.subheader("Generation Settings")
|
| 845 |
-
|
| 846 |
-
if st.session_state.ab_testing_mode:
|
| 847 |
-
num_stages_a = len([k for k in st.session_state.campaign_config_a.keys() if k.isdigit()])
|
| 848 |
-
num_stages_b = len([k for k in st.session_state.campaign_config_b.keys() if k.isdigit()])
|
| 849 |
-
st.info(f"ℹ️ Will generate messages for **{num_stages_a} stages (A)** and **{num_stages_b} stages (B)** in parallel")
|
| 850 |
-
else:
|
| 851 |
-
num_stages = len([k for k in st.session_state.campaign_config.keys() if k.isdigit()])
|
| 852 |
-
st.info(f"ℹ️ Will generate messages for **all {num_stages} stages** configured in the campaign")
|
| 853 |
-
|
| 854 |
-
# Clear previous results option
|
| 855 |
-
clear_previous = st.checkbox(
|
| 856 |
-
"Start new experiment (clears previous results from memory)",
|
| 857 |
-
value=True,
|
| 858 |
-
help="Clear previous experiment data from session_state and start fresh"
|
| 859 |
-
)
|
| 860 |
-
|
| 861 |
-
st.markdown("---")
|
| 862 |
-
|
| 863 |
-
# Generate button
|
| 864 |
-
if st.button("🚀 Start Generation", use_container_width=True, type="primary"):
|
| 865 |
-
# Reset next steps flag
|
| 866 |
-
st.session_state.show_next_steps = False
|
| 867 |
-
st.session_state.generation_complete = False
|
| 868 |
-
|
| 869 |
-
# Clear previous experiment data if requested
|
| 870 |
-
if clear_previous:
|
| 871 |
-
st.session_state.ui_log_data = None
|
| 872 |
-
st.session_state.current_experiment_metadata = []
|
| 873 |
-
st.session_state.current_feedbacks = []
|
| 874 |
-
st.session_state.ui_log_data_a = None
|
| 875 |
-
st.session_state.ui_log_data_b = None
|
| 876 |
-
st.session_state.experiment_a_metadata = []
|
| 877 |
-
st.session_state.experiment_b_metadata = []
|
| 878 |
-
st.session_state.feedbacks_a = []
|
| 879 |
-
st.session_state.feedbacks_b = []
|
| 880 |
-
st.info("🗑️ Cleared previous experiment data from memory")
|
| 881 |
-
|
| 882 |
-
# Sample users randomly from the brand's user file
|
| 883 |
-
with st.spinner(f"Sampling {user_count_selected} random users from {brand}_users.csv..."):
|
| 884 |
-
sampled_users_df = data_loader.sample_users_randomly(brand, user_count_selected)
|
| 885 |
-
|
| 886 |
-
if sampled_users_df is None or len(sampled_users_df) == 0:
|
| 887 |
-
st.error("❌ Failed to sample users. Please check your user files.")
|
| 888 |
-
st.stop()
|
| 889 |
-
|
| 890 |
-
st.success(f"✅ Sampled {len(sampled_users_df)} users successfully")
|
| 891 |
-
|
| 892 |
-
st.markdown("---")
|
| 893 |
-
|
| 894 |
-
# Initialize ExperimentRunner
|
| 895 |
-
system_config = get_system_config()
|
| 896 |
-
runner = ExperimentRunner(brand=brand, system_config=system_config)
|
| 897 |
-
|
| 898 |
-
if st.session_state.ab_testing_mode:
|
| 899 |
-
# A/B Testing Mode - Parallel Execution
|
| 900 |
-
st.markdown("## 🧪 Running A/B Tests in Parallel")
|
| 901 |
-
st.info("⏳ Generating messages for both experiments in parallel. This may take a few minutes...")
|
| 902 |
-
|
| 903 |
-
# Generate shared timestamp for both experiments
|
| 904 |
-
shared_timestamp = datetime.now().strftime('%Y%m%d_%H%M')
|
| 905 |
-
|
| 906 |
-
# Create experiment IDs
|
| 907 |
-
experiment_a_id = f"messages_a_{brand}_{shared_timestamp}"
|
| 908 |
-
experiment_b_id = f"messages_b_{brand}_{shared_timestamp}"
|
| 909 |
-
|
| 910 |
-
# Get configs
|
| 911 |
-
config_a = st.session_state.campaign_config_a
|
| 912 |
-
config_b = st.session_state.campaign_config_b
|
| 913 |
-
|
| 914 |
-
# Run parallel AB test using ExperimentRunner
|
| 915 |
-
with st.spinner("Generating messages for both experiments..."):
|
| 916 |
-
results = runner.run_ab_test_parallel(
|
| 917 |
-
config_a=config_a,
|
| 918 |
-
config_b=config_b,
|
| 919 |
-
sampled_users_df=sampled_users_df,
|
| 920 |
-
experiment_a_id=experiment_a_id,
|
| 921 |
-
experiment_b_id=experiment_b_id,
|
| 922 |
-
create_session_func=create_snowflake_session
|
| 923 |
-
)
|
| 924 |
-
|
| 925 |
-
st.markdown("---")
|
| 926 |
-
|
| 927 |
-
# Store results in session_state
|
| 928 |
-
st.session_state.experiment_a_id = experiment_a_id
|
| 929 |
-
st.session_state.experiment_b_id = experiment_b_id
|
| 930 |
-
st.session_state.ui_log_data_a = results['a']['ui_log_data']
|
| 931 |
-
st.session_state.ui_log_data_b = results['b']['ui_log_data']
|
| 932 |
-
st.session_state.experiment_a_metadata = results['a']['metadata']
|
| 933 |
-
st.session_state.experiment_b_metadata = results['b']['metadata']
|
| 934 |
-
|
| 935 |
-
# Display results
|
| 936 |
-
st.markdown("## 📊 A/B Test Results")
|
| 937 |
-
|
| 938 |
-
col_a_result, col_b_result = st.columns(2)
|
| 939 |
-
|
| 940 |
-
# Experiment A Results
|
| 941 |
-
with col_a_result:
|
| 942 |
-
st.markdown("### 🅰️ Experiment A")
|
| 943 |
-
if results['a']['success']:
|
| 944 |
-
total_messages_a = len(st.session_state.ui_log_data_a) if st.session_state.ui_log_data_a is not None else 0
|
| 945 |
-
st.success(f"✅ Generated {total_messages_a} messages")
|
| 946 |
-
st.metric("Stages Completed", f"{len(st.session_state.experiment_a_metadata)}")
|
| 947 |
-
|
| 948 |
-
# Show stage summary
|
| 949 |
-
with st.expander("View Stage Details"):
|
| 950 |
-
for meta in st.session_state.experiment_a_metadata:
|
| 951 |
-
st.info(f"Stage {meta['stage']}: {meta['total_messages']} messages using {meta['llm_model']}")
|
| 952 |
-
else:
|
| 953 |
-
st.error(f"❌ Error: {results['a'].get('error', 'Unknown error')}")
|
| 954 |
-
|
| 955 |
-
# Experiment B Results
|
| 956 |
-
with col_b_result:
|
| 957 |
-
st.markdown("### 🅱️ Experiment B")
|
| 958 |
-
if results['b']['success']:
|
| 959 |
-
total_messages_b = len(st.session_state.ui_log_data_b) if st.session_state.ui_log_data_b is not None else 0
|
| 960 |
-
st.success(f"✅ Generated {total_messages_b} messages")
|
| 961 |
-
st.metric("Stages Completed", f"{len(st.session_state.experiment_b_metadata)}")
|
| 962 |
-
|
| 963 |
-
# Show stage summary
|
| 964 |
-
with st.expander("View Stage Details"):
|
| 965 |
-
for meta in st.session_state.experiment_b_metadata:
|
| 966 |
-
st.info(f"Stage {meta['stage']}: {meta['total_messages']} messages using {meta['llm_model']}")
|
| 967 |
-
else:
|
| 968 |
-
st.error(f"❌ Error: {results['b'].get('error', 'Unknown error')}")
|
| 969 |
-
|
| 970 |
-
# Mark generation as complete
|
| 971 |
-
st.session_state.generation_complete = True
|
| 972 |
-
st.session_state.show_next_steps = True
|
| 973 |
-
|
| 974 |
-
else:
|
| 975 |
-
# Single Mode
|
| 976 |
-
st.markdown("## 📊 Generating Messages")
|
| 977 |
-
|
| 978 |
-
# Generate experiment ID
|
| 979 |
-
experiment_id = f"{brand}_experiment_{datetime.now().strftime('%Y%m%d_%H%M')}"
|
| 980 |
-
|
| 981 |
-
# Run single experiment using ExperimentRunner
|
| 982 |
-
success, ui_log_data, metadata_list = runner.run_single_experiment(
|
| 983 |
-
config=st.session_state.campaign_config,
|
| 984 |
-
sampled_users_df=sampled_users_df,
|
| 985 |
-
experiment_id=experiment_id,
|
| 986 |
-
create_session_func=create_snowflake_session,
|
| 987 |
-
progress_container=st.container()
|
| 988 |
-
)
|
| 989 |
-
|
| 990 |
-
st.markdown("---")
|
| 991 |
-
|
| 992 |
-
# Store results in session_state
|
| 993 |
-
st.session_state.current_experiment_id = experiment_id
|
| 994 |
-
st.session_state.ui_log_data = ui_log_data
|
| 995 |
-
st.session_state.current_experiment_metadata = metadata_list
|
| 996 |
-
|
| 997 |
-
if success and ui_log_data is not None:
|
| 998 |
-
total_messages = len(ui_log_data)
|
| 999 |
-
st.success(f"✅ Generation complete! Generated {total_messages} total messages.")
|
| 1000 |
-
|
| 1001 |
-
# Show stage summary
|
| 1002 |
-
with st.expander("📋 View Stage Summary"):
|
| 1003 |
-
for meta in metadata_list:
|
| 1004 |
-
st.info(f"Stage {meta['stage']}: {meta['total_messages']} messages using {meta['llm_model']}")
|
| 1005 |
-
else:
|
| 1006 |
-
st.warning("⚠️ Generation completed with some errors. Please check the output above.")
|
| 1007 |
-
|
| 1008 |
-
# Mark generation as complete
|
| 1009 |
-
st.session_state.generation_complete = True
|
| 1010 |
-
st.session_state.show_next_steps = True
|
| 1011 |
-
|
| 1012 |
-
# Show next steps outside the generation button block (persists across reruns)
|
| 1013 |
-
if st.session_state.get("show_next_steps", False):
|
| 1014 |
-
st.markdown("---")
|
| 1015 |
-
st.subheader("📋 Next Steps")
|
| 1016 |
-
|
| 1017 |
-
col1, col2 = st.columns(2)
|
| 1018 |
-
|
| 1019 |
-
with col1:
|
| 1020 |
-
if st.button("👀 View Messages", use_container_width=True, key="view_messages_after_gen"):
|
| 1021 |
-
st.switch_page("pages/2_Message_Viewer.py")
|
| 1022 |
-
|
| 1023 |
-
with col2:
|
| 1024 |
-
if st.button("📊 View Analytics", use_container_width=True, key="view_analytics_after_gen"):
|
| 1025 |
-
st.switch_page("pages/4_Analytics.py")
|
| 1026 |
-
|
| 1027 |
-
# Show existing messages from session_state if any
|
| 1028 |
-
if st.session_state.ab_testing_mode:
|
| 1029 |
-
# AB mode - check both experiments
|
| 1030 |
-
has_messages = (st.session_state.ui_log_data_a is not None and len(st.session_state.ui_log_data_a) > 0) or \
|
| 1031 |
-
(st.session_state.ui_log_data_b is not None and len(st.session_state.ui_log_data_b) > 0)
|
| 1032 |
-
else:
|
| 1033 |
-
# Single mode
|
| 1034 |
-
has_messages = st.session_state.ui_log_data is not None and len(st.session_state.ui_log_data) > 0
|
| 1035 |
-
|
| 1036 |
-
if has_messages:
|
| 1037 |
-
st.markdown("---")
|
| 1038 |
-
st.subheader("📨 Current Experiment Messages")
|
| 1039 |
-
|
| 1040 |
-
# Calculate stats from session_state
|
| 1041 |
-
if st.session_state.ab_testing_mode:
|
| 1042 |
-
total_messages_a = len(st.session_state.ui_log_data_a) if st.session_state.ui_log_data_a is not None else 0
|
| 1043 |
-
total_messages_b = len(st.session_state.ui_log_data_b) if st.session_state.ui_log_data_b is not None else 0
|
| 1044 |
-
total_messages = total_messages_a + total_messages_b
|
| 1045 |
-
stats = {
|
| 1046 |
-
"total_messages": total_messages,
|
| 1047 |
-
"experiment_a": total_messages_a,
|
| 1048 |
-
"experiment_b": total_messages_b,
|
| 1049 |
-
"mode": "AB Testing"
|
| 1050 |
-
}
|
| 1051 |
-
else:
|
| 1052 |
-
total_messages = len(st.session_state.ui_log_data) if st.session_state.ui_log_data is not None else 0
|
| 1053 |
-
total_users = st.session_state.ui_log_data['user_id'].nunique() if st.session_state.ui_log_data is not None and 'user_id' in st.session_state.ui_log_data.columns else 0
|
| 1054 |
-
total_stages = st.session_state.ui_log_data['stage'].nunique() if st.session_state.ui_log_data is not None and 'stage' in st.session_state.ui_log_data.columns else 0
|
| 1055 |
-
stats = {
|
| 1056 |
-
"total_messages": total_messages,
|
| 1057 |
-
"total_users": total_users,
|
| 1058 |
-
"total_stages": total_stages,
|
| 1059 |
-
"mode": "Single"
|
| 1060 |
-
}
|
| 1061 |
-
|
| 1062 |
-
if st.session_state.ab_testing_mode:
|
| 1063 |
-
col1, col2, col3 = st.columns(3)
|
| 1064 |
-
|
| 1065 |
-
with col1:
|
| 1066 |
-
st.metric("Total Messages", stats['total_messages'])
|
| 1067 |
-
|
| 1068 |
-
with col2:
|
| 1069 |
-
st.metric("Experiment A", stats['experiment_a'])
|
| 1070 |
-
|
| 1071 |
-
with col3:
|
| 1072 |
-
st.metric("Experiment B", stats['experiment_b'])
|
| 1073 |
-
else:
|
| 1074 |
-
col1, col2, col3 = st.columns(3)
|
| 1075 |
-
|
| 1076 |
-
with col1:
|
| 1077 |
-
st.metric("Total Messages", stats['total_messages'])
|
| 1078 |
-
|
| 1079 |
-
with col2:
|
| 1080 |
-
st.metric("Total Users", stats['total_users'])
|
| 1081 |
-
|
| 1082 |
-
with col3:
|
| 1083 |
-
st.metric("Total Stages", stats['total_stages'])
|
| 1084 |
-
|
| 1085 |
-
if st.button("🗑️ Clear Experiment Data from Memory", use_container_width=False):
|
| 1086 |
-
# Clear session_state data
|
| 1087 |
-
st.session_state.ui_log_data = None
|
| 1088 |
-
st.session_state.current_experiment_metadata = []
|
| 1089 |
-
st.session_state.current_feedbacks = []
|
| 1090 |
-
st.session_state.ui_log_data_a = None
|
| 1091 |
-
st.session_state.ui_log_data_b = None
|
| 1092 |
-
st.session_state.experiment_a_metadata = []
|
| 1093 |
-
st.session_state.experiment_b_metadata = []
|
| 1094 |
-
st.session_state.feedbacks_a = []
|
| 1095 |
-
st.session_state.feedbacks_b = []
|
| 1096 |
-
st.success("✅ All experiment data cleared from memory")
|
| 1097 |
-
st.rerun()
|
| 1098 |
-
|
| 1099 |
-
st.markdown("---")
|
| 1100 |
-
st.markdown("**���� Tip:** Use A/B testing to compare different configurations with the same users!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/pages/2_Message_Viewer.py
DELETED
|
@@ -1,939 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Page 2: Message Viewer
|
| 3 |
-
Browse, search, and evaluate generated messages with feedback system.
|
| 4 |
-
Supports A/B testing mode for comparing two experiments side-by-side.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import streamlit as st
|
| 8 |
-
import sys
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
import json
|
| 11 |
-
import pandas as pd
|
| 12 |
-
import re
|
| 13 |
-
from datetime import datetime
|
| 14 |
-
|
| 15 |
-
# Add parent directories to path
|
| 16 |
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 17 |
-
|
| 18 |
-
from utils.auth import check_authentication
|
| 19 |
-
from utils.theme import apply_theme, get_brand_emoji, create_badge
|
| 20 |
-
from utils.data_loader import DataLoader
|
| 21 |
-
from utils.session_feedback_manager import SessionFeedbackManager
|
| 22 |
-
from utils.db_manager import UIDatabaseManager
|
| 23 |
-
from snowflake.snowpark import Session
|
| 24 |
-
import os
|
| 25 |
-
|
| 26 |
-
# Page configuration
|
| 27 |
-
st.set_page_config(
|
| 28 |
-
page_title="Message Viewer",
|
| 29 |
-
page_icon="👀",
|
| 30 |
-
layout="wide"
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
# Check authentication
|
| 34 |
-
if not check_authentication():
|
| 35 |
-
st.error("🔒 Please login first")
|
| 36 |
-
st.stop()
|
| 37 |
-
|
| 38 |
-
# Initialize utilities
|
| 39 |
-
data_loader = DataLoader()
|
| 40 |
-
# Note: Using SessionFeedbackManager for in-memory feedback (no files)
|
| 41 |
-
|
| 42 |
-
# Helper function to create Snowflake session
|
| 43 |
-
def create_snowflake_session() -> Session:
|
| 44 |
-
"""Create a Snowflake session using environment variables."""
|
| 45 |
-
conn_params = {
|
| 46 |
-
"user": os.getenv("SNOWFLAKE_USER"),
|
| 47 |
-
"password": os.getenv("SNOWFLAKE_PASSWORD"),
|
| 48 |
-
"account": os.getenv("SNOWFLAKE_ACCOUNT"),
|
| 49 |
-
"role": os.getenv("SNOWFLAKE_ROLE"),
|
| 50 |
-
"database": os.getenv("SNOWFLAKE_DATABASE"),
|
| 51 |
-
"warehouse": os.getenv("SNOWFLAKE_WAREHOUSE"),
|
| 52 |
-
"schema": os.getenv("SNOWFLAKE_SCHEMA"),
|
| 53 |
-
}
|
| 54 |
-
return Session.builder.configs(conn_params).create()
|
| 55 |
-
|
| 56 |
-
# Check if brand is selected
|
| 57 |
-
if "selected_brand" not in st.session_state or not st.session_state.selected_brand:
|
| 58 |
-
st.error("⚠️ Please select a brand from the home page first")
|
| 59 |
-
st.stop()
|
| 60 |
-
|
| 61 |
-
brand = st.session_state.selected_brand
|
| 62 |
-
apply_theme(brand)
|
| 63 |
-
|
| 64 |
-
# Helper functions for detecting mode from session_state
|
| 65 |
-
def detect_ab_testing_mode():
|
| 66 |
-
"""
|
| 67 |
-
Detect if we're in A/B testing mode from session_state.
|
| 68 |
-
Returns (is_ab_mode, messages_a_df, messages_b_df, experiment_a_id, experiment_b_id)
|
| 69 |
-
"""
|
| 70 |
-
if st.session_state.get('ab_testing_mode', False):
|
| 71 |
-
# Check if we have AB test data in session_state
|
| 72 |
-
has_data_a = st.session_state.get('ui_log_data_a') is not None
|
| 73 |
-
has_data_b = st.session_state.get('ui_log_data_b') is not None
|
| 74 |
-
|
| 75 |
-
if has_data_a and has_data_b:
|
| 76 |
-
return (
|
| 77 |
-
True,
|
| 78 |
-
st.session_state.ui_log_data_a,
|
| 79 |
-
st.session_state.ui_log_data_b,
|
| 80 |
-
st.session_state.get('experiment_a_id'),
|
| 81 |
-
st.session_state.get('experiment_b_id')
|
| 82 |
-
)
|
| 83 |
-
|
| 84 |
-
return False, None, None, None, None
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def get_single_experiment_data():
|
| 88 |
-
"""
|
| 89 |
-
Get single mode experiment data from session_state.
|
| 90 |
-
Returns (messages_df, experiment_id)
|
| 91 |
-
"""
|
| 92 |
-
messages_df = st.session_state.get('ui_log_data')
|
| 93 |
-
experiment_id = st.session_state.get('current_experiment_id')
|
| 94 |
-
|
| 95 |
-
if messages_df is not None and len(messages_df) > 0:
|
| 96 |
-
return messages_df, experiment_id
|
| 97 |
-
|
| 98 |
-
return None, None
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
# Page Header
|
| 102 |
-
emoji = get_brand_emoji(brand)
|
| 103 |
-
st.title(f"👀 Message Viewer - {emoji} {brand.title()}")
|
| 104 |
-
st.markdown("**Browse and evaluate generated messages**")
|
| 105 |
-
|
| 106 |
-
# Detect A/B testing mode from session_state
|
| 107 |
-
is_ab_mode, messages_a_df, messages_b_df, experiment_a_id, experiment_b_id = detect_ab_testing_mode()
|
| 108 |
-
|
| 109 |
-
if is_ab_mode:
|
| 110 |
-
st.info(f"🔬 **A/B Testing Mode Active** - Comparing two experiments side-by-side")
|
| 111 |
-
|
| 112 |
-
st.markdown("---")
|
| 113 |
-
|
| 114 |
-
# Load messages based on mode
|
| 115 |
-
if is_ab_mode:
|
| 116 |
-
# AB mode - data already loaded from session_state
|
| 117 |
-
if messages_a_df is None or len(messages_a_df) == 0 or messages_b_df is None or len(messages_b_df) == 0:
|
| 118 |
-
st.warning("⚠️ No A/B test messages found in memory. Please generate messages first in Campaign Builder.")
|
| 119 |
-
if st.button("🏗️ Go to Campaign Builder"):
|
| 120 |
-
st.switch_page("pages/1_Campaign_Builder.py")
|
| 121 |
-
st.stop()
|
| 122 |
-
|
| 123 |
-
messages_df = None # Not used in AB mode
|
| 124 |
-
else:
|
| 125 |
-
# Single mode - load from session_state
|
| 126 |
-
messages_df, experiment_id = get_single_experiment_data()
|
| 127 |
-
|
| 128 |
-
if messages_df is None or len(messages_df) == 0:
|
| 129 |
-
st.warning("⚠️ No messages found in memory. Please generate messages first in Campaign Builder.")
|
| 130 |
-
if st.button("🏗️ Go to Campaign Builder"):
|
| 131 |
-
st.switch_page("pages/1_Campaign_Builder.py")
|
| 132 |
-
st.stop()
|
| 133 |
-
|
| 134 |
-
# Ensure experiment ID is set
|
| 135 |
-
if experiment_id is None or not experiment_id:
|
| 136 |
-
# Create experiment ID based on campaign name and timestamp
|
| 137 |
-
if 'campaign_name' in messages_df.columns and len(messages_df) > 0:
|
| 138 |
-
campaign_name = messages_df['campaign_name'].iloc[0]
|
| 139 |
-
experiment_id = campaign_name.replace(" ", "_")
|
| 140 |
-
else:
|
| 141 |
-
experiment_id = f"{brand}_experiment"
|
| 142 |
-
|
| 143 |
-
st.session_state.current_experiment_id = experiment_id
|
| 144 |
-
|
| 145 |
-
experiment_id = st.session_state.current_experiment_id
|
| 146 |
-
messages_a_df = None
|
| 147 |
-
messages_b_df = None
|
| 148 |
-
experiment_a_id = None
|
| 149 |
-
experiment_b_id = None
|
| 150 |
-
|
| 151 |
-
# Sidebar - Filters and Settings
|
| 152 |
-
with st.sidebar:
|
| 153 |
-
st.header("🔍 Filters & Settings")
|
| 154 |
-
|
| 155 |
-
# View mode
|
| 156 |
-
if is_ab_mode:
|
| 157 |
-
st.markdown("**View Mode (applies to both experiments)**")
|
| 158 |
-
view_mode = st.radio(
|
| 159 |
-
"View Mode",
|
| 160 |
-
["User-Centric", "Stage-Centric"],
|
| 161 |
-
help="User-Centric: All stages for each user\nStage-Centric: All users for each stage"
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
st.markdown("---")
|
| 165 |
-
|
| 166 |
-
# Stage filter
|
| 167 |
-
if is_ab_mode:
|
| 168 |
-
# Get stages from both dataframes
|
| 169 |
-
stages_a = sorted(messages_a_df['stage'].unique()) if 'stage' in messages_a_df.columns else []
|
| 170 |
-
stages_b = sorted(messages_b_df['stage'].unique()) if 'stage' in messages_b_df.columns else []
|
| 171 |
-
available_stages = sorted(list(set(stages_a + stages_b)))
|
| 172 |
-
else:
|
| 173 |
-
available_stages = sorted(messages_df['stage'].unique()) if 'stage' in messages_df.columns else []
|
| 174 |
-
|
| 175 |
-
selected_stages = st.multiselect(
|
| 176 |
-
"Filter by Stage",
|
| 177 |
-
available_stages,
|
| 178 |
-
default=available_stages,
|
| 179 |
-
help="Select specific stages to view" + (" (applies to both experiments)" if is_ab_mode else "")
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
# Search
|
| 183 |
-
search_term = st.text_input(
|
| 184 |
-
"🔎 Search Messages",
|
| 185 |
-
placeholder="Enter keyword...",
|
| 186 |
-
help="Search for specific keywords in messages" + (" (applies to both experiments)" if is_ab_mode else "")
|
| 187 |
-
)
|
| 188 |
-
|
| 189 |
-
st.markdown("---")
|
| 190 |
-
|
| 191 |
-
# Display settings
|
| 192 |
-
st.subheader("⚙️ Display Settings")
|
| 193 |
-
|
| 194 |
-
show_feedback = st.checkbox(
|
| 195 |
-
"Show Feedback Options",
|
| 196 |
-
value=True,
|
| 197 |
-
help="Show reject button and feedback form"
|
| 198 |
-
)
|
| 199 |
-
|
| 200 |
-
messages_per_page = st.number_input(
|
| 201 |
-
"Messages per Page",
|
| 202 |
-
min_value=5,
|
| 203 |
-
max_value=100,
|
| 204 |
-
value=20,
|
| 205 |
-
help="Number of messages to display per page"
|
| 206 |
-
)
|
| 207 |
-
|
| 208 |
-
# Filter messages
|
| 209 |
-
if is_ab_mode:
|
| 210 |
-
# Filter both experiments
|
| 211 |
-
filtered_messages_a = messages_a_df.copy()
|
| 212 |
-
filtered_messages_b = messages_b_df.copy()
|
| 213 |
-
|
| 214 |
-
if selected_stages:
|
| 215 |
-
filtered_messages_a = filtered_messages_a[filtered_messages_a['stage'].isin(selected_stages)]
|
| 216 |
-
filtered_messages_b = filtered_messages_b[filtered_messages_b['stage'].isin(selected_stages)]
|
| 217 |
-
|
| 218 |
-
if search_term:
|
| 219 |
-
filtered_messages_a = data_loader.search_messages(filtered_messages_a, search_term)
|
| 220 |
-
filtered_messages_b = data_loader.search_messages(filtered_messages_b, search_term)
|
| 221 |
-
|
| 222 |
-
filtered_messages = None # Not used in AB mode
|
| 223 |
-
else:
|
| 224 |
-
filtered_messages = messages_df.copy()
|
| 225 |
-
|
| 226 |
-
if selected_stages:
|
| 227 |
-
filtered_messages = filtered_messages[filtered_messages['stage'].isin(selected_stages)]
|
| 228 |
-
|
| 229 |
-
if search_term:
|
| 230 |
-
filtered_messages = data_loader.search_messages(filtered_messages, search_term)
|
| 231 |
-
|
| 232 |
-
filtered_messages_a = None
|
| 233 |
-
filtered_messages_b = None
|
| 234 |
-
|
| 235 |
-
# Summary stats
|
| 236 |
-
st.subheader("📊 Summary")
|
| 237 |
-
|
| 238 |
-
if is_ab_mode:
|
| 239 |
-
# Show stats for both experiments
|
| 240 |
-
st.markdown("##### Experiment A vs Experiment B")
|
| 241 |
-
|
| 242 |
-
col1, col2, col3, col4 = st.columns(4)
|
| 243 |
-
|
| 244 |
-
with col1:
|
| 245 |
-
st.metric("Total Messages (A)", len(filtered_messages_a))
|
| 246 |
-
st.metric("Total Messages (B)", len(filtered_messages_b))
|
| 247 |
-
|
| 248 |
-
with col2:
|
| 249 |
-
unique_users_a = filtered_messages_a['user_id'].nunique() if 'user_id' in filtered_messages_a.columns else 0
|
| 250 |
-
unique_users_b = filtered_messages_b['user_id'].nunique() if 'user_id' in filtered_messages_b.columns else 0
|
| 251 |
-
st.metric("Unique Users (A)", unique_users_a)
|
| 252 |
-
st.metric("Unique Users (B)", unique_users_b)
|
| 253 |
-
|
| 254 |
-
with col3:
|
| 255 |
-
unique_stages_a = filtered_messages_a['stage'].nunique() if 'stage' in filtered_messages_a.columns else 0
|
| 256 |
-
unique_stages_b = filtered_messages_b['stage'].nunique() if 'stage' in filtered_messages_b.columns else 0
|
| 257 |
-
st.metric("Stages (A)", unique_stages_a)
|
| 258 |
-
st.metric("Stages (B)", unique_stages_b)
|
| 259 |
-
|
| 260 |
-
with col4:
|
| 261 |
-
# Get feedback stats from session_state
|
| 262 |
-
feedback_stats_a = SessionFeedbackManager.get_feedback_stats(
|
| 263 |
-
experiment_a_id,
|
| 264 |
-
total_messages=len(filtered_messages_a),
|
| 265 |
-
feedback_list_key="feedbacks_a"
|
| 266 |
-
)
|
| 267 |
-
feedback_stats_b = SessionFeedbackManager.get_feedback_stats(
|
| 268 |
-
experiment_b_id,
|
| 269 |
-
total_messages=len(filtered_messages_b),
|
| 270 |
-
feedback_list_key="feedbacks_b"
|
| 271 |
-
)
|
| 272 |
-
|
| 273 |
-
st.metric("Rejected (A)", feedback_stats_a['total_rejects'])
|
| 274 |
-
st.metric("Rejected (B)", feedback_stats_b['total_rejects'])
|
| 275 |
-
else:
|
| 276 |
-
# Single experiment stats
|
| 277 |
-
col1, col2, col3, col4 = st.columns(4)
|
| 278 |
-
|
| 279 |
-
with col1:
|
| 280 |
-
st.metric("Total Messages", len(filtered_messages))
|
| 281 |
-
|
| 282 |
-
with col2:
|
| 283 |
-
unique_users = filtered_messages['user_id'].nunique() if 'user_id' in filtered_messages.columns else 0
|
| 284 |
-
st.metric("Unique Users", unique_users)
|
| 285 |
-
|
| 286 |
-
with col3:
|
| 287 |
-
unique_stages = filtered_messages['stage'].nunique() if 'stage' in filtered_messages.columns else 0
|
| 288 |
-
st.metric("Stages", unique_stages)
|
| 289 |
-
|
| 290 |
-
with col4:
|
| 291 |
-
# Get feedback stats from session_state
|
| 292 |
-
feedback_stats = SessionFeedbackManager.get_feedback_stats(
|
| 293 |
-
experiment_id,
|
| 294 |
-
total_messages=len(filtered_messages),
|
| 295 |
-
feedback_list_key="current_feedbacks"
|
| 296 |
-
)
|
| 297 |
-
st.metric("Rejected", feedback_stats['total_rejects'])
|
| 298 |
-
|
| 299 |
-
st.markdown("---")
|
| 300 |
-
|
| 301 |
-
# Helper function to parse message JSON
|
| 302 |
-
def parse_message_content(message_str):
|
| 303 |
-
"""Parse message JSON string and extract content."""
|
| 304 |
-
try:
|
| 305 |
-
if isinstance(message_str, str):
|
| 306 |
-
message_data = json.loads(message_str)
|
| 307 |
-
elif isinstance(message_str, dict):
|
| 308 |
-
message_data = message_str
|
| 309 |
-
else:
|
| 310 |
-
return None
|
| 311 |
-
|
| 312 |
-
# Handle different message formats
|
| 313 |
-
if isinstance(message_data, dict):
|
| 314 |
-
# Check for stage-keyed format: {"1": {"header": ..., "message": ...}}
|
| 315 |
-
if any(k.isdigit() for k in message_data.keys()):
|
| 316 |
-
# Extract messages from all stages
|
| 317 |
-
messages = []
|
| 318 |
-
for key in sorted(message_data.keys(), key=lambda x: int(x) if x.isdigit() else 0):
|
| 319 |
-
if isinstance(message_data[key], dict):
|
| 320 |
-
messages.append(message_data[key])
|
| 321 |
-
return messages
|
| 322 |
-
else:
|
| 323 |
-
# Single message format
|
| 324 |
-
return [message_data]
|
| 325 |
-
elif isinstance(message_data, list):
|
| 326 |
-
return message_data
|
| 327 |
-
|
| 328 |
-
except (json.JSONDecodeError, TypeError):
|
| 329 |
-
return None
|
| 330 |
-
|
| 331 |
-
return None
|
| 332 |
-
|
| 333 |
-
# Helper function to parse and display recommendation
|
| 334 |
-
def parse_recommendation(rec_str):
|
| 335 |
-
"""Parse recommendation JSON string and extract details."""
|
| 336 |
-
try:
|
| 337 |
-
if isinstance(rec_str, str):
|
| 338 |
-
rec_data = json.loads(rec_str)
|
| 339 |
-
elif isinstance(rec_str, dict):
|
| 340 |
-
rec_data = rec_str
|
| 341 |
-
else:
|
| 342 |
-
return None
|
| 343 |
-
|
| 344 |
-
if isinstance(rec_data, dict):
|
| 345 |
-
return rec_data
|
| 346 |
-
|
| 347 |
-
except (json.JSONDecodeError, TypeError):
|
| 348 |
-
return None
|
| 349 |
-
|
| 350 |
-
return None
|
| 351 |
-
|
| 352 |
-
def display_recommendation(recommendation):
|
| 353 |
-
"""Display recommendation with thumbnail and link."""
|
| 354 |
-
if not recommendation:
|
| 355 |
-
return
|
| 356 |
-
|
| 357 |
-
rec_data = parse_recommendation(recommendation)
|
| 358 |
-
|
| 359 |
-
if rec_data:
|
| 360 |
-
# Extract recommendation details
|
| 361 |
-
title = rec_data.get('title', 'Recommended Content')
|
| 362 |
-
web_url = rec_data.get('web_url_path', '#')
|
| 363 |
-
thumbnail_url = rec_data.get('thumbnail_url', '')
|
| 364 |
-
|
| 365 |
-
# Create a clickable card with thumbnail
|
| 366 |
-
st.markdown("**📍 Recommended Content:**")
|
| 367 |
-
|
| 368 |
-
if thumbnail_url:
|
| 369 |
-
# Use container to prevent layout shift during image loading
|
| 370 |
-
# Note: Images are displayed without columns to prevent flickering on HuggingFace
|
| 371 |
-
try:
|
| 372 |
-
# Use a fixed container to prevent layout shifts
|
| 373 |
-
st.image(thumbnail_url, width=200) # Fixed width prevents flickering
|
| 374 |
-
except Exception:
|
| 375 |
-
# Fallback if image fails to load
|
| 376 |
-
st.markdown("🖼️ *[Image unavailable]*")
|
| 377 |
-
|
| 378 |
-
st.markdown(f"**{title}**")
|
| 379 |
-
if web_url and web_url != '#':
|
| 380 |
-
st.markdown(f"[🔗 View Content]({web_url})")
|
| 381 |
-
else:
|
| 382 |
-
# No thumbnail, just show title and link
|
| 383 |
-
st.markdown(f"**{title}**")
|
| 384 |
-
if web_url and web_url != '#':
|
| 385 |
-
st.markdown(f"[🔗 View Content]({web_url})")
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
def display_user_centric_messages(filtered_df, exp_id, key_suffix, show_feedback_option, msgs_per_page):
|
| 389 |
-
"""Display messages in user-centric view for a single experiment."""
|
| 390 |
-
if 'user_id' not in filtered_df.columns:
|
| 391 |
-
st.warning("No user_id column found in messages.")
|
| 392 |
-
return
|
| 393 |
-
|
| 394 |
-
unique_users = filtered_df['user_id'].unique()
|
| 395 |
-
|
| 396 |
-
# Pagination
|
| 397 |
-
total_users = len(unique_users)
|
| 398 |
-
users_per_page = msgs_per_page
|
| 399 |
-
|
| 400 |
-
if total_users > users_per_page:
|
| 401 |
-
page = st.number_input(
|
| 402 |
-
f"Page",
|
| 403 |
-
min_value=1,
|
| 404 |
-
max_value=(total_users // users_per_page) + 1,
|
| 405 |
-
value=1,
|
| 406 |
-
key=f"page_{key_suffix}"
|
| 407 |
-
)
|
| 408 |
-
start_idx = (page - 1) * users_per_page
|
| 409 |
-
end_idx = min(start_idx + users_per_page, total_users)
|
| 410 |
-
users_to_display = unique_users[start_idx:end_idx]
|
| 411 |
-
|
| 412 |
-
st.markdown(f"*Showing users {start_idx + 1}-{end_idx} of {total_users}*")
|
| 413 |
-
else:
|
| 414 |
-
users_to_display = unique_users
|
| 415 |
-
|
| 416 |
-
# Display each user
|
| 417 |
-
for user_id in users_to_display:
|
| 418 |
-
user_messages = filtered_df[filtered_df['user_id'] == user_id].sort_values('stage')
|
| 419 |
-
|
| 420 |
-
if len(user_messages) == 0:
|
| 421 |
-
continue
|
| 422 |
-
|
| 423 |
-
# Get user info from first message
|
| 424 |
-
first_msg = user_messages.iloc[0]
|
| 425 |
-
user_email = first_msg.get('email', 'N/A')
|
| 426 |
-
user_first_name = first_msg.get('first_name', 'N/A')
|
| 427 |
-
|
| 428 |
-
# User expander
|
| 429 |
-
with st.expander(f"👤 User {user_id} - {user_first_name} ({user_email})", expanded=False):
|
| 430 |
-
# User profile
|
| 431 |
-
st.markdown("##### 📋 User Profile")
|
| 432 |
-
|
| 433 |
-
profile_cols = st.columns(4)
|
| 434 |
-
profile_fields = ['first_name', 'country', 'instrument', 'subscription_status']
|
| 435 |
-
|
| 436 |
-
for idx, field in enumerate(profile_fields):
|
| 437 |
-
if field in first_msg:
|
| 438 |
-
profile_cols[idx % 4].markdown(f"**{field.replace('_', ' ').title()}:** {first_msg[field]}")
|
| 439 |
-
|
| 440 |
-
st.markdown("---")
|
| 441 |
-
|
| 442 |
-
# Display all stages for this user
|
| 443 |
-
st.markdown("##### 📨 Messages Across All Stages")
|
| 444 |
-
|
| 445 |
-
for _, row in user_messages.iterrows():
|
| 446 |
-
stage = row['stage']
|
| 447 |
-
|
| 448 |
-
# Parse message content
|
| 449 |
-
message_content = parse_message_content(row.get('message', ''))
|
| 450 |
-
|
| 451 |
-
if message_content:
|
| 452 |
-
# Find the message for this stage
|
| 453 |
-
stage_message = None
|
| 454 |
-
for msg in message_content:
|
| 455 |
-
if isinstance(msg, dict):
|
| 456 |
-
stage_message = msg
|
| 457 |
-
break
|
| 458 |
-
|
| 459 |
-
if stage_message:
|
| 460 |
-
# Create message card
|
| 461 |
-
st.markdown(f"**Stage {stage}**")
|
| 462 |
-
|
| 463 |
-
msg_col1, msg_col2 = st.columns([4, 1])
|
| 464 |
-
|
| 465 |
-
with msg_col1:
|
| 466 |
-
header = stage_message.get('header', 'No header')
|
| 467 |
-
message_text = stage_message.get('message', 'No message')
|
| 468 |
-
|
| 469 |
-
st.markdown(f"**📧 Header:** {header}")
|
| 470 |
-
st.markdown(f"**💬 Message:** {message_text}")
|
| 471 |
-
|
| 472 |
-
# Show recommendation if available
|
| 473 |
-
if 'thumbnail_url' in stage_message or 'web_url_path' in stage_message:
|
| 474 |
-
rec_data = {
|
| 475 |
-
'title': stage_message.get('title', 'Recommended Content'),
|
| 476 |
-
'web_url_path': stage_message.get('web_url_path', '#'),
|
| 477 |
-
'thumbnail_url': stage_message.get('thumbnail_url', '')
|
| 478 |
-
}
|
| 479 |
-
display_recommendation(rec_data)
|
| 480 |
-
elif 'recommendation' in row and pd.notna(row['recommendation']):
|
| 481 |
-
display_recommendation(row['recommendation'])
|
| 482 |
-
|
| 483 |
-
with msg_col2:
|
| 484 |
-
# Feedback section
|
| 485 |
-
if show_feedback_option:
|
| 486 |
-
# Determine feedback list key based on mode
|
| 487 |
-
if is_ab_mode:
|
| 488 |
-
feedback_key = "feedbacks_a" if "exp_a" in key_suffix else "feedbacks_b"
|
| 489 |
-
else:
|
| 490 |
-
feedback_key = "current_feedbacks"
|
| 491 |
-
|
| 492 |
-
existing_feedback = SessionFeedbackManager.get_feedback(
|
| 493 |
-
exp_id, user_id, stage, feedback_list_key=feedback_key
|
| 494 |
-
)
|
| 495 |
-
|
| 496 |
-
if existing_feedback:
|
| 497 |
-
st.error("❌ Rejected")
|
| 498 |
-
if st.button(f"Undo", key=f"undo_{user_id}_{stage}_{key_suffix}"):
|
| 499 |
-
SessionFeedbackManager.delete_feedback(exp_id, user_id, stage, feedback_list_key=feedback_key)
|
| 500 |
-
st.rerun()
|
| 501 |
-
else:
|
| 502 |
-
if st.button("🚫 Reject", key=f"reject_{user_id}_{stage}_{key_suffix}"):
|
| 503 |
-
st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}"] = True
|
| 504 |
-
st.rerun()
|
| 505 |
-
|
| 506 |
-
# Feedback form
|
| 507 |
-
if st.session_state.get(f"show_feedback_form_{user_id}_{stage}_{key_suffix}", False):
|
| 508 |
-
with st.form(f"feedback_form_{user_id}_{stage}_{key_suffix}"):
|
| 509 |
-
st.markdown("**Why is this message rejected?**")
|
| 510 |
-
|
| 511 |
-
rejection_reasons = SessionFeedbackManager.get_all_rejection_reasons()
|
| 512 |
-
selected_reason = st.selectbox(
|
| 513 |
-
"Rejection Reason",
|
| 514 |
-
list(rejection_reasons.keys()),
|
| 515 |
-
format_func=lambda x: rejection_reasons[x],
|
| 516 |
-
key=f"reason_{user_id}_{stage}_{key_suffix}"
|
| 517 |
-
)
|
| 518 |
-
|
| 519 |
-
rejection_text = st.text_area(
|
| 520 |
-
"Additional Comments (Optional)",
|
| 521 |
-
key=f"text_{user_id}_{stage}_{key_suffix}"
|
| 522 |
-
)
|
| 523 |
-
|
| 524 |
-
col1, col2 = st.columns(2)
|
| 525 |
-
|
| 526 |
-
with col1:
|
| 527 |
-
if st.form_submit_button("💾 Save", use_container_width=True):
|
| 528 |
-
# Determine feedback list key based on mode
|
| 529 |
-
if is_ab_mode:
|
| 530 |
-
feedback_key = "feedbacks_a" if "exp_a" in key_suffix else "feedbacks_b"
|
| 531 |
-
else:
|
| 532 |
-
feedback_key = "current_feedbacks"
|
| 533 |
-
|
| 534 |
-
# Get config name from session_state
|
| 535 |
-
config_name = row.get('campaign_name', 'Unknown')
|
| 536 |
-
|
| 537 |
-
SessionFeedbackManager.add_feedback(
|
| 538 |
-
experiment_id=exp_id,
|
| 539 |
-
user_id=user_id,
|
| 540 |
-
stage=stage,
|
| 541 |
-
feedback_type="reject",
|
| 542 |
-
rejection_reason=selected_reason,
|
| 543 |
-
rejection_text=rejection_text,
|
| 544 |
-
campaign_name=row.get('campaign_name'),
|
| 545 |
-
config_name=config_name,
|
| 546 |
-
brand=brand,
|
| 547 |
-
message_header=stage_message.get('header', 'N/A'),
|
| 548 |
-
message_body=stage_message.get('message', 'N/A'),
|
| 549 |
-
feedback_list_key=feedback_key
|
| 550 |
-
)
|
| 551 |
-
st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}"] = False
|
| 552 |
-
st.success("✅ Feedback saved to memory")
|
| 553 |
-
st.rerun()
|
| 554 |
-
|
| 555 |
-
with col2:
|
| 556 |
-
if st.form_submit_button("❌ Cancel", use_container_width=True):
|
| 557 |
-
st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}"] = False
|
| 558 |
-
st.rerun()
|
| 559 |
-
|
| 560 |
-
st.markdown("---")
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
def display_stage_centric_messages(filtered_df, exp_id, key_suffix, show_feedback_option, msgs_per_page, stages_to_display):
|
| 564 |
-
"""Display messages in stage-centric view for a single experiment."""
|
| 565 |
-
# Group by stage
|
| 566 |
-
for stage in sorted(stages_to_display if stages_to_display else filtered_df['stage'].unique()):
|
| 567 |
-
stage_messages = filtered_df[filtered_df['stage'] == stage]
|
| 568 |
-
|
| 569 |
-
if len(stage_messages) == 0:
|
| 570 |
-
continue
|
| 571 |
-
|
| 572 |
-
with st.expander(f"📨 Stage {stage} - {len(stage_messages)} messages", expanded=(stage == 1)):
|
| 573 |
-
st.markdown(f"##### Stage {stage} Messages")
|
| 574 |
-
|
| 575 |
-
# Pagination for this stage
|
| 576 |
-
total_stage_messages = len(stage_messages)
|
| 577 |
-
stage_messages_per_page = msgs_per_page
|
| 578 |
-
|
| 579 |
-
if total_stage_messages > stage_messages_per_page:
|
| 580 |
-
page = st.number_input(
|
| 581 |
-
f"Page for Stage {stage}",
|
| 582 |
-
min_value=1,
|
| 583 |
-
max_value=(total_stage_messages // stage_messages_per_page) + 1,
|
| 584 |
-
value=1,
|
| 585 |
-
key=f"page_stage_{stage}_{key_suffix}"
|
| 586 |
-
)
|
| 587 |
-
start_idx = (page - 1) * stage_messages_per_page
|
| 588 |
-
end_idx = min(start_idx + stage_messages_per_page, total_stage_messages)
|
| 589 |
-
messages_to_display = stage_messages.iloc[start_idx:end_idx]
|
| 590 |
-
|
| 591 |
-
st.markdown(f"*Showing messages {start_idx + 1}-{end_idx} of {total_stage_messages}*")
|
| 592 |
-
else:
|
| 593 |
-
messages_to_display = stage_messages
|
| 594 |
-
|
| 595 |
-
# Display messages
|
| 596 |
-
for idx, row in messages_to_display.iterrows():
|
| 597 |
-
user_id = row.get('user_id', 'N/A')
|
| 598 |
-
user_email = row.get('email', 'N/A')
|
| 599 |
-
user_name = row.get('first_name', 'N/A')
|
| 600 |
-
|
| 601 |
-
# Parse message content
|
| 602 |
-
message_content = parse_message_content(row.get('message', ''))
|
| 603 |
-
|
| 604 |
-
if message_content:
|
| 605 |
-
stage_message = None
|
| 606 |
-
for msg in message_content:
|
| 607 |
-
if isinstance(msg, dict):
|
| 608 |
-
stage_message = msg
|
| 609 |
-
break
|
| 610 |
-
|
| 611 |
-
if stage_message:
|
| 612 |
-
# Message card
|
| 613 |
-
msg_col1, msg_col2 = st.columns([4, 1])
|
| 614 |
-
|
| 615 |
-
with msg_col1:
|
| 616 |
-
st.markdown(f"**User:** {user_name} ({user_email}) - ID: {user_id}")
|
| 617 |
-
|
| 618 |
-
header = stage_message.get('header', 'No header')
|
| 619 |
-
message_text = stage_message.get('message', 'No message')
|
| 620 |
-
|
| 621 |
-
st.markdown(f"**�� Header:** {header}")
|
| 622 |
-
st.markdown(f"**💬 Message:** {message_text}")
|
| 623 |
-
|
| 624 |
-
# Show recommendation if available
|
| 625 |
-
if 'thumbnail_url' in stage_message or 'web_url_path' in stage_message:
|
| 626 |
-
rec_data = {
|
| 627 |
-
'title': stage_message.get('title', 'Recommended Content'),
|
| 628 |
-
'web_url_path': stage_message.get('web_url_path', '#'),
|
| 629 |
-
'thumbnail_url': stage_message.get('thumbnail_url', '')
|
| 630 |
-
}
|
| 631 |
-
display_recommendation(rec_data)
|
| 632 |
-
elif 'recommendation' in row and pd.notna(row['recommendation']):
|
| 633 |
-
display_recommendation(row['recommendation'])
|
| 634 |
-
|
| 635 |
-
with msg_col2:
|
| 636 |
-
# Feedback section
|
| 637 |
-
if show_feedback_option:
|
| 638 |
-
# Determine feedback list key based on mode
|
| 639 |
-
if is_ab_mode:
|
| 640 |
-
feedback_key = "feedbacks_a" if "exp_a" in key_suffix else "feedbacks_b"
|
| 641 |
-
else:
|
| 642 |
-
feedback_key = "current_feedbacks"
|
| 643 |
-
|
| 644 |
-
existing_feedback = SessionFeedbackManager.get_feedback(
|
| 645 |
-
exp_id, user_id, stage, feedback_list_key=feedback_key
|
| 646 |
-
)
|
| 647 |
-
|
| 648 |
-
if existing_feedback:
|
| 649 |
-
st.error("❌ Rejected")
|
| 650 |
-
if st.button(f"Undo", key=f"undo_{user_id}_{stage}_{key_suffix}_sc"):
|
| 651 |
-
SessionFeedbackManager.delete_feedback(exp_id, user_id, stage, feedback_list_key=feedback_key)
|
| 652 |
-
st.rerun()
|
| 653 |
-
else:
|
| 654 |
-
if st.button("🚫 Reject", key=f"reject_{user_id}_{stage}_{key_suffix}_sc"):
|
| 655 |
-
st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc"] = True
|
| 656 |
-
st.rerun()
|
| 657 |
-
|
| 658 |
-
# Feedback form
|
| 659 |
-
if st.session_state.get(f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc", False):
|
| 660 |
-
with st.form(f"feedback_form_{user_id}_{stage}_{key_suffix}_sc"):
|
| 661 |
-
st.markdown("**Why is this message rejected?**")
|
| 662 |
-
|
| 663 |
-
rejection_reasons = SessionFeedbackManager.get_all_rejection_reasons()
|
| 664 |
-
selected_reason = st.selectbox(
|
| 665 |
-
"Rejection Reason",
|
| 666 |
-
list(rejection_reasons.keys()),
|
| 667 |
-
format_func=lambda x: rejection_reasons[x],
|
| 668 |
-
key=f"reason_{user_id}_{stage}_{key_suffix}_sc"
|
| 669 |
-
)
|
| 670 |
-
|
| 671 |
-
rejection_text = st.text_area(
|
| 672 |
-
"Additional Comments (Optional)",
|
| 673 |
-
key=f"text_{user_id}_{stage}_{key_suffix}_sc"
|
| 674 |
-
)
|
| 675 |
-
|
| 676 |
-
col1, col2 = st.columns(2)
|
| 677 |
-
|
| 678 |
-
with col1:
|
| 679 |
-
if st.form_submit_button("💾 Save", use_container_width=True):
|
| 680 |
-
# Determine feedback list key based on mode
|
| 681 |
-
if is_ab_mode:
|
| 682 |
-
feedback_key = "feedbacks_a" if "exp_a" in key_suffix else "feedbacks_b"
|
| 683 |
-
else:
|
| 684 |
-
feedback_key = "current_feedbacks"
|
| 685 |
-
|
| 686 |
-
# Get config name from session_state
|
| 687 |
-
config_name = row.get('campaign_name', 'Unknown')
|
| 688 |
-
|
| 689 |
-
SessionFeedbackManager.add_feedback(
|
| 690 |
-
experiment_id=exp_id,
|
| 691 |
-
user_id=user_id,
|
| 692 |
-
stage=stage,
|
| 693 |
-
feedback_type="reject",
|
| 694 |
-
rejection_reason=selected_reason,
|
| 695 |
-
rejection_text=rejection_text,
|
| 696 |
-
campaign_name=row.get('campaign_name'),
|
| 697 |
-
config_name=config_name,
|
| 698 |
-
brand=brand,
|
| 699 |
-
message_header=stage_message.get('header', 'N/A'),
|
| 700 |
-
message_body=stage_message.get('message', 'N/A'),
|
| 701 |
-
feedback_list_key=feedback_key
|
| 702 |
-
)
|
| 703 |
-
st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc"] = False
|
| 704 |
-
st.success("✅ Feedback saved to memory")
|
| 705 |
-
st.rerun()
|
| 706 |
-
|
| 707 |
-
with col2:
|
| 708 |
-
if st.form_submit_button("❌ Cancel", use_container_width=True):
|
| 709 |
-
st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc"] = False
|
| 710 |
-
st.rerun()
|
| 711 |
-
|
| 712 |
-
st.markdown("---")
|
| 713 |
-
|
| 714 |
-
# Display messages based on mode and view
|
| 715 |
-
if is_ab_mode:
|
| 716 |
-
# A/B Testing Mode - Show both experiments side by side
|
| 717 |
-
if view_mode == "User-Centric":
|
| 718 |
-
st.subheader("👤 User-Centric View")
|
| 719 |
-
st.markdown("*All stages for each user - comparing both experiments*")
|
| 720 |
-
else:
|
| 721 |
-
st.subheader("📊 Stage-Centric View")
|
| 722 |
-
st.markdown("*All users for each stage - comparing both experiments*")
|
| 723 |
-
|
| 724 |
-
# Create two columns for A/B comparison
|
| 725 |
-
col_a, col_b = st.columns(2)
|
| 726 |
-
|
| 727 |
-
with col_a:
|
| 728 |
-
st.markdown("### 🅰️ Experiment A")
|
| 729 |
-
if view_mode == "User-Centric":
|
| 730 |
-
display_user_centric_messages(
|
| 731 |
-
filtered_messages_a,
|
| 732 |
-
experiment_a_id,
|
| 733 |
-
"exp_a",
|
| 734 |
-
show_feedback,
|
| 735 |
-
messages_per_page
|
| 736 |
-
)
|
| 737 |
-
else:
|
| 738 |
-
display_stage_centric_messages(
|
| 739 |
-
filtered_messages_a,
|
| 740 |
-
experiment_a_id,
|
| 741 |
-
"exp_a",
|
| 742 |
-
show_feedback,
|
| 743 |
-
messages_per_page,
|
| 744 |
-
selected_stages
|
| 745 |
-
)
|
| 746 |
-
|
| 747 |
-
with col_b:
|
| 748 |
-
st.markdown("### 🅱️ Experiment B")
|
| 749 |
-
if view_mode == "User-Centric":
|
| 750 |
-
display_user_centric_messages(
|
| 751 |
-
filtered_messages_b,
|
| 752 |
-
experiment_b_id,
|
| 753 |
-
"exp_b",
|
| 754 |
-
show_feedback,
|
| 755 |
-
messages_per_page
|
| 756 |
-
)
|
| 757 |
-
else:
|
| 758 |
-
display_stage_centric_messages(
|
| 759 |
-
filtered_messages_b,
|
| 760 |
-
experiment_b_id,
|
| 761 |
-
"exp_b",
|
| 762 |
-
show_feedback,
|
| 763 |
-
messages_per_page,
|
| 764 |
-
selected_stages
|
| 765 |
-
)
|
| 766 |
-
|
| 767 |
-
else:
|
| 768 |
-
# Single Experiment Mode - Original behavior
|
| 769 |
-
if view_mode == "User-Centric":
|
| 770 |
-
st.subheader("👤 User-Centric View")
|
| 771 |
-
st.markdown("*All stages for each user*")
|
| 772 |
-
display_user_centric_messages(
|
| 773 |
-
filtered_messages,
|
| 774 |
-
experiment_id,
|
| 775 |
-
"single",
|
| 776 |
-
show_feedback,
|
| 777 |
-
messages_per_page
|
| 778 |
-
)
|
| 779 |
-
else:
|
| 780 |
-
st.subheader("📊 Stage-Centric View")
|
| 781 |
-
st.markdown("*All users for each stage*")
|
| 782 |
-
display_stage_centric_messages(
|
| 783 |
-
filtered_messages,
|
| 784 |
-
experiment_id,
|
| 785 |
-
"single",
|
| 786 |
-
show_feedback,
|
| 787 |
-
messages_per_page,
|
| 788 |
-
selected_stages
|
| 789 |
-
)
|
| 790 |
-
|
| 791 |
-
st.markdown("---")
|
| 792 |
-
|
| 793 |
-
# ============================================================================
|
| 794 |
-
# STORE RESULTS TO SNOWFLAKE SECTION (CRITICAL NEW FEATURE)
|
| 795 |
-
# ============================================================================
|
| 796 |
-
|
| 797 |
-
st.subheader("💾 Store Experiment Results to Snowflake")
|
| 798 |
-
|
| 799 |
-
st.markdown("""
|
| 800 |
-
**Important:** Experiment results are currently stored in memory only. Click the button below to persist your experiment metadata and feedback to Snowflake for long-term storage and historical analysis.
|
| 801 |
-
""")
|
| 802 |
-
|
| 803 |
-
if is_ab_mode:
|
| 804 |
-
# AB Testing Mode
|
| 805 |
-
st.info(f"🔬 Ready to store A/B test results for:\n- **Experiment A**: {experiment_a_id}\n- **Experiment B**: {experiment_b_id}")
|
| 806 |
-
|
| 807 |
-
col1, col2 = st.columns(2)
|
| 808 |
-
|
| 809 |
-
with col1:
|
| 810 |
-
st.metric("Experiment A Feedback", len(st.session_state.get('feedbacks_a', [])))
|
| 811 |
-
st.metric("Experiment A Metadata", len(st.session_state.get('experiment_a_metadata', [])))
|
| 812 |
-
|
| 813 |
-
with col2:
|
| 814 |
-
st.metric("Experiment B Feedback", len(st.session_state.get('feedbacks_b', [])))
|
| 815 |
-
st.metric("Experiment B Metadata", len(st.session_state.get('experiment_b_metadata', [])))
|
| 816 |
-
|
| 817 |
-
else:
|
| 818 |
-
# Single Mode
|
| 819 |
-
st.info(f"📊 Ready to store experiment results for: **{experiment_id}**")
|
| 820 |
-
|
| 821 |
-
col1, col2 = st.columns(2)
|
| 822 |
-
|
| 823 |
-
with col1:
|
| 824 |
-
st.metric("Total Feedback", len(st.session_state.get('current_feedbacks', [])))
|
| 825 |
-
|
| 826 |
-
with col2:
|
| 827 |
-
st.metric("Metadata Records", len(st.session_state.get('current_experiment_metadata', [])))
|
| 828 |
-
|
| 829 |
-
st.markdown("---")
|
| 830 |
-
|
| 831 |
-
if st.button("💾 Store Results to Snowflake", type="primary", use_container_width=True):
|
| 832 |
-
with st.spinner("Storing results to Snowflake..."):
|
| 833 |
-
try:
|
| 834 |
-
# Create Snowflake session
|
| 835 |
-
session = create_snowflake_session()
|
| 836 |
-
db_manager = UIDatabaseManager(session)
|
| 837 |
-
|
| 838 |
-
if is_ab_mode:
|
| 839 |
-
# Store A/B Test Results
|
| 840 |
-
st.write("Storing Experiment A metadata...")
|
| 841 |
-
for meta in st.session_state.get('experiment_a_metadata', []):
|
| 842 |
-
db_manager.store_experiment_metadata(meta)
|
| 843 |
-
|
| 844 |
-
st.write("Storing Experiment B metadata...")
|
| 845 |
-
for meta in st.session_state.get('experiment_b_metadata', []):
|
| 846 |
-
db_manager.store_experiment_metadata(meta)
|
| 847 |
-
|
| 848 |
-
st.write("Storing Experiment A feedback...")
|
| 849 |
-
feedbacks_a = st.session_state.get('feedbacks_a', [])
|
| 850 |
-
if feedbacks_a:
|
| 851 |
-
db_manager.store_feedback_batch(feedbacks_a)
|
| 852 |
-
|
| 853 |
-
st.write("Storing Experiment B feedback...")
|
| 854 |
-
feedbacks_b = st.session_state.get('feedbacks_b', [])
|
| 855 |
-
if feedbacks_b:
|
| 856 |
-
db_manager.store_feedback_batch(feedbacks_b)
|
| 857 |
-
|
| 858 |
-
session.close()
|
| 859 |
-
|
| 860 |
-
st.success(f"""
|
| 861 |
-
✅ **Successfully stored A/B test results!**
|
| 862 |
-
|
| 863 |
-
- **Experiment A** ({experiment_a_id}): {len(st.session_state.get('experiment_a_metadata', []))} metadata records, {len(feedbacks_a)} feedback records
|
| 864 |
-
- **Experiment B** ({experiment_b_id}): {len(st.session_state.get('experiment_b_metadata', []))} metadata records, {len(feedbacks_b)} feedback records
|
| 865 |
-
|
| 866 |
-
Results are now available in Historical Analytics.
|
| 867 |
-
""")
|
| 868 |
-
|
| 869 |
-
else:
|
| 870 |
-
# Store Single Experiment Results
|
| 871 |
-
st.write("Storing experiment metadata...")
|
| 872 |
-
for meta in st.session_state.get('current_experiment_metadata', []):
|
| 873 |
-
db_manager.store_experiment_metadata(meta)
|
| 874 |
-
|
| 875 |
-
st.write("Storing feedback...")
|
| 876 |
-
feedbacks = st.session_state.get('current_feedbacks', [])
|
| 877 |
-
if feedbacks:
|
| 878 |
-
db_manager.store_feedback_batch(feedbacks)
|
| 879 |
-
|
| 880 |
-
session.close()
|
| 881 |
-
|
| 882 |
-
st.success(f"""
|
| 883 |
-
✅ **Successfully stored experiment results!**
|
| 884 |
-
|
| 885 |
-
- **Experiment** ({experiment_id}): {len(st.session_state.get('current_experiment_metadata', []))} metadata records, {len(feedbacks)} feedback records
|
| 886 |
-
|
| 887 |
-
Results are now available in Historical Analytics.
|
| 888 |
-
""")
|
| 889 |
-
|
| 890 |
-
except Exception as e:
|
| 891 |
-
st.error(f"❌ Error storing results to Snowflake: {e}")
|
| 892 |
-
st.error("Please check your Snowflake credentials and connection.")
|
| 893 |
-
|
| 894 |
-
st.markdown("---")
|
| 895 |
-
|
| 896 |
-
# Quick actions
|
| 897 |
-
col1, col2, col3 = st.columns(3)
|
| 898 |
-
|
| 899 |
-
with col1:
|
| 900 |
-
if st.button("🏗️ Back to Campaign Builder", use_container_width=True):
|
| 901 |
-
st.switch_page("pages/1_Campaign_Builder.py")
|
| 902 |
-
|
| 903 |
-
with col2:
|
| 904 |
-
if st.button("📊 View Analytics", use_container_width=True):
|
| 905 |
-
st.switch_page("pages/4_Analytics.py")
|
| 906 |
-
|
| 907 |
-
with col3:
|
| 908 |
-
# Export messages
|
| 909 |
-
if st.button("💾 Export Messages", use_container_width=True):
|
| 910 |
-
if is_ab_mode:
|
| 911 |
-
# In AB mode, offer to export both experiments
|
| 912 |
-
st.markdown("**Export Options:**")
|
| 913 |
-
csv_a = filtered_messages_a.to_csv(index=False, encoding='utf-8')
|
| 914 |
-
csv_b = filtered_messages_b.to_csv(index=False, encoding='utf-8')
|
| 915 |
-
|
| 916 |
-
st.download_button(
|
| 917 |
-
label="⬇️ Download Experiment A CSV",
|
| 918 |
-
data=csv_a,
|
| 919 |
-
file_name=f"{brand}_messages_experiment_a_{ab_timestamp}.csv",
|
| 920 |
-
mime="text/csv",
|
| 921 |
-
use_container_width=True
|
| 922 |
-
)
|
| 923 |
-
|
| 924 |
-
st.download_button(
|
| 925 |
-
label="⬇️ Download Experiment B CSV",
|
| 926 |
-
data=csv_b,
|
| 927 |
-
file_name=f"{brand}_messages_experiment_b_{ab_timestamp}.csv",
|
| 928 |
-
mime="text/csv",
|
| 929 |
-
use_container_width=True
|
| 930 |
-
)
|
| 931 |
-
else:
|
| 932 |
-
csv = filtered_messages.to_csv(index=False, encoding='utf-8')
|
| 933 |
-
st.download_button(
|
| 934 |
-
label="⬇️ Download CSV",
|
| 935 |
-
data=csv,
|
| 936 |
-
file_name=f"{brand}_messages_{experiment_id}.csv",
|
| 937 |
-
mime="text/csv",
|
| 938 |
-
use_container_width=True
|
| 939 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/pages/4_Analytics.py
DELETED
|
@@ -1,886 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Page 4: Analytics Dashboard
|
| 3 |
-
Visualize performance metrics and insights from generated messages and feedback.
|
| 4 |
-
Supports A/B testing mode for comparing two experiments side-by-side.
|
| 5 |
-
Works with in-memory session_state data.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import streamlit as st
|
| 9 |
-
import sys
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
import pandas as pd
|
| 12 |
-
|
| 13 |
-
# Try to import plotly with helpful error message
|
| 14 |
-
try:
|
| 15 |
-
import plotly.express as px
|
| 16 |
-
import plotly.graph_objects as go
|
| 17 |
-
except ModuleNotFoundError:
|
| 18 |
-
st.error("""
|
| 19 |
-
⚠️ **Missing Dependency: plotly**
|
| 20 |
-
|
| 21 |
-
Plotly is not installed in the current Python environment.
|
| 22 |
-
|
| 23 |
-
**To fix this:**
|
| 24 |
-
1. Make sure you're running Streamlit from the correct Python environment
|
| 25 |
-
2. Install plotly: `pip install plotly>=5.17.0`
|
| 26 |
-
3. Or install all requirements: `pip install -r visualization/requirements.txt`
|
| 27 |
-
|
| 28 |
-
**Current Python:** {}
|
| 29 |
-
""".format(sys.executable))
|
| 30 |
-
st.stop()
|
| 31 |
-
|
| 32 |
-
# Add parent directories to path
|
| 33 |
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 34 |
-
|
| 35 |
-
from utils.auth import check_authentication
|
| 36 |
-
from utils.theme import apply_theme, get_brand_emoji, get_brand_theme
|
| 37 |
-
from utils.session_feedback_manager import SessionFeedbackManager
|
| 38 |
-
|
| 39 |
-
# Page configuration
|
| 40 |
-
st.set_page_config(
|
| 41 |
-
page_title="Analytics Dashboard",
|
| 42 |
-
page_icon="📊",
|
| 43 |
-
layout="wide"
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
# Check authentication
|
| 47 |
-
if not check_authentication():
|
| 48 |
-
st.error("🔒 Please login first")
|
| 49 |
-
st.stop()
|
| 50 |
-
|
| 51 |
-
# Check if brand is selected
|
| 52 |
-
if "selected_brand" not in st.session_state or not st.session_state.selected_brand:
|
| 53 |
-
st.error("⚠️ Please select a brand from the home page first")
|
| 54 |
-
st.stop()
|
| 55 |
-
|
| 56 |
-
brand = st.session_state.selected_brand
|
| 57 |
-
apply_theme(brand)
|
| 58 |
-
theme = get_brand_theme(brand)
|
| 59 |
-
|
| 60 |
-
# Helper functions to detect AB mode and get data from session_state
|
| 61 |
-
def detect_ab_testing_mode():
|
| 62 |
-
"""
|
| 63 |
-
Detect if we're in AB testing mode based on session_state.
|
| 64 |
-
Returns True if AB mode data exists in session_state.
|
| 65 |
-
"""
|
| 66 |
-
return (
|
| 67 |
-
'ui_log_data_a' in st.session_state and
|
| 68 |
-
'ui_log_data_b' in st.session_state and
|
| 69 |
-
st.session_state.ui_log_data_a is not None and
|
| 70 |
-
st.session_state.ui_log_data_b is not None and
|
| 71 |
-
len(st.session_state.ui_log_data_a) > 0 and
|
| 72 |
-
len(st.session_state.ui_log_data_b) > 0
|
| 73 |
-
)
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def get_single_experiment_data():
|
| 77 |
-
"""Get single experiment data from session_state."""
|
| 78 |
-
if 'ui_log_data' in st.session_state and st.session_state.ui_log_data is not None:
|
| 79 |
-
if isinstance(st.session_state.ui_log_data, pd.DataFrame):
|
| 80 |
-
return st.session_state.ui_log_data
|
| 81 |
-
elif isinstance(st.session_state.ui_log_data, list):
|
| 82 |
-
return pd.DataFrame(st.session_state.ui_log_data)
|
| 83 |
-
return None
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
# Page Header
|
| 87 |
-
emoji = get_brand_emoji(brand)
|
| 88 |
-
st.title(f"📊 Analytics Dashboard - {emoji} {brand.title()}")
|
| 89 |
-
st.markdown("**Performance metrics and insights for current experiment**")
|
| 90 |
-
|
| 91 |
-
# Detect A/B testing mode
|
| 92 |
-
is_ab_mode = detect_ab_testing_mode()
|
| 93 |
-
|
| 94 |
-
if is_ab_mode:
|
| 95 |
-
st.success("🔬 **A/B Testing Mode - Comparing Experiments Side-by-Side**")
|
| 96 |
-
|
| 97 |
-
st.markdown("---")
|
| 98 |
-
|
| 99 |
-
# Load messages based on mode
|
| 100 |
-
if is_ab_mode:
|
| 101 |
-
# Load A/B test messages from session_state
|
| 102 |
-
messages_a_df = st.session_state.ui_log_data_a
|
| 103 |
-
messages_b_df = st.session_state.ui_log_data_b
|
| 104 |
-
|
| 105 |
-
# Convert to DataFrame if needed
|
| 106 |
-
if isinstance(messages_a_df, list):
|
| 107 |
-
messages_a_df = pd.DataFrame(messages_a_df)
|
| 108 |
-
if isinstance(messages_b_df, list):
|
| 109 |
-
messages_b_df = pd.DataFrame(messages_b_df)
|
| 110 |
-
|
| 111 |
-
if messages_a_df is None or len(messages_a_df) == 0 or messages_b_df is None or len(messages_b_df) == 0:
|
| 112 |
-
st.warning("⚠️ No A/B test messages found in current session. Please generate messages first in Campaign Builder.")
|
| 113 |
-
if st.button("🏗️ Go to Campaign Builder"):
|
| 114 |
-
st.switch_page("pages/1_Campaign_Builder.py")
|
| 115 |
-
st.stop()
|
| 116 |
-
|
| 117 |
-
# Get experiment IDs from session_state metadata
|
| 118 |
-
experiment_a_id = st.session_state.get('experiment_a_id', 'experiment_a')
|
| 119 |
-
experiment_b_id = st.session_state.get('experiment_b_id', 'experiment_b')
|
| 120 |
-
|
| 121 |
-
messages_df = None # Not used in AB mode
|
| 122 |
-
else:
|
| 123 |
-
# Load single experiment messages from session_state
|
| 124 |
-
messages_df = get_single_experiment_data()
|
| 125 |
-
|
| 126 |
-
if messages_df is None or len(messages_df) == 0:
|
| 127 |
-
st.warning("⚠️ No messages found in current session. Please generate messages first in Campaign Builder.")
|
| 128 |
-
if st.button("🏗️ Go to Campaign Builder"):
|
| 129 |
-
st.switch_page("pages/1_Campaign_Builder.py")
|
| 130 |
-
st.stop()
|
| 131 |
-
|
| 132 |
-
# Get experiment ID from session_state
|
| 133 |
-
experiment_id = st.session_state.get('current_experiment_id', f"{brand}_experiment")
|
| 134 |
-
|
| 135 |
-
# Sidebar - Filters
|
| 136 |
-
with st.sidebar:
|
| 137 |
-
st.header("⚙️ Settings")
|
| 138 |
-
|
| 139 |
-
# Show current experiment info
|
| 140 |
-
if is_ab_mode:
|
| 141 |
-
st.info(f"**Experiment A:** {experiment_a_id}")
|
| 142 |
-
st.info(f"**Experiment B:** {experiment_b_id}")
|
| 143 |
-
else:
|
| 144 |
-
st.info(f"**Experiment:** {experiment_id}")
|
| 145 |
-
|
| 146 |
-
st.markdown("---")
|
| 147 |
-
|
| 148 |
-
# Filters
|
| 149 |
-
st.subheader("🔍 Filters")
|
| 150 |
-
|
| 151 |
-
# Stage filter
|
| 152 |
-
if is_ab_mode:
|
| 153 |
-
# Get stages from both dataframes
|
| 154 |
-
stages_a = sorted(messages_a_df['stage'].unique()) if 'stage' in messages_a_df.columns else []
|
| 155 |
-
stages_b = sorted(messages_b_df['stage'].unique()) if 'stage' in messages_b_df.columns else []
|
| 156 |
-
available_stages = sorted(list(set(stages_a + stages_b)))
|
| 157 |
-
else:
|
| 158 |
-
available_stages = sorted(messages_df['stage'].unique()) if 'stage' in messages_df.columns else []
|
| 159 |
-
|
| 160 |
-
if available_stages:
|
| 161 |
-
selected_stages = st.multiselect(
|
| 162 |
-
"Filter by Stage",
|
| 163 |
-
available_stages,
|
| 164 |
-
default=available_stages,
|
| 165 |
-
help="Select stages to include in analysis" + (" (applies to both experiments)" if is_ab_mode else "")
|
| 166 |
-
)
|
| 167 |
-
else:
|
| 168 |
-
selected_stages = []
|
| 169 |
-
|
| 170 |
-
# Filter messages by selected stages
|
| 171 |
-
if is_ab_mode:
|
| 172 |
-
# Filter both experiments
|
| 173 |
-
filtered_messages_a = messages_a_df.copy()
|
| 174 |
-
filtered_messages_b = messages_b_df.copy()
|
| 175 |
-
|
| 176 |
-
if selected_stages:
|
| 177 |
-
filtered_messages_a = filtered_messages_a[filtered_messages_a['stage'].isin(selected_stages)]
|
| 178 |
-
filtered_messages_b = filtered_messages_b[filtered_messages_b['stage'].isin(selected_stages)]
|
| 179 |
-
|
| 180 |
-
filtered_messages = None # Not used in AB mode
|
| 181 |
-
|
| 182 |
-
# Get feedback stats for both experiments using SessionFeedbackManager
|
| 183 |
-
feedback_stats_a = SessionFeedbackManager.get_feedback_stats(
|
| 184 |
-
experiment_a_id,
|
| 185 |
-
total_messages=len(filtered_messages_a),
|
| 186 |
-
feedback_list_key="feedbacks_a"
|
| 187 |
-
)
|
| 188 |
-
feedback_stats_b = SessionFeedbackManager.get_feedback_stats(
|
| 189 |
-
experiment_b_id,
|
| 190 |
-
total_messages=len(filtered_messages_b),
|
| 191 |
-
feedback_list_key="feedbacks_b"
|
| 192 |
-
)
|
| 193 |
-
stage_feedback_stats_a = SessionFeedbackManager.get_stage_feedback_stats(
|
| 194 |
-
experiment_a_id,
|
| 195 |
-
messages_df=filtered_messages_a,
|
| 196 |
-
feedback_list_key="feedbacks_a"
|
| 197 |
-
)
|
| 198 |
-
stage_feedback_stats_b = SessionFeedbackManager.get_stage_feedback_stats(
|
| 199 |
-
experiment_b_id,
|
| 200 |
-
messages_df=filtered_messages_b,
|
| 201 |
-
feedback_list_key="feedbacks_b"
|
| 202 |
-
)
|
| 203 |
-
else:
|
| 204 |
-
if selected_stages and 'stage' in messages_df.columns:
|
| 205 |
-
filtered_messages = messages_df[messages_df['stage'].isin(selected_stages)]
|
| 206 |
-
else:
|
| 207 |
-
filtered_messages = messages_df
|
| 208 |
-
|
| 209 |
-
# Get feedback stats using SessionFeedbackManager
|
| 210 |
-
feedback_stats = SessionFeedbackManager.get_feedback_stats(
|
| 211 |
-
experiment_id,
|
| 212 |
-
total_messages=len(filtered_messages),
|
| 213 |
-
feedback_list_key="current_feedbacks"
|
| 214 |
-
)
|
| 215 |
-
stage_feedback_stats = SessionFeedbackManager.get_stage_feedback_stats(
|
| 216 |
-
experiment_id,
|
| 217 |
-
messages_df=filtered_messages,
|
| 218 |
-
feedback_list_key="current_feedbacks"
|
| 219 |
-
)
|
| 220 |
-
|
| 221 |
-
# ============================================================================
|
| 222 |
-
# OVERALL METRICS
|
| 223 |
-
# ============================================================================
|
| 224 |
-
st.header("📈 Overall Performance")
|
| 225 |
-
|
| 226 |
-
if is_ab_mode:
|
| 227 |
-
# A/B Testing Mode - Show comparison side-by-side
|
| 228 |
-
st.markdown("##### Experiment A vs Experiment B")
|
| 229 |
-
|
| 230 |
-
col1, col2 = st.columns(2)
|
| 231 |
-
|
| 232 |
-
with col1:
|
| 233 |
-
st.markdown("### 🅰️ Experiment A")
|
| 234 |
-
sub_col1, sub_col2 = st.columns(2)
|
| 235 |
-
|
| 236 |
-
with sub_col1:
|
| 237 |
-
st.metric(
|
| 238 |
-
"Total Messages",
|
| 239 |
-
len(filtered_messages_a),
|
| 240 |
-
help="Total number of generated messages"
|
| 241 |
-
)
|
| 242 |
-
st.metric(
|
| 243 |
-
"Rejected Messages",
|
| 244 |
-
feedback_stats_a['total_rejects'],
|
| 245 |
-
f"{feedback_stats_a['reject_rate']:.1f}%"
|
| 246 |
-
)
|
| 247 |
-
|
| 248 |
-
with sub_col2:
|
| 249 |
-
st.metric(
|
| 250 |
-
"Total Feedback",
|
| 251 |
-
feedback_stats_a['total_feedback'],
|
| 252 |
-
help="Number of messages with feedback"
|
| 253 |
-
)
|
| 254 |
-
approved_a = len(filtered_messages_a) - feedback_stats_a['total_rejects']
|
| 255 |
-
approval_rate_a = (approved_a / len(filtered_messages_a) * 100) if len(filtered_messages_a) > 0 else 0
|
| 256 |
-
st.metric(
|
| 257 |
-
"Approved/OK",
|
| 258 |
-
approved_a,
|
| 259 |
-
f"{approval_rate_a:.1f}%"
|
| 260 |
-
)
|
| 261 |
-
|
| 262 |
-
with col2:
|
| 263 |
-
st.markdown("### 🅱️ Experiment B")
|
| 264 |
-
sub_col1, sub_col2 = st.columns(2)
|
| 265 |
-
|
| 266 |
-
with sub_col1:
|
| 267 |
-
st.metric(
|
| 268 |
-
"Total Messages",
|
| 269 |
-
len(filtered_messages_b),
|
| 270 |
-
help="Total number of generated messages"
|
| 271 |
-
)
|
| 272 |
-
st.metric(
|
| 273 |
-
"Rejected Messages",
|
| 274 |
-
feedback_stats_b['total_rejects'],
|
| 275 |
-
f"{feedback_stats_b['reject_rate']:.1f}%"
|
| 276 |
-
)
|
| 277 |
-
|
| 278 |
-
with sub_col2:
|
| 279 |
-
st.metric(
|
| 280 |
-
"Total Feedback",
|
| 281 |
-
feedback_stats_b['total_feedback'],
|
| 282 |
-
help="Number of messages with feedback"
|
| 283 |
-
)
|
| 284 |
-
approved_b = len(filtered_messages_b) - feedback_stats_b['total_rejects']
|
| 285 |
-
approval_rate_b = (approved_b / len(filtered_messages_b) * 100) if len(filtered_messages_b) > 0 else 0
|
| 286 |
-
st.metric(
|
| 287 |
-
"Approved/OK",
|
| 288 |
-
approved_b,
|
| 289 |
-
f"{approval_rate_b:.1f}%"
|
| 290 |
-
)
|
| 291 |
-
|
| 292 |
-
# Winner determination
|
| 293 |
-
st.markdown("---")
|
| 294 |
-
st.markdown("#### 🏆 Winner Determination")
|
| 295 |
-
|
| 296 |
-
reject_rate_diff = feedback_stats_a['reject_rate'] - feedback_stats_b['reject_rate']
|
| 297 |
-
|
| 298 |
-
col1, col2, col3 = st.columns([1, 2, 1])
|
| 299 |
-
|
| 300 |
-
with col2:
|
| 301 |
-
if reject_rate_diff < -1:
|
| 302 |
-
st.success(f"**🏆 Experiment A is performing better** by {abs(reject_rate_diff):.1f}% (lower rejection rate)")
|
| 303 |
-
elif reject_rate_diff > 1:
|
| 304 |
-
st.success(f"**🏆 Experiment B is performing better** by {abs(reject_rate_diff):.1f}% (lower rejection rate)")
|
| 305 |
-
else:
|
| 306 |
-
st.info("**➡️ Both experiments have similar performance** (difference < 1%)")
|
| 307 |
-
|
| 308 |
-
else:
|
| 309 |
-
# Single Experiment Mode - Original behavior
|
| 310 |
-
col1, col2, col3, col4 = st.columns(4)
|
| 311 |
-
|
| 312 |
-
with col1:
|
| 313 |
-
st.metric(
|
| 314 |
-
"Total Messages",
|
| 315 |
-
len(filtered_messages),
|
| 316 |
-
help="Total number of generated messages"
|
| 317 |
-
)
|
| 318 |
-
|
| 319 |
-
with col2:
|
| 320 |
-
st.metric(
|
| 321 |
-
"Total Feedback",
|
| 322 |
-
feedback_stats['total_feedback'],
|
| 323 |
-
help="Number of messages with feedback"
|
| 324 |
-
)
|
| 325 |
-
|
| 326 |
-
with col3:
|
| 327 |
-
st.metric(
|
| 328 |
-
"Rejected Messages",
|
| 329 |
-
feedback_stats['total_rejects'],
|
| 330 |
-
f"{feedback_stats['reject_rate']:.1f}%"
|
| 331 |
-
)
|
| 332 |
-
|
| 333 |
-
with col4:
|
| 334 |
-
approved = len(filtered_messages) - feedback_stats['total_rejects']
|
| 335 |
-
approval_rate = (approved / len(filtered_messages) * 100) if len(filtered_messages) > 0 else 0
|
| 336 |
-
st.metric(
|
| 337 |
-
"Approved/OK Messages",
|
| 338 |
-
approved,
|
| 339 |
-
f"{approval_rate:.1f}%"
|
| 340 |
-
)
|
| 341 |
-
|
| 342 |
-
st.markdown("---")
|
| 343 |
-
|
| 344 |
-
# ============================================================================
|
| 345 |
-
# STAGE-BY-STAGE PERFORMANCE
|
| 346 |
-
# ============================================================================
|
| 347 |
-
st.header("📊 Stage-by-Stage Performance")
|
| 348 |
-
|
| 349 |
-
if is_ab_mode:
|
| 350 |
-
# A/B Testing Mode - Show both experiments on same chart for easy comparison
|
| 351 |
-
if len(stage_feedback_stats_a) > 0 or len(stage_feedback_stats_b) > 0:
|
| 352 |
-
# Create comparison chart
|
| 353 |
-
fig = go.Figure()
|
| 354 |
-
|
| 355 |
-
if len(stage_feedback_stats_a) > 0:
|
| 356 |
-
fig.add_trace(go.Bar(
|
| 357 |
-
x=stage_feedback_stats_a['stage'],
|
| 358 |
-
y=stage_feedback_stats_a['reject_rate'],
|
| 359 |
-
name='Experiment A',
|
| 360 |
-
marker_color='#4A90E2',
|
| 361 |
-
text=stage_feedback_stats_a['reject_rate'].round(1),
|
| 362 |
-
textposition='auto',
|
| 363 |
-
))
|
| 364 |
-
|
| 365 |
-
if len(stage_feedback_stats_b) > 0:
|
| 366 |
-
fig.add_trace(go.Bar(
|
| 367 |
-
x=stage_feedback_stats_b['stage'],
|
| 368 |
-
y=stage_feedback_stats_b['reject_rate'],
|
| 369 |
-
name='Experiment B',
|
| 370 |
-
marker_color='#E94B3C',
|
| 371 |
-
text=stage_feedback_stats_b['reject_rate'].round(1),
|
| 372 |
-
textposition='auto',
|
| 373 |
-
))
|
| 374 |
-
|
| 375 |
-
max_reject_rate = 10
|
| 376 |
-
if len(stage_feedback_stats_a) > 0:
|
| 377 |
-
max_reject_rate = max(max_reject_rate, stage_feedback_stats_a['reject_rate'].max())
|
| 378 |
-
if len(stage_feedback_stats_b) > 0:
|
| 379 |
-
max_reject_rate = max(max_reject_rate, stage_feedback_stats_b['reject_rate'].max())
|
| 380 |
-
|
| 381 |
-
fig.update_layout(
|
| 382 |
-
title="Rejection Rate by Stage - A vs B Comparison",
|
| 383 |
-
xaxis_title="Stage",
|
| 384 |
-
yaxis_title="Rejection Rate (%)",
|
| 385 |
-
yaxis=dict(range=[0, max_reject_rate * 1.2]),
|
| 386 |
-
template="plotly_dark",
|
| 387 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 388 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 389 |
-
barmode='group'
|
| 390 |
-
)
|
| 391 |
-
|
| 392 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 393 |
-
|
| 394 |
-
# Stage details tables side-by-side
|
| 395 |
-
with st.expander("📋 View Stage Details Comparison"):
|
| 396 |
-
col1, col2 = st.columns(2)
|
| 397 |
-
|
| 398 |
-
with col1:
|
| 399 |
-
st.markdown("**🅰️ Experiment A**")
|
| 400 |
-
if len(stage_feedback_stats_a) > 0:
|
| 401 |
-
st.dataframe(
|
| 402 |
-
stage_feedback_stats_a,
|
| 403 |
-
use_container_width=True,
|
| 404 |
-
column_config={
|
| 405 |
-
"stage": "Stage",
|
| 406 |
-
"total_messages": "Messages",
|
| 407 |
-
"total_feedback": "Feedback",
|
| 408 |
-
"rejects": "Rejected",
|
| 409 |
-
"reject_rate": st.column_config.NumberColumn(
|
| 410 |
-
"Reject Rate (%)",
|
| 411 |
-
format="%.1f%%"
|
| 412 |
-
)
|
| 413 |
-
}
|
| 414 |
-
)
|
| 415 |
-
else:
|
| 416 |
-
st.info("No feedback data for Experiment A")
|
| 417 |
-
|
| 418 |
-
with col2:
|
| 419 |
-
st.markdown("**🅱️ Experiment B**")
|
| 420 |
-
if len(stage_feedback_stats_b) > 0:
|
| 421 |
-
st.dataframe(
|
| 422 |
-
stage_feedback_stats_b,
|
| 423 |
-
use_container_width=True,
|
| 424 |
-
column_config={
|
| 425 |
-
"stage": "Stage",
|
| 426 |
-
"total_messages": "Messages",
|
| 427 |
-
"total_feedback": "Feedback",
|
| 428 |
-
"rejects": "Rejected",
|
| 429 |
-
"reject_rate": st.column_config.NumberColumn(
|
| 430 |
-
"Reject Rate (%)",
|
| 431 |
-
format="%.1f%%"
|
| 432 |
-
)
|
| 433 |
-
}
|
| 434 |
-
)
|
| 435 |
-
else:
|
| 436 |
-
st.info("No feedback data for Experiment B")
|
| 437 |
-
else:
|
| 438 |
-
st.info("ℹ️ No feedback data available yet. Provide feedback in the Message Viewer to see stage-by-stage analysis.")
|
| 439 |
-
|
| 440 |
-
else:
|
| 441 |
-
# Single Experiment Mode - Original behavior
|
| 442 |
-
if len(stage_feedback_stats) > 0:
|
| 443 |
-
# Create bar chart for stage performance
|
| 444 |
-
fig = go.Figure()
|
| 445 |
-
|
| 446 |
-
fig.add_trace(go.Bar(
|
| 447 |
-
x=stage_feedback_stats['stage'],
|
| 448 |
-
y=stage_feedback_stats['reject_rate'],
|
| 449 |
-
name='Rejection Rate (%)',
|
| 450 |
-
marker_color=theme['accent'],
|
| 451 |
-
text=stage_feedback_stats['reject_rate'].round(1),
|
| 452 |
-
textposition='auto',
|
| 453 |
-
))
|
| 454 |
-
|
| 455 |
-
fig.update_layout(
|
| 456 |
-
title="Rejection Rate by Stage",
|
| 457 |
-
xaxis_title="Stage",
|
| 458 |
-
yaxis_title="Rejection Rate (%)",
|
| 459 |
-
yaxis=dict(range=[0, max(stage_feedback_stats['reject_rate'].max() * 1.2, 10)]),
|
| 460 |
-
template="plotly_dark",
|
| 461 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 462 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 463 |
-
)
|
| 464 |
-
|
| 465 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 466 |
-
|
| 467 |
-
# Stage details table
|
| 468 |
-
with st.expander("📋 View Stage Details"):
|
| 469 |
-
st.dataframe(
|
| 470 |
-
stage_feedback_stats,
|
| 471 |
-
use_container_width=True,
|
| 472 |
-
column_config={
|
| 473 |
-
"stage": "Stage",
|
| 474 |
-
"total_messages": "Total Messages",
|
| 475 |
-
"total_feedback": "Total Feedback",
|
| 476 |
-
"rejects": "Rejected",
|
| 477 |
-
"reject_rate": st.column_config.NumberColumn(
|
| 478 |
-
"Rejection Rate (%)",
|
| 479 |
-
format="%.1f%%"
|
| 480 |
-
)
|
| 481 |
-
}
|
| 482 |
-
)
|
| 483 |
-
else:
|
| 484 |
-
st.info("ℹ️ No feedback data available yet. Provide feedback in the Message Viewer to see stage-by-stage analysis.")
|
| 485 |
-
|
| 486 |
-
st.markdown("---")
|
| 487 |
-
|
| 488 |
-
# ============================================================================
|
| 489 |
-
# REJECTION REASONS ANALYSIS
|
| 490 |
-
# ============================================================================
|
| 491 |
-
st.header("🔍 Rejection Reasons Analysis")
|
| 492 |
-
|
| 493 |
-
if is_ab_mode:
|
| 494 |
-
# A/B Testing Mode - Show side-by-side pie charts for both experiments
|
| 495 |
-
has_rejection_a = len(feedback_stats_a['rejection_reasons']) > 0
|
| 496 |
-
has_rejection_b = len(feedback_stats_b['rejection_reasons']) > 0
|
| 497 |
-
|
| 498 |
-
if has_rejection_a or has_rejection_b:
|
| 499 |
-
col1, col2 = st.columns(2)
|
| 500 |
-
|
| 501 |
-
with col1:
|
| 502 |
-
st.markdown("### 🅰️ Experiment A")
|
| 503 |
-
if has_rejection_a:
|
| 504 |
-
rejection_reasons_a_df = pd.DataFrame([
|
| 505 |
-
{"Reason": reason, "Count": count}
|
| 506 |
-
for reason, count in feedback_stats_a['rejection_reasons'].items()
|
| 507 |
-
]).sort_values('Count', ascending=False)
|
| 508 |
-
|
| 509 |
-
# Pie chart for rejection reasons
|
| 510 |
-
fig_a = px.pie(
|
| 511 |
-
rejection_reasons_a_df,
|
| 512 |
-
names='Reason',
|
| 513 |
-
values='Count',
|
| 514 |
-
title="Distribution of Rejection Reasons",
|
| 515 |
-
color_discrete_sequence=px.colors.qualitative.Set3
|
| 516 |
-
)
|
| 517 |
-
|
| 518 |
-
fig_a.update_layout(
|
| 519 |
-
template="plotly_dark",
|
| 520 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 521 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 522 |
-
)
|
| 523 |
-
|
| 524 |
-
st.plotly_chart(fig_a, use_container_width=True)
|
| 525 |
-
|
| 526 |
-
# Most common issue
|
| 527 |
-
most_common_reason_a = rejection_reasons_a_df.iloc[0]
|
| 528 |
-
st.info(f"💡 **Most Common:** {most_common_reason_a['Reason']} ({most_common_reason_a['Count']})")
|
| 529 |
-
else:
|
| 530 |
-
st.info("No rejection feedback for Experiment A")
|
| 531 |
-
|
| 532 |
-
with col2:
|
| 533 |
-
st.markdown("### 🅱️ Experiment B")
|
| 534 |
-
if has_rejection_b:
|
| 535 |
-
rejection_reasons_b_df = pd.DataFrame([
|
| 536 |
-
{"Reason": reason, "Count": count}
|
| 537 |
-
for reason, count in feedback_stats_b['rejection_reasons'].items()
|
| 538 |
-
]).sort_values('Count', ascending=False)
|
| 539 |
-
|
| 540 |
-
# Pie chart for rejection reasons
|
| 541 |
-
fig_b = px.pie(
|
| 542 |
-
rejection_reasons_b_df,
|
| 543 |
-
names='Reason',
|
| 544 |
-
values='Count',
|
| 545 |
-
title="Distribution of Rejection Reasons",
|
| 546 |
-
color_discrete_sequence=px.colors.qualitative.Pastel
|
| 547 |
-
)
|
| 548 |
-
|
| 549 |
-
fig_b.update_layout(
|
| 550 |
-
template="plotly_dark",
|
| 551 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 552 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 553 |
-
)
|
| 554 |
-
|
| 555 |
-
st.plotly_chart(fig_b, use_container_width=True)
|
| 556 |
-
|
| 557 |
-
# Most common issue
|
| 558 |
-
most_common_reason_b = rejection_reasons_b_df.iloc[0]
|
| 559 |
-
st.info(f"💡 **Most Common:** {most_common_reason_b['Reason']} ({most_common_reason_b['Count']})")
|
| 560 |
-
else:
|
| 561 |
-
st.info("No rejection feedback for Experiment B")
|
| 562 |
-
|
| 563 |
-
# Detailed rejection reasons tables side-by-side
|
| 564 |
-
with st.expander("📋 View Detailed Rejection Feedback"):
|
| 565 |
-
col1, col2 = st.columns(2)
|
| 566 |
-
|
| 567 |
-
with col1:
|
| 568 |
-
st.markdown("**🅰️ Experiment A**")
|
| 569 |
-
# Load feedback from session_state
|
| 570 |
-
feedbacks_a = st.session_state.get('feedbacks_a', [])
|
| 571 |
-
feedback_a_list = [fb for fb in feedbacks_a if fb['experiment_id'] == experiment_a_id]
|
| 572 |
-
|
| 573 |
-
if len(feedback_a_list) > 0:
|
| 574 |
-
feedback_a_df = pd.DataFrame(feedback_a_list)
|
| 575 |
-
reject_a_df = feedback_a_df[feedback_a_df['feedback_type'] == 'reject']
|
| 576 |
-
|
| 577 |
-
if len(reject_a_df) > 0:
|
| 578 |
-
display_a_df = reject_a_df[[
|
| 579 |
-
'user_id', 'stage', 'rejection_reason', 'rejection_text', 'timestamp'
|
| 580 |
-
]].copy()
|
| 581 |
-
|
| 582 |
-
display_a_df['rejection_reason'] = display_a_df['rejection_reason'].apply(
|
| 583 |
-
lambda x: SessionFeedbackManager.get_rejection_reason_label(x) if pd.notna(x) else 'N/A'
|
| 584 |
-
)
|
| 585 |
-
|
| 586 |
-
st.dataframe(display_a_df, use_container_width=True)
|
| 587 |
-
else:
|
| 588 |
-
st.info("No rejection feedback")
|
| 589 |
-
else:
|
| 590 |
-
st.info("No feedback data")
|
| 591 |
-
|
| 592 |
-
with col2:
|
| 593 |
-
st.markdown("**🅱️ Experiment B**")
|
| 594 |
-
# Load feedback from session_state
|
| 595 |
-
feedbacks_b = st.session_state.get('feedbacks_b', [])
|
| 596 |
-
feedback_b_list = [fb for fb in feedbacks_b if fb['experiment_id'] == experiment_b_id]
|
| 597 |
-
|
| 598 |
-
if len(feedback_b_list) > 0:
|
| 599 |
-
feedback_b_df = pd.DataFrame(feedback_b_list)
|
| 600 |
-
reject_b_df = feedback_b_df[feedback_b_df['feedback_type'] == 'reject']
|
| 601 |
-
|
| 602 |
-
if len(reject_b_df) > 0:
|
| 603 |
-
display_b_df = reject_b_df[[
|
| 604 |
-
'user_id', 'stage', 'rejection_reason', 'rejection_text', 'timestamp'
|
| 605 |
-
]].copy()
|
| 606 |
-
|
| 607 |
-
display_b_df['rejection_reason'] = display_b_df['rejection_reason'].apply(
|
| 608 |
-
lambda x: SessionFeedbackManager.get_rejection_reason_label(x) if pd.notna(x) else 'N/A'
|
| 609 |
-
)
|
| 610 |
-
|
| 611 |
-
st.dataframe(display_b_df, use_container_width=True)
|
| 612 |
-
else:
|
| 613 |
-
st.info("No rejection feedback")
|
| 614 |
-
else:
|
| 615 |
-
st.info("No feedback data")
|
| 616 |
-
else:
|
| 617 |
-
st.info("ℹ️ No rejection feedback available yet. Reject messages in the Message Viewer to see detailed analysis.")
|
| 618 |
-
|
| 619 |
-
else:
|
| 620 |
-
# Single Experiment Mode - Original behavior
|
| 621 |
-
if len(feedback_stats['rejection_reasons']) > 0:
|
| 622 |
-
# Create DataFrame for rejection reasons
|
| 623 |
-
rejection_reasons_df = pd.DataFrame([
|
| 624 |
-
{"Reason": reason, "Count": count}
|
| 625 |
-
for reason, count in feedback_stats['rejection_reasons'].items()
|
| 626 |
-
]).sort_values('Count', ascending=False)
|
| 627 |
-
|
| 628 |
-
col1, col2 = st.columns([1, 1])
|
| 629 |
-
|
| 630 |
-
with col1:
|
| 631 |
-
# Pie chart for rejection reasons
|
| 632 |
-
fig = px.pie(
|
| 633 |
-
rejection_reasons_df,
|
| 634 |
-
names='Reason',
|
| 635 |
-
values='Count',
|
| 636 |
-
title="Distribution of Rejection Reasons",
|
| 637 |
-
color_discrete_sequence=px.colors.qualitative.Set3
|
| 638 |
-
)
|
| 639 |
-
|
| 640 |
-
fig.update_layout(
|
| 641 |
-
template="plotly_dark",
|
| 642 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 643 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 644 |
-
)
|
| 645 |
-
|
| 646 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 647 |
-
|
| 648 |
-
with col2:
|
| 649 |
-
# Bar chart for rejection reasons
|
| 650 |
-
fig = px.bar(
|
| 651 |
-
rejection_reasons_df,
|
| 652 |
-
x='Reason',
|
| 653 |
-
y='Count',
|
| 654 |
-
title="Rejection Reasons Count",
|
| 655 |
-
color='Count',
|
| 656 |
-
color_continuous_scale='Reds'
|
| 657 |
-
)
|
| 658 |
-
|
| 659 |
-
fig.update_layout(
|
| 660 |
-
template="plotly_dark",
|
| 661 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 662 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 663 |
-
xaxis_tickangle=-45
|
| 664 |
-
)
|
| 665 |
-
|
| 666 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 667 |
-
|
| 668 |
-
# Most common issue
|
| 669 |
-
most_common_reason = rejection_reasons_df.iloc[0]
|
| 670 |
-
st.info(f"💡 **Most Common Issue:** {most_common_reason['Reason']} ({most_common_reason['Count']} occurrences)")
|
| 671 |
-
|
| 672 |
-
# Detailed rejection reasons table
|
| 673 |
-
with st.expander("📋 View Detailed Rejection Feedback"):
|
| 674 |
-
# Load feedback from session_state
|
| 675 |
-
current_feedbacks = st.session_state.get('current_feedbacks', [])
|
| 676 |
-
feedback_list = [fb for fb in current_feedbacks if fb['experiment_id'] == experiment_id]
|
| 677 |
-
|
| 678 |
-
if len(feedback_list) > 0:
|
| 679 |
-
feedback_df = pd.DataFrame(feedback_list)
|
| 680 |
-
reject_df = feedback_df[feedback_df['feedback_type'] == 'reject']
|
| 681 |
-
|
| 682 |
-
if len(reject_df) > 0:
|
| 683 |
-
# Prepare display dataframe
|
| 684 |
-
display_df = reject_df[[
|
| 685 |
-
'user_id', 'stage', 'rejection_reason', 'rejection_text', 'timestamp'
|
| 686 |
-
]].copy()
|
| 687 |
-
|
| 688 |
-
# Map rejection reason keys to labels
|
| 689 |
-
display_df['rejection_reason'] = display_df['rejection_reason'].apply(
|
| 690 |
-
lambda x: SessionFeedbackManager.get_rejection_reason_label(x) if pd.notna(x) else 'N/A'
|
| 691 |
-
)
|
| 692 |
-
|
| 693 |
-
st.dataframe(display_df, use_container_width=True)
|
| 694 |
-
else:
|
| 695 |
-
st.info("ℹ️ No rejection feedback available yet. Reject messages in the Message Viewer to see detailed analysis.")
|
| 696 |
-
|
| 697 |
-
st.markdown("---")
|
| 698 |
-
|
| 699 |
-
# ============================================================================
|
| 700 |
-
# STATISTICAL COMPARISON (AB Testing Mode Only)
|
| 701 |
-
# ============================================================================
|
| 702 |
-
if is_ab_mode:
|
| 703 |
-
st.header("📊 Statistical Comparison")
|
| 704 |
-
|
| 705 |
-
# Create comparison using feedback stats we already have
|
| 706 |
-
col1, col2, col3 = st.columns(3)
|
| 707 |
-
|
| 708 |
-
with col1:
|
| 709 |
-
st.markdown("### 🅰️ Experiment A")
|
| 710 |
-
st.metric(
|
| 711 |
-
"Rejection Rate",
|
| 712 |
-
f"{feedback_stats_a['reject_rate']:.1f}%"
|
| 713 |
-
)
|
| 714 |
-
st.metric(
|
| 715 |
-
"Total Feedback",
|
| 716 |
-
feedback_stats_a['total_feedback']
|
| 717 |
-
)
|
| 718 |
-
st.metric(
|
| 719 |
-
"Total Messages",
|
| 720 |
-
len(filtered_messages_a)
|
| 721 |
-
)
|
| 722 |
-
|
| 723 |
-
with col2:
|
| 724 |
-
st.markdown("### 🅱️ Experiment B")
|
| 725 |
-
st.metric(
|
| 726 |
-
"Rejection Rate",
|
| 727 |
-
f"{feedback_stats_b['reject_rate']:.1f}%"
|
| 728 |
-
)
|
| 729 |
-
st.metric(
|
| 730 |
-
"Total Feedback",
|
| 731 |
-
feedback_stats_b['total_feedback']
|
| 732 |
-
)
|
| 733 |
-
st.metric(
|
| 734 |
-
"Total Messages",
|
| 735 |
-
len(filtered_messages_b)
|
| 736 |
-
)
|
| 737 |
-
|
| 738 |
-
with col3:
|
| 739 |
-
st.markdown("### 📊 Difference")
|
| 740 |
-
diff = feedback_stats_a['reject_rate'] - feedback_stats_b['reject_rate']
|
| 741 |
-
|
| 742 |
-
st.metric(
|
| 743 |
-
"Rejection Rate Δ",
|
| 744 |
-
f"{diff:.1f}%",
|
| 745 |
-
delta=f"{-diff:.1f}%" if diff != 0 else "0%",
|
| 746 |
-
delta_color="inverse"
|
| 747 |
-
)
|
| 748 |
-
|
| 749 |
-
feedback_diff = feedback_stats_b['total_feedback'] - feedback_stats_a['total_feedback']
|
| 750 |
-
st.metric(
|
| 751 |
-
"Feedback Δ",
|
| 752 |
-
feedback_diff,
|
| 753 |
-
delta=f"{feedback_diff}" if feedback_diff != 0 else "0"
|
| 754 |
-
)
|
| 755 |
-
|
| 756 |
-
st.markdown("---")
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
# Export options
|
| 760 |
-
st.header("💾 Export Data")
|
| 761 |
-
|
| 762 |
-
if is_ab_mode:
|
| 763 |
-
# A/B Testing Mode - Export both experiments
|
| 764 |
-
st.markdown("##### Export Experiment Data")
|
| 765 |
-
|
| 766 |
-
col1, col2 = st.columns(2)
|
| 767 |
-
|
| 768 |
-
with col1:
|
| 769 |
-
st.markdown("**🅰️ Experiment A**")
|
| 770 |
-
|
| 771 |
-
if st.button("📥 Export Messages A", use_container_width=True):
|
| 772 |
-
csv = filtered_messages_a.to_csv(index=False, encoding='utf-8')
|
| 773 |
-
st.download_button(
|
| 774 |
-
label="⬇️ Download Messages A CSV",
|
| 775 |
-
data=csv,
|
| 776 |
-
file_name=f"{brand}_{experiment_a_id}_messages.csv",
|
| 777 |
-
mime="text/csv",
|
| 778 |
-
use_container_width=True
|
| 779 |
-
)
|
| 780 |
-
|
| 781 |
-
feedbacks_a = st.session_state.get('feedbacks_a', [])
|
| 782 |
-
feedback_a_list = [fb for fb in feedbacks_a if fb['experiment_id'] == experiment_a_id]
|
| 783 |
-
if len(feedback_a_list) > 0:
|
| 784 |
-
if st.button("📥 Export Feedback A", use_container_width=True):
|
| 785 |
-
feedback_a_df = pd.DataFrame(feedback_a_list)
|
| 786 |
-
csv = feedback_a_df.to_csv(index=False, encoding='utf-8')
|
| 787 |
-
st.download_button(
|
| 788 |
-
label="⬇️ Download Feedback A CSV",
|
| 789 |
-
data=csv,
|
| 790 |
-
file_name=f"{brand}_{experiment_a_id}_feedback.csv",
|
| 791 |
-
mime="text/csv",
|
| 792 |
-
use_container_width=True
|
| 793 |
-
)
|
| 794 |
-
|
| 795 |
-
if len(stage_feedback_stats_a) > 0:
|
| 796 |
-
if st.button("📥 Export Analytics A", use_container_width=True):
|
| 797 |
-
csv = stage_feedback_stats_a.to_csv(index=False, encoding='utf-8')
|
| 798 |
-
st.download_button(
|
| 799 |
-
label="⬇️ Download Analytics A CSV",
|
| 800 |
-
data=csv,
|
| 801 |
-
file_name=f"{brand}_{experiment_a_id}_analytics.csv",
|
| 802 |
-
mime="text/csv",
|
| 803 |
-
use_container_width=True
|
| 804 |
-
)
|
| 805 |
-
|
| 806 |
-
with col2:
|
| 807 |
-
st.markdown("**🅱️ Experiment B**")
|
| 808 |
-
|
| 809 |
-
if st.button("📥 Export Messages B", use_container_width=True):
|
| 810 |
-
csv = filtered_messages_b.to_csv(index=False, encoding='utf-8')
|
| 811 |
-
st.download_button(
|
| 812 |
-
label="⬇️ Download Messages B CSV",
|
| 813 |
-
data=csv,
|
| 814 |
-
file_name=f"{brand}_{experiment_b_id}_messages.csv",
|
| 815 |
-
mime="text/csv",
|
| 816 |
-
use_container_width=True
|
| 817 |
-
)
|
| 818 |
-
|
| 819 |
-
feedbacks_b = st.session_state.get('feedbacks_b', [])
|
| 820 |
-
feedback_b_list = [fb for fb in feedbacks_b if fb['experiment_id'] == experiment_b_id]
|
| 821 |
-
if len(feedback_b_list) > 0:
|
| 822 |
-
if st.button("📥 Export Feedback B", use_container_width=True):
|
| 823 |
-
feedback_b_df = pd.DataFrame(feedback_b_list)
|
| 824 |
-
csv = feedback_b_df.to_csv(index=False, encoding='utf-8')
|
| 825 |
-
st.download_button(
|
| 826 |
-
label="⬇️ Download Feedback B CSV",
|
| 827 |
-
data=csv,
|
| 828 |
-
file_name=f"{brand}_{experiment_b_id}_feedback.csv",
|
| 829 |
-
mime="text/csv",
|
| 830 |
-
use_container_width=True
|
| 831 |
-
)
|
| 832 |
-
|
| 833 |
-
if len(stage_feedback_stats_b) > 0:
|
| 834 |
-
if st.button("📥 Export Analytics B", use_container_width=True):
|
| 835 |
-
csv = stage_feedback_stats_b.to_csv(index=False, encoding='utf-8')
|
| 836 |
-
st.download_button(
|
| 837 |
-
label="⬇️ Download Analytics B CSV",
|
| 838 |
-
data=csv,
|
| 839 |
-
file_name=f"{brand}_{experiment_b_id}_analytics.csv",
|
| 840 |
-
mime="text/csv",
|
| 841 |
-
use_container_width=True
|
| 842 |
-
)
|
| 843 |
-
|
| 844 |
-
else:
|
| 845 |
-
# Single Experiment Mode - Original behavior
|
| 846 |
-
col1, col2, col3 = st.columns(3)
|
| 847 |
-
|
| 848 |
-
with col1:
|
| 849 |
-
if st.button("📥 Export Messages", use_container_width=True):
|
| 850 |
-
csv = filtered_messages.to_csv(index=False, encoding='utf-8')
|
| 851 |
-
st.download_button(
|
| 852 |
-
label="⬇️ Download Messages CSV",
|
| 853 |
-
data=csv,
|
| 854 |
-
file_name=f"{brand}_{experiment_id}_messages.csv",
|
| 855 |
-
mime="text/csv",
|
| 856 |
-
use_container_width=True
|
| 857 |
-
)
|
| 858 |
-
|
| 859 |
-
with col2:
|
| 860 |
-
current_feedbacks = st.session_state.get('current_feedbacks', [])
|
| 861 |
-
feedback_list = [fb for fb in current_feedbacks if fb['experiment_id'] == experiment_id]
|
| 862 |
-
if len(feedback_list) > 0:
|
| 863 |
-
if st.button("📥 Export Feedback", use_container_width=True):
|
| 864 |
-
feedback_df = pd.DataFrame(feedback_list)
|
| 865 |
-
csv = feedback_df.to_csv(index=False, encoding='utf-8')
|
| 866 |
-
st.download_button(
|
| 867 |
-
label="⬇️ Download Feedback CSV",
|
| 868 |
-
data=csv,
|
| 869 |
-
file_name=f"{brand}_{experiment_id}_feedback.csv",
|
| 870 |
-
mime="text/csv",
|
| 871 |
-
use_container_width=True
|
| 872 |
-
)
|
| 873 |
-
|
| 874 |
-
with col3:
|
| 875 |
-
if len(stage_feedback_stats) > 0:
|
| 876 |
-
if st.button("📥 Export Analytics", use_container_width=True):
|
| 877 |
-
csv = stage_feedback_stats.to_csv(index=False, encoding='utf-8')
|
| 878 |
-
st.download_button(
|
| 879 |
-
label="⬇️ Download Analytics CSV",
|
| 880 |
-
data=csv,
|
| 881 |
-
file_name=f"{brand}_{experiment_id}_analytics.csv",
|
| 882 |
-
mime="text/csv",
|
| 883 |
-
use_container_width=True
|
| 884 |
-
)
|
| 885 |
-
|
| 886 |
-
st.markdown("---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/pages/5_Historical_Analytics.py
DELETED
|
@@ -1,1043 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Page 5: Historical Analytics (Enhanced)
|
| 3 |
-
View comprehensive performance metrics and insights from all previous experiments stored in Snowflake.
|
| 4 |
-
Includes campaign-level analysis, model performance, and detailed rejection analytics.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import streamlit as st
|
| 8 |
-
import sys
|
| 9 |
-
import os
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
import pandas as pd
|
| 12 |
-
from datetime import datetime, timedelta
|
| 13 |
-
from dotenv import load_dotenv
|
| 14 |
-
|
| 15 |
-
# Load environment variables
|
| 16 |
-
env_path = Path(__file__).parent.parent.parent / '.env'
|
| 17 |
-
if env_path.exists():
|
| 18 |
-
load_dotenv(env_path)
|
| 19 |
-
else:
|
| 20 |
-
# Try parent directory
|
| 21 |
-
parent_env_path = Path(__file__).parent.parent.parent.parent / '.env'
|
| 22 |
-
if parent_env_path.exists():
|
| 23 |
-
load_dotenv(parent_env_path)
|
| 24 |
-
|
| 25 |
-
# Try to import plotly with helpful error message
|
| 26 |
-
try:
|
| 27 |
-
import plotly.express as px
|
| 28 |
-
import plotly.graph_objects as go
|
| 29 |
-
from plotly.subplots import make_subplots
|
| 30 |
-
except ModuleNotFoundError:
|
| 31 |
-
st.error("""
|
| 32 |
-
⚠️ **Missing Dependency: plotly**
|
| 33 |
-
|
| 34 |
-
Plotly is not installed in the current Python environment.
|
| 35 |
-
|
| 36 |
-
**To fix this:**
|
| 37 |
-
1. Make sure you're running Streamlit from the correct Python environment
|
| 38 |
-
2. Install plotly: `pip install plotly>=5.17.0`
|
| 39 |
-
3. Or install all requirements: `pip install -r visualization/requirements.txt`
|
| 40 |
-
|
| 41 |
-
**Current Python:** {}
|
| 42 |
-
""".format(sys.executable))
|
| 43 |
-
st.stop()
|
| 44 |
-
|
| 45 |
-
# Add parent directories to path
|
| 46 |
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 47 |
-
|
| 48 |
-
from utils.auth import check_authentication
|
| 49 |
-
from utils.theme import apply_theme, get_brand_emoji, get_brand_theme
|
| 50 |
-
from utils.db_manager import UIDatabaseManager
|
| 51 |
-
|
| 52 |
-
# Helper function to create Snowflake session
|
| 53 |
-
def create_snowflake_session():
|
| 54 |
-
"""Create a Snowflake session using environment variables."""
|
| 55 |
-
try:
|
| 56 |
-
from snowflake.snowpark import Session
|
| 57 |
-
connection_parameters = {
|
| 58 |
-
"user": os.getenv("SNOWFLAKE_USER"),
|
| 59 |
-
"password": os.getenv("SNOWFLAKE_PASSWORD"),
|
| 60 |
-
"account": os.getenv("SNOWFLAKE_ACCOUNT"),
|
| 61 |
-
"role": os.getenv("SNOWFLAKE_ROLE"),
|
| 62 |
-
"database": os.getenv("SNOWFLAKE_DATABASE"),
|
| 63 |
-
"warehouse": os.getenv("SNOWFLAKE_WAREHOUSE"),
|
| 64 |
-
"schema": os.getenv("SNOWFLAKE_SCHEMA"),
|
| 65 |
-
}
|
| 66 |
-
return Session.builder.configs(connection_parameters).create()
|
| 67 |
-
except Exception as e:
|
| 68 |
-
st.error(f"Failed to create Snowflake session: {e}")
|
| 69 |
-
import traceback
|
| 70 |
-
st.error(f"Traceback: {traceback.format_exc()}")
|
| 71 |
-
return None
|
| 72 |
-
|
| 73 |
-
# Rejection reason mapping
|
| 74 |
-
REJECTION_REASON_LABELS = {
|
| 75 |
-
"poor_header": "Poor Header",
|
| 76 |
-
"poor_body": "Poor Body/Content",
|
| 77 |
-
"grammar_issues": "Grammar Issues",
|
| 78 |
-
"emoji_problems": "Emoji Problems",
|
| 79 |
-
"recommendation_issues": "Recommendation Issues",
|
| 80 |
-
"wrong_information": "Wrong/Inaccurate Information",
|
| 81 |
-
"tone_issues": "Tone Issues",
|
| 82 |
-
"similarity": "Similar To Previous",
|
| 83 |
-
"other": "Other"
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
def get_rejection_label(reason_key):
|
| 87 |
-
"""Get human-readable label for rejection reason."""
|
| 88 |
-
if reason_key is None:
|
| 89 |
-
return "Unknown"
|
| 90 |
-
return REJECTION_REASON_LABELS.get(reason_key, reason_key)
|
| 91 |
-
|
| 92 |
-
# Page configuration
|
| 93 |
-
st.set_page_config(
|
| 94 |
-
page_title="Historical Analytics",
|
| 95 |
-
page_icon="📚",
|
| 96 |
-
layout="wide"
|
| 97 |
-
)
|
| 98 |
-
|
| 99 |
-
# Check authentication
|
| 100 |
-
if not check_authentication():
|
| 101 |
-
st.error("🔒 Please login first")
|
| 102 |
-
st.stop()
|
| 103 |
-
|
| 104 |
-
# Check if brand is selected
|
| 105 |
-
if "selected_brand" not in st.session_state or not st.session_state.selected_brand:
|
| 106 |
-
st.error("⚠️ Please select a brand from the home page first")
|
| 107 |
-
st.stop()
|
| 108 |
-
|
| 109 |
-
brand = st.session_state.selected_brand
|
| 110 |
-
apply_theme(brand)
|
| 111 |
-
theme = get_brand_theme(brand)
|
| 112 |
-
|
| 113 |
-
# Page Header
|
| 114 |
-
emoji = get_brand_emoji(brand)
|
| 115 |
-
st.title(f"📚 Enhanced Historical Analytics - {emoji} {brand.title()}")
|
| 116 |
-
st.markdown("**Comprehensive insights across campaigns, models, stages, and rejection patterns**")
|
| 117 |
-
|
| 118 |
-
st.markdown("---")
|
| 119 |
-
|
| 120 |
-
# ============================================================================
|
| 121 |
-
# LOAD DATA SECTION
|
| 122 |
-
# ============================================================================
|
| 123 |
-
col1, col2, col3 = st.columns([1, 2, 1])
|
| 124 |
-
with col2:
|
| 125 |
-
if st.button("📊 Load Historical Data from Snowflake", use_container_width=True, type="primary"):
|
| 126 |
-
with st.spinner("Loading historical data from Snowflake..."):
|
| 127 |
-
# Create Snowflake session
|
| 128 |
-
session = create_snowflake_session()
|
| 129 |
-
|
| 130 |
-
if session:
|
| 131 |
-
try:
|
| 132 |
-
db_manager = UIDatabaseManager(session)
|
| 133 |
-
|
| 134 |
-
# Load campaigns summary (new enhanced view)
|
| 135 |
-
campaigns_df = db_manager.get_all_campaigns_summary(brand=brand)
|
| 136 |
-
|
| 137 |
-
# Load experiment summary (original view for backward compatibility)
|
| 138 |
-
experiments_df = db_manager.get_experiment_summary(brand=brand)
|
| 139 |
-
|
| 140 |
-
# Store in session_state
|
| 141 |
-
st.session_state['historical_campaigns'] = campaigns_df
|
| 142 |
-
st.session_state['historical_experiments'] = experiments_df
|
| 143 |
-
st.session_state['historical_data_loaded'] = True
|
| 144 |
-
st.session_state['selected_campaign'] = None # Reset selection
|
| 145 |
-
|
| 146 |
-
# Close session
|
| 147 |
-
db_manager.close()
|
| 148 |
-
|
| 149 |
-
st.success(f"✅ Loaded {len(campaigns_df)} campaigns and {len(experiments_df)} experiments from Snowflake!")
|
| 150 |
-
st.rerun()
|
| 151 |
-
|
| 152 |
-
except Exception as e:
|
| 153 |
-
st.error(f"Error loading historical data: {e}")
|
| 154 |
-
import traceback
|
| 155 |
-
st.error(f"Traceback: {traceback.format_exc()}")
|
| 156 |
-
if session:
|
| 157 |
-
session.close()
|
| 158 |
-
else:
|
| 159 |
-
st.error("Failed to connect to Snowflake. Please check your credentials.")
|
| 160 |
-
|
| 161 |
-
st.markdown("---")
|
| 162 |
-
|
| 163 |
-
# Check if data is loaded
|
| 164 |
-
if not st.session_state.get('historical_data_loaded', False):
|
| 165 |
-
st.info("👆 Click the button above to load historical data from Snowflake")
|
| 166 |
-
st.markdown("""
|
| 167 |
-
### What's in Historical Analytics:
|
| 168 |
-
- **Campaign-Level Analysis**: View aggregated metrics for all experiments in a campaign
|
| 169 |
-
- **Model Performance**: Compare LLM models with detailed rejection metrics
|
| 170 |
-
- **Stage Analysis**: Identify which stages have the highest rejection rates
|
| 171 |
-
- **Rejection Reasons**: Interactive breakdown of why messages get rejected
|
| 172 |
-
- **Filtering**: Drill down by campaign, stage, and model
|
| 173 |
-
""")
|
| 174 |
-
st.stop()
|
| 175 |
-
|
| 176 |
-
# Get loaded data
|
| 177 |
-
campaigns_df = st.session_state.get('historical_campaigns', pd.DataFrame())
|
| 178 |
-
experiments_df = st.session_state.get('historical_experiments', pd.DataFrame())
|
| 179 |
-
|
| 180 |
-
if campaigns_df is None or len(campaigns_df) == 0:
|
| 181 |
-
st.warning("⚠️ No historical campaigns found in Snowflake. Generate and store experiments first using Campaign Builder.")
|
| 182 |
-
st.stop()
|
| 183 |
-
|
| 184 |
-
# ============================================================================
|
| 185 |
-
# SIDEBAR - CAMPAIGN SELECTOR
|
| 186 |
-
# ============================================================================
|
| 187 |
-
with st.sidebar:
|
| 188 |
-
st.header("🎯 Campaign Selector")
|
| 189 |
-
|
| 190 |
-
# Campaign selection
|
| 191 |
-
campaign_options = ["-- All Campaigns Overview --"] + list(campaigns_df['campaign_name'].unique())
|
| 192 |
-
selected_campaign = st.selectbox(
|
| 193 |
-
"Select Campaign",
|
| 194 |
-
campaign_options,
|
| 195 |
-
index=0,
|
| 196 |
-
key='campaign_selector'
|
| 197 |
-
)
|
| 198 |
-
|
| 199 |
-
# Store selected campaign
|
| 200 |
-
if selected_campaign != "-- All Campaigns Overview --":
|
| 201 |
-
st.session_state['selected_campaign'] = selected_campaign
|
| 202 |
-
else:
|
| 203 |
-
st.session_state['selected_campaign'] = None
|
| 204 |
-
|
| 205 |
-
st.markdown("---")
|
| 206 |
-
|
| 207 |
-
# Refresh button
|
| 208 |
-
if st.button("🔄 Reload Data", use_container_width=True):
|
| 209 |
-
st.session_state['historical_data_loaded'] = False
|
| 210 |
-
st.session_state['selected_campaign'] = None
|
| 211 |
-
st.rerun()
|
| 212 |
-
|
| 213 |
-
st.markdown("---")
|
| 214 |
-
st.markdown("**💡 Tip:** Select a campaign to see detailed analytics including model performance and rejection patterns.")
|
| 215 |
-
|
| 216 |
-
# ============================================================================
|
| 217 |
-
# MAIN CONTENT - CONDITIONAL BASED ON SELECTION
|
| 218 |
-
# ============================================================================
|
| 219 |
-
|
| 220 |
-
if st.session_state.get('selected_campaign') is None:
|
| 221 |
-
# ========================================================================
|
| 222 |
-
# ALL CAMPAIGNS OVERVIEW
|
| 223 |
-
# ========================================================================
|
| 224 |
-
st.header("🌐 All Campaigns Overview")
|
| 225 |
-
|
| 226 |
-
# Overview metrics
|
| 227 |
-
col1, col2, col3, col4 = st.columns(4)
|
| 228 |
-
|
| 229 |
-
with col1:
|
| 230 |
-
st.metric("Total Campaigns", len(campaigns_df))
|
| 231 |
-
|
| 232 |
-
with col2:
|
| 233 |
-
total_experiments = campaigns_df['total_experiments'].sum() if 'total_experiments' in campaigns_df.columns else 0
|
| 234 |
-
st.metric("Total Experiments", f"{int(total_experiments):,}")
|
| 235 |
-
|
| 236 |
-
with col3:
|
| 237 |
-
total_messages = campaigns_df['total_messages'].sum() if 'total_messages' in campaigns_df.columns else 0
|
| 238 |
-
st.metric("Total Messages", f"{int(total_messages):,}")
|
| 239 |
-
|
| 240 |
-
with col4:
|
| 241 |
-
total_rejections = campaigns_df['total_rejections'].sum() if 'total_rejections' in campaigns_df.columns else 0
|
| 242 |
-
st.metric("Total Rejections", f"{int(total_rejections):,}")
|
| 243 |
-
|
| 244 |
-
st.markdown("---")
|
| 245 |
-
|
| 246 |
-
# Campaigns summary table
|
| 247 |
-
st.subheader("📊 Campaigns Summary")
|
| 248 |
-
|
| 249 |
-
display_df = campaigns_df.copy()
|
| 250 |
-
|
| 251 |
-
# Format dates
|
| 252 |
-
if 'first_experiment' in display_df.columns:
|
| 253 |
-
display_df['first_experiment'] = pd.to_datetime(display_df['first_experiment']).dt.strftime('%Y-%m-%d')
|
| 254 |
-
if 'last_experiment' in display_df.columns:
|
| 255 |
-
display_df['last_experiment'] = pd.to_datetime(display_df['last_experiment']).dt.strftime('%Y-%m-%d')
|
| 256 |
-
|
| 257 |
-
# Select and order columns for display
|
| 258 |
-
display_columns = ['campaign_name', 'total_experiments', 'total_messages', 'total_rejections',
|
| 259 |
-
'rejection_rate', 'models_used', 'first_experiment', 'last_experiment']
|
| 260 |
-
display_columns = [col for col in display_columns if col in display_df.columns]
|
| 261 |
-
|
| 262 |
-
st.dataframe(
|
| 263 |
-
display_df[display_columns],
|
| 264 |
-
use_container_width=True,
|
| 265 |
-
hide_index=True,
|
| 266 |
-
column_config={
|
| 267 |
-
"campaign_name": "Campaign Name",
|
| 268 |
-
"total_experiments": st.column_config.NumberColumn("Experiments", format="%d"),
|
| 269 |
-
"total_messages": st.column_config.NumberColumn("Messages", format="%d"),
|
| 270 |
-
"total_rejections": st.column_config.NumberColumn("Rejections", format="%d"),
|
| 271 |
-
"rejection_rate": st.column_config.NumberColumn("Rejection Rate", format="%.1f%%"),
|
| 272 |
-
"models_used": "Models Used",
|
| 273 |
-
"first_experiment": "First Run",
|
| 274 |
-
"last_experiment": "Last Run"
|
| 275 |
-
}
|
| 276 |
-
)
|
| 277 |
-
|
| 278 |
-
st.markdown("---")
|
| 279 |
-
|
| 280 |
-
# Campaign comparison charts
|
| 281 |
-
col1, col2 = st.columns(2)
|
| 282 |
-
|
| 283 |
-
with col1:
|
| 284 |
-
st.subheader("📉 Rejection Rate by Campaign")
|
| 285 |
-
if 'rejection_rate' in campaigns_df.columns and len(campaigns_df) > 0:
|
| 286 |
-
fig = px.bar(
|
| 287 |
-
campaigns_df.sort_values('rejection_rate', ascending=False),
|
| 288 |
-
x='campaign_name',
|
| 289 |
-
y='rejection_rate',
|
| 290 |
-
color='rejection_rate',
|
| 291 |
-
color_continuous_scale='RdYlGn_r',
|
| 292 |
-
text='rejection_rate'
|
| 293 |
-
)
|
| 294 |
-
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
|
| 295 |
-
fig.update_layout(
|
| 296 |
-
template="plotly_dark",
|
| 297 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 298 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 299 |
-
xaxis_title="Campaign",
|
| 300 |
-
yaxis_title="Rejection Rate (%)",
|
| 301 |
-
showlegend=False
|
| 302 |
-
)
|
| 303 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 304 |
-
else:
|
| 305 |
-
st.info("No rejection data available")
|
| 306 |
-
|
| 307 |
-
with col2:
|
| 308 |
-
st.subheader("📊 Messages vs Rejections")
|
| 309 |
-
if 'total_messages' in campaigns_df.columns and 'total_rejections' in campaigns_df.columns:
|
| 310 |
-
fig = go.Figure()
|
| 311 |
-
fig.add_trace(go.Bar(
|
| 312 |
-
x=campaigns_df['campaign_name'],
|
| 313 |
-
y=campaigns_df['total_messages'],
|
| 314 |
-
name='Total Messages',
|
| 315 |
-
marker_color=theme['primary']
|
| 316 |
-
))
|
| 317 |
-
fig.add_trace(go.Bar(
|
| 318 |
-
x=campaigns_df['campaign_name'],
|
| 319 |
-
y=campaigns_df['total_rejections'],
|
| 320 |
-
name='Rejections',
|
| 321 |
-
marker_color='#ff4444'
|
| 322 |
-
))
|
| 323 |
-
fig.update_layout(
|
| 324 |
-
template="plotly_dark",
|
| 325 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 326 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 327 |
-
xaxis_title="Campaign",
|
| 328 |
-
yaxis_title="Count",
|
| 329 |
-
barmode='group'
|
| 330 |
-
)
|
| 331 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 332 |
-
else:
|
| 333 |
-
st.info("No message data available")
|
| 334 |
-
|
| 335 |
-
else:
|
| 336 |
-
# ========================================================================
|
| 337 |
-
# SELECTED CAMPAIGN DETAILED ANALYSIS
|
| 338 |
-
# ========================================================================
|
| 339 |
-
selected_campaign = st.session_state['selected_campaign']
|
| 340 |
-
|
| 341 |
-
st.header(f"🎯 Campaign Analysis: {selected_campaign}")
|
| 342 |
-
|
| 343 |
-
# Load detailed campaign analysis
|
| 344 |
-
with st.spinner(f"Loading detailed analytics for '{selected_campaign}'..."):
|
| 345 |
-
session = create_snowflake_session()
|
| 346 |
-
if session:
|
| 347 |
-
try:
|
| 348 |
-
db_manager = UIDatabaseManager(session)
|
| 349 |
-
|
| 350 |
-
# Get detailed feedback analysis
|
| 351 |
-
analysis = db_manager.get_campaign_feedback_analysis(selected_campaign, brand=brand)
|
| 352 |
-
|
| 353 |
-
# Store in session state
|
| 354 |
-
st.session_state['campaign_analysis'] = analysis
|
| 355 |
-
|
| 356 |
-
db_manager.close()
|
| 357 |
-
except Exception as e:
|
| 358 |
-
st.error(f"Error loading campaign analysis: {e}")
|
| 359 |
-
import traceback
|
| 360 |
-
st.error(f"Traceback: {traceback.format_exc()}")
|
| 361 |
-
if session:
|
| 362 |
-
session.close()
|
| 363 |
-
st.stop()
|
| 364 |
-
else:
|
| 365 |
-
st.error("Failed to connect to Snowflake")
|
| 366 |
-
st.stop()
|
| 367 |
-
|
| 368 |
-
analysis = st.session_state.get('campaign_analysis', {})
|
| 369 |
-
|
| 370 |
-
# Get campaign summary
|
| 371 |
-
campaign_summary = campaigns_df[campaigns_df['campaign_name'] == selected_campaign].iloc[0]
|
| 372 |
-
|
| 373 |
-
# Campaign overview metrics
|
| 374 |
-
col1, col2, col3, col4, col5 = st.columns(5)
|
| 375 |
-
|
| 376 |
-
with col1:
|
| 377 |
-
st.metric("Experiments", int(campaign_summary.get('total_experiments', 0)))
|
| 378 |
-
|
| 379 |
-
with col2:
|
| 380 |
-
st.metric("Total Messages", f"{int(campaign_summary.get('total_messages', 0)):,}")
|
| 381 |
-
|
| 382 |
-
with col3:
|
| 383 |
-
st.metric("Total Rejections", f"{int(campaign_summary.get('total_rejections', 0)):,}")
|
| 384 |
-
|
| 385 |
-
with col4:
|
| 386 |
-
st.metric("Rejection Rate", f"{campaign_summary.get('rejection_rate', 0):.1f}%")
|
| 387 |
-
|
| 388 |
-
with col5:
|
| 389 |
-
st.metric("Models Used", int(len(str(campaign_summary.get('models_used', '')).split(', '))))
|
| 390 |
-
|
| 391 |
-
st.markdown("---")
|
| 392 |
-
|
| 393 |
-
# ========================================================================
|
| 394 |
-
# OVERALL FEEDBACK ANALYSIS
|
| 395 |
-
# ========================================================================
|
| 396 |
-
st.header("📊 Overall Feedback Analysis")
|
| 397 |
-
|
| 398 |
-
# Get data for overall analysis
|
| 399 |
-
reasons_df = analysis.get('rejection_reasons', pd.DataFrame())
|
| 400 |
-
stage_df = analysis.get('by_stage', pd.DataFrame())
|
| 401 |
-
model_df = analysis.get('by_model', pd.DataFrame())
|
| 402 |
-
|
| 403 |
-
col1, col2, col3 = st.columns(3)
|
| 404 |
-
|
| 405 |
-
with col1:
|
| 406 |
-
st.subheader("📋 Top Rejection Reasons")
|
| 407 |
-
if not reasons_df.empty:
|
| 408 |
-
top_reasons = reasons_df.nlargest(5, 'count')
|
| 409 |
-
for idx, row in top_reasons.iterrows():
|
| 410 |
-
reason_label = get_rejection_label(row['rejection_reason'])
|
| 411 |
-
st.metric(reason_label, f"{int(row['count']):,}",
|
| 412 |
-
help=f"Affected {int(row.get('experiments_affected', 0))} experiments")
|
| 413 |
-
else:
|
| 414 |
-
st.info("No rejection data available")
|
| 415 |
-
|
| 416 |
-
with col2:
|
| 417 |
-
st.subheader("⚠️ Highest Rejection Rate Stages")
|
| 418 |
-
if not stage_df.empty and 'rejection_rate' in stage_df.columns:
|
| 419 |
-
top_stages = stage_df.nlargest(5, 'rejection_rate')
|
| 420 |
-
for idx, row in top_stages.iterrows():
|
| 421 |
-
st.metric(
|
| 422 |
-
f"Stage {int(row['stage'])}",
|
| 423 |
-
f"{row['rejection_rate']:.1f}%",
|
| 424 |
-
help=f"{int(row.get('total_rejections', 0))} rejections out of {int(row.get('total_messages', 0))} messages"
|
| 425 |
-
)
|
| 426 |
-
else:
|
| 427 |
-
st.info("No stage rejection data available")
|
| 428 |
-
|
| 429 |
-
with col3:
|
| 430 |
-
st.subheader("🤖 Highest Rejection Rate Models")
|
| 431 |
-
if not model_df.empty and 'rejection_rate' in model_df.columns:
|
| 432 |
-
top_models = model_df.nlargest(5, 'rejection_rate')
|
| 433 |
-
for idx, row in top_models.iterrows():
|
| 434 |
-
model_name = row['llm_model']
|
| 435 |
-
# Truncate long model names
|
| 436 |
-
display_name = model_name if len(model_name) <= 20 else model_name[:17] + "..."
|
| 437 |
-
st.metric(
|
| 438 |
-
display_name,
|
| 439 |
-
f"{row['rejection_rate']:.1f}%",
|
| 440 |
-
help=f"{model_name}: {int(row.get('total_rejections', 0))} rejections"
|
| 441 |
-
)
|
| 442 |
-
else:
|
| 443 |
-
st.info("No model rejection data available")
|
| 444 |
-
|
| 445 |
-
# Overall rejection reasons chart
|
| 446 |
-
if not reasons_df.empty:
|
| 447 |
-
st.subheader("🔍 Rejection Reasons Distribution")
|
| 448 |
-
reasons_display = reasons_df.copy()
|
| 449 |
-
reasons_display['reason_label'] = reasons_display['rejection_reason'].apply(get_rejection_label)
|
| 450 |
-
|
| 451 |
-
fig = px.bar(
|
| 452 |
-
reasons_display.sort_values('count', ascending=False),
|
| 453 |
-
x='reason_label',
|
| 454 |
-
y='count',
|
| 455 |
-
color='count',
|
| 456 |
-
color_continuous_scale='Reds',
|
| 457 |
-
text='count'
|
| 458 |
-
)
|
| 459 |
-
fig.update_traces(textposition='outside')
|
| 460 |
-
fig.update_layout(
|
| 461 |
-
template="plotly_dark",
|
| 462 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 463 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 464 |
-
xaxis_title="Rejection Reason",
|
| 465 |
-
yaxis_title="Count",
|
| 466 |
-
showlegend=False,
|
| 467 |
-
height=350
|
| 468 |
-
)
|
| 469 |
-
fig.update_xaxes(tickangle=-45)
|
| 470 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 471 |
-
|
| 472 |
-
st.markdown("---")
|
| 473 |
-
|
| 474 |
-
# ========================================================================
|
| 475 |
-
# MODEL PERFORMANCE ANALYSIS
|
| 476 |
-
# ========================================================================
|
| 477 |
-
st.header("🤖 Model Performance Analysis")
|
| 478 |
-
|
| 479 |
-
model_df = analysis.get('by_model', pd.DataFrame())
|
| 480 |
-
|
| 481 |
-
if not model_df.empty:
|
| 482 |
-
col1, col2 = st.columns(2)
|
| 483 |
-
|
| 484 |
-
with col1:
|
| 485 |
-
st.subheader("📊 Rejection Rate by Model")
|
| 486 |
-
fig = px.bar(
|
| 487 |
-
model_df.sort_values('rejection_rate', ascending=True),
|
| 488 |
-
y='llm_model',
|
| 489 |
-
x='rejection_rate',
|
| 490 |
-
orientation='h',
|
| 491 |
-
color='rejection_rate',
|
| 492 |
-
color_continuous_scale='RdYlGn_r',
|
| 493 |
-
text='rejection_rate'
|
| 494 |
-
)
|
| 495 |
-
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
|
| 496 |
-
fig.update_layout(
|
| 497 |
-
template="plotly_dark",
|
| 498 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 499 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 500 |
-
xaxis_title="Rejection Rate (%)",
|
| 501 |
-
yaxis_title="Model",
|
| 502 |
-
showlegend=False
|
| 503 |
-
)
|
| 504 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 505 |
-
|
| 506 |
-
with col2:
|
| 507 |
-
st.subheader("📈 Messages vs Rejections by Model")
|
| 508 |
-
fig = go.Figure()
|
| 509 |
-
fig.add_trace(go.Bar(
|
| 510 |
-
x=model_df['llm_model'],
|
| 511 |
-
y=model_df['total_messages_generated'],
|
| 512 |
-
name='Total Messages',
|
| 513 |
-
marker_color=theme['primary']
|
| 514 |
-
))
|
| 515 |
-
fig.add_trace(go.Bar(
|
| 516 |
-
x=model_df['llm_model'],
|
| 517 |
-
y=model_df['total_rejections'],
|
| 518 |
-
name='Rejections',
|
| 519 |
-
marker_color='#ff4444'
|
| 520 |
-
))
|
| 521 |
-
fig.update_layout(
|
| 522 |
-
template="plotly_dark",
|
| 523 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 524 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 525 |
-
xaxis_title="Model",
|
| 526 |
-
yaxis_title="Count",
|
| 527 |
-
barmode='group'
|
| 528 |
-
)
|
| 529 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 530 |
-
|
| 531 |
-
# Model performance table
|
| 532 |
-
st.subheader("📋 Detailed Model Statistics")
|
| 533 |
-
st.dataframe(
|
| 534 |
-
model_df,
|
| 535 |
-
use_container_width=True,
|
| 536 |
-
hide_index=True,
|
| 537 |
-
column_config={
|
| 538 |
-
"llm_model": "Model",
|
| 539 |
-
"total_rejections": st.column_config.NumberColumn("Rejections", format="%d"),
|
| 540 |
-
"unique_users_rejected": st.column_config.NumberColumn("Unique Users", format="%d"),
|
| 541 |
-
"total_messages_generated": st.column_config.NumberColumn("Total Messages", format="%d"),
|
| 542 |
-
"rejection_rate": st.column_config.NumberColumn("Rejection Rate", format="%.2f%%")
|
| 543 |
-
}
|
| 544 |
-
)
|
| 545 |
-
else:
|
| 546 |
-
st.info("No model performance data available for this campaign")
|
| 547 |
-
|
| 548 |
-
st.markdown("---")
|
| 549 |
-
|
| 550 |
-
# ========================================================================
|
| 551 |
-
# COMPREHENSIVE STAGE ANALYSIS
|
| 552 |
-
# ========================================================================
|
| 553 |
-
st.header("📍 Comprehensive Stage Analysis")
|
| 554 |
-
|
| 555 |
-
stage_df = analysis.get('by_stage', pd.DataFrame())
|
| 556 |
-
reasons_by_stage_df = analysis.get('reasons_by_stage', pd.DataFrame())
|
| 557 |
-
|
| 558 |
-
if not stage_df.empty:
|
| 559 |
-
# Messages vs Rejections Chart
|
| 560 |
-
st.subheader("📊 Messages vs Rejections by Stage")
|
| 561 |
-
fig = go.Figure()
|
| 562 |
-
fig.add_trace(go.Bar(
|
| 563 |
-
x=stage_df['stage'],
|
| 564 |
-
y=stage_df['total_messages'],
|
| 565 |
-
name='Total Messages',
|
| 566 |
-
marker_color=theme['primary'],
|
| 567 |
-
text=stage_df['total_messages'],
|
| 568 |
-
textposition='outside'
|
| 569 |
-
))
|
| 570 |
-
fig.add_trace(go.Bar(
|
| 571 |
-
x=stage_df['stage'],
|
| 572 |
-
y=stage_df['total_rejections'],
|
| 573 |
-
name='Rejections',
|
| 574 |
-
marker_color='#ff4444',
|
| 575 |
-
text=stage_df['total_rejections'],
|
| 576 |
-
textposition='outside'
|
| 577 |
-
))
|
| 578 |
-
fig.update_layout(
|
| 579 |
-
template="plotly_dark",
|
| 580 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 581 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 582 |
-
xaxis_title="Stage",
|
| 583 |
-
yaxis_title="Count",
|
| 584 |
-
barmode='group',
|
| 585 |
-
height=400
|
| 586 |
-
)
|
| 587 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 588 |
-
|
| 589 |
-
# Comprehensive Stage Statistics Table
|
| 590 |
-
st.subheader("📋 Detailed Stage Metrics")
|
| 591 |
-
|
| 592 |
-
# Prepare display dataframe
|
| 593 |
-
stage_display = stage_df.copy()
|
| 594 |
-
|
| 595 |
-
# Column configuration for display
|
| 596 |
-
column_config = {
|
| 597 |
-
"stage": "Stage",
|
| 598 |
-
"total_messages": st.column_config.NumberColumn("Messages", format="%d"),
|
| 599 |
-
"total_rejections": st.column_config.NumberColumn("Rejections", format="%d"),
|
| 600 |
-
"rejection_rate": st.column_config.NumberColumn("Rejection Rate", format="%.2f%%"),
|
| 601 |
-
"unique_users_rejected": st.column_config.NumberColumn("Unique Users", format="%d"),
|
| 602 |
-
"experiments_count": st.column_config.NumberColumn("Experiments", format="%d"),
|
| 603 |
-
"models_used": "Models Used"
|
| 604 |
-
}
|
| 605 |
-
|
| 606 |
-
# Only show columns that exist
|
| 607 |
-
available_columns = [col for col in ['stage', 'total_messages', 'total_rejections', 'rejection_rate',
|
| 608 |
-
'unique_users_rejected', 'experiments_count', 'models_used']
|
| 609 |
-
if col in stage_display.columns]
|
| 610 |
-
|
| 611 |
-
st.dataframe(
|
| 612 |
-
stage_display[available_columns],
|
| 613 |
-
use_container_width=True,
|
| 614 |
-
hide_index=True,
|
| 615 |
-
column_config=column_config
|
| 616 |
-
)
|
| 617 |
-
|
| 618 |
-
st.markdown("---")
|
| 619 |
-
|
| 620 |
-
# ========================================================================
|
| 621 |
-
# INTERACTIVE STAGE SELECTOR
|
| 622 |
-
# ========================================================================
|
| 623 |
-
st.subheader("🎯 Detailed Stage-by-Stage Analysis")
|
| 624 |
-
st.markdown("Select a stage to see detailed breakdown of models, rejection reasons, and performance metrics.")
|
| 625 |
-
|
| 626 |
-
# Stage selector
|
| 627 |
-
available_stages = sorted(stage_df['stage'].unique())
|
| 628 |
-
|
| 629 |
-
if len(available_stages) > 0:
|
| 630 |
-
selected_stage_detail = st.selectbox(
|
| 631 |
-
"Select Stage for Detailed View",
|
| 632 |
-
available_stages,
|
| 633 |
-
key='detailed_stage_selector'
|
| 634 |
-
)
|
| 635 |
-
|
| 636 |
-
# Get selected stage data
|
| 637 |
-
stage_info = stage_df[stage_df['stage'] == selected_stage_detail].iloc[0]
|
| 638 |
-
|
| 639 |
-
# Display stage metrics
|
| 640 |
-
col1, col2, col3, col4 = st.columns(4)
|
| 641 |
-
|
| 642 |
-
with col1:
|
| 643 |
-
st.metric("Total Messages", f"{int(stage_info.get('total_messages', 0)):,}")
|
| 644 |
-
|
| 645 |
-
with col2:
|
| 646 |
-
st.metric("Total Rejections", f"{int(stage_info.get('total_rejections', 0)):,}")
|
| 647 |
-
|
| 648 |
-
with col3:
|
| 649 |
-
st.metric("Rejection Rate", f"{stage_info.get('rejection_rate', 0):.2f}%")
|
| 650 |
-
|
| 651 |
-
with col4:
|
| 652 |
-
st.metric("Experiments", int(stage_info.get('experiments_count', 0)))
|
| 653 |
-
|
| 654 |
-
# Models used at this stage
|
| 655 |
-
st.markdown(f"**🤖 Models Used at Stage {selected_stage_detail}:**")
|
| 656 |
-
models_used = stage_info.get('models_used', 'N/A')
|
| 657 |
-
st.info(models_used)
|
| 658 |
-
|
| 659 |
-
# Rejection reasons for this stage
|
| 660 |
-
if not reasons_by_stage_df.empty:
|
| 661 |
-
stage_reasons = reasons_by_stage_df[reasons_by_stage_df['stage'] == selected_stage_detail].copy()
|
| 662 |
-
|
| 663 |
-
if not stage_reasons.empty:
|
| 664 |
-
st.markdown(f"**🔍 Rejection Reasons for Stage {selected_stage_detail}:**")
|
| 665 |
-
|
| 666 |
-
# Apply labels
|
| 667 |
-
stage_reasons['reason_label'] = stage_reasons['rejection_reason'].apply(get_rejection_label)
|
| 668 |
-
|
| 669 |
-
col1, col2 = st.columns(2)
|
| 670 |
-
|
| 671 |
-
with col1:
|
| 672 |
-
# Pie chart
|
| 673 |
-
fig = px.pie(
|
| 674 |
-
stage_reasons,
|
| 675 |
-
values='count',
|
| 676 |
-
names='reason_label',
|
| 677 |
-
title=f"Rejection Reasons Distribution - Stage {selected_stage_detail}",
|
| 678 |
-
color_discrete_sequence=px.colors.qualitative.Set3
|
| 679 |
-
)
|
| 680 |
-
fig.update_layout(
|
| 681 |
-
template="plotly_dark",
|
| 682 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 683 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 684 |
-
height=350
|
| 685 |
-
)
|
| 686 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 687 |
-
|
| 688 |
-
with col2:
|
| 689 |
-
# Bar chart
|
| 690 |
-
fig = px.bar(
|
| 691 |
-
stage_reasons.sort_values('count', ascending=False),
|
| 692 |
-
x='reason_label',
|
| 693 |
-
y='count',
|
| 694 |
-
color='count',
|
| 695 |
-
color_continuous_scale='Oranges',
|
| 696 |
-
text='count',
|
| 697 |
-
title=f"Top Rejection Reasons - Stage {selected_stage_detail}"
|
| 698 |
-
)
|
| 699 |
-
fig.update_traces(textposition='outside')
|
| 700 |
-
fig.update_layout(
|
| 701 |
-
template="plotly_dark",
|
| 702 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 703 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 704 |
-
xaxis_title="Rejection Reason",
|
| 705 |
-
yaxis_title="Count",
|
| 706 |
-
showlegend=False,
|
| 707 |
-
height=350
|
| 708 |
-
)
|
| 709 |
-
fig.update_xaxes(tickangle=-45)
|
| 710 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 711 |
-
|
| 712 |
-
# Comparison to other stages
|
| 713 |
-
st.markdown(f"**📊 Stage {selected_stage_detail} Comparison:**")
|
| 714 |
-
|
| 715 |
-
# Calculate average rejection rate across all stages
|
| 716 |
-
avg_rejection_rate = stage_df['rejection_rate'].mean()
|
| 717 |
-
stage_rejection_rate = stage_info.get('rejection_rate', 0)
|
| 718 |
-
|
| 719 |
-
if stage_rejection_rate > avg_rejection_rate:
|
| 720 |
-
delta_text = f"{stage_rejection_rate - avg_rejection_rate:.1f}% above average"
|
| 721 |
-
st.warning(f"⚠️ This stage has a rejection rate **{stage_rejection_rate:.1f}%**, which is **{delta_text}**")
|
| 722 |
-
elif stage_rejection_rate < avg_rejection_rate:
|
| 723 |
-
delta_text = f"{avg_rejection_rate - stage_rejection_rate:.1f}% below average"
|
| 724 |
-
st.success(f"✅ This stage has a rejection rate **{stage_rejection_rate:.1f}%**, which is **{delta_text}**")
|
| 725 |
-
else:
|
| 726 |
-
st.info(f"ℹ️ This stage has an average rejection rate of **{stage_rejection_rate:.1f}%**")
|
| 727 |
-
|
| 728 |
-
# Show detailed breakdown table
|
| 729 |
-
st.markdown("**📋 Detailed Rejection Breakdown:**")
|
| 730 |
-
st.dataframe(
|
| 731 |
-
stage_reasons[['reason_label', 'count']].rename(columns={'reason_label': 'Rejection Reason', 'count': 'Count'}),
|
| 732 |
-
use_container_width=True,
|
| 733 |
-
hide_index=True
|
| 734 |
-
)
|
| 735 |
-
else:
|
| 736 |
-
st.info(f"No rejection reasons data available for Stage {selected_stage_detail}")
|
| 737 |
-
else:
|
| 738 |
-
st.info("No rejection reasons data available")
|
| 739 |
-
else:
|
| 740 |
-
st.info("No stage-level data available for this campaign")
|
| 741 |
-
|
| 742 |
-
st.markdown("---")
|
| 743 |
-
|
| 744 |
-
# ========================================================================
|
| 745 |
-
# REJECTION REASONS ANALYSIS (Interactive)
|
| 746 |
-
# ========================================================================
|
| 747 |
-
st.header("🔍 Rejection Reasons Analysis")
|
| 748 |
-
|
| 749 |
-
# Get rejection reasons data
|
| 750 |
-
reasons_df = analysis.get('rejection_reasons', pd.DataFrame())
|
| 751 |
-
reasons_by_stage_df = analysis.get('reasons_by_stage', pd.DataFrame())
|
| 752 |
-
reasons_by_model_df = analysis.get('reasons_by_model', pd.DataFrame())
|
| 753 |
-
|
| 754 |
-
if not reasons_df.empty:
|
| 755 |
-
# Overall rejection reasons
|
| 756 |
-
st.subheader("📊 Overall Rejection Reasons Distribution")
|
| 757 |
-
|
| 758 |
-
# Apply labels to reasons
|
| 759 |
-
reasons_display = reasons_df.copy()
|
| 760 |
-
reasons_display['reason_label'] = reasons_display['rejection_reason'].apply(get_rejection_label)
|
| 761 |
-
|
| 762 |
-
col1, col2 = st.columns(2)
|
| 763 |
-
|
| 764 |
-
with col1:
|
| 765 |
-
# Pie chart
|
| 766 |
-
fig = px.pie(
|
| 767 |
-
reasons_display,
|
| 768 |
-
values='count',
|
| 769 |
-
names='reason_label',
|
| 770 |
-
title="Rejection Reasons Breakdown",
|
| 771 |
-
color_discrete_sequence=px.colors.qualitative.Set3
|
| 772 |
-
)
|
| 773 |
-
fig.update_layout(
|
| 774 |
-
template="plotly_dark",
|
| 775 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 776 |
-
plot_bgcolor='rgba(0,0,0,0)'
|
| 777 |
-
)
|
| 778 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 779 |
-
|
| 780 |
-
with col2:
|
| 781 |
-
# Bar chart
|
| 782 |
-
fig = px.bar(
|
| 783 |
-
reasons_display.sort_values('count', ascending=False),
|
| 784 |
-
x='reason_label',
|
| 785 |
-
y='count',
|
| 786 |
-
color='count',
|
| 787 |
-
color_continuous_scale='Reds',
|
| 788 |
-
text='count'
|
| 789 |
-
)
|
| 790 |
-
fig.update_traces(textposition='outside')
|
| 791 |
-
fig.update_layout(
|
| 792 |
-
template="plotly_dark",
|
| 793 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 794 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 795 |
-
xaxis_title="Rejection Reason",
|
| 796 |
-
yaxis_title="Count",
|
| 797 |
-
showlegend=False
|
| 798 |
-
)
|
| 799 |
-
fig.update_xaxes(tickangle=-45)
|
| 800 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 801 |
-
|
| 802 |
-
st.markdown("---")
|
| 803 |
-
|
| 804 |
-
# Interactive filtering section
|
| 805 |
-
st.subheader("🎛️ Interactive Rejection Analysis")
|
| 806 |
-
|
| 807 |
-
tab1, tab2 = st.tabs(["📍 By Stage", "🤖 By Model"])
|
| 808 |
-
|
| 809 |
-
with tab1:
|
| 810 |
-
# Rejection reasons by stage
|
| 811 |
-
if not reasons_by_stage_df.empty:
|
| 812 |
-
# Apply labels
|
| 813 |
-
reasons_by_stage_display = reasons_by_stage_df.copy()
|
| 814 |
-
reasons_by_stage_display['reason_label'] = reasons_by_stage_display['rejection_reason'].apply(get_rejection_label)
|
| 815 |
-
|
| 816 |
-
# Stage selector
|
| 817 |
-
available_stages = sorted(reasons_by_stage_display['stage'].unique())
|
| 818 |
-
|
| 819 |
-
if len(available_stages) > 0:
|
| 820 |
-
selected_stage = st.selectbox(
|
| 821 |
-
"Select Stage to Analyze",
|
| 822 |
-
available_stages,
|
| 823 |
-
key='stage_selector'
|
| 824 |
-
)
|
| 825 |
-
|
| 826 |
-
# Filter by selected stage
|
| 827 |
-
stage_reasons = reasons_by_stage_display[reasons_by_stage_display['stage'] == selected_stage]
|
| 828 |
-
|
| 829 |
-
col1, col2 = st.columns(2)
|
| 830 |
-
|
| 831 |
-
with col1:
|
| 832 |
-
# Pie chart for selected stage
|
| 833 |
-
fig = px.pie(
|
| 834 |
-
stage_reasons,
|
| 835 |
-
values='count',
|
| 836 |
-
names='reason_label',
|
| 837 |
-
title=f"Rejection Reasons - Stage {selected_stage}",
|
| 838 |
-
color_discrete_sequence=px.colors.qualitative.Pastel
|
| 839 |
-
)
|
| 840 |
-
fig.update_layout(
|
| 841 |
-
template="plotly_dark",
|
| 842 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 843 |
-
plot_bgcolor='rgba(0,0,0,0)'
|
| 844 |
-
)
|
| 845 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 846 |
-
|
| 847 |
-
with col2:
|
| 848 |
-
# Bar chart for selected stage
|
| 849 |
-
fig = px.bar(
|
| 850 |
-
stage_reasons.sort_values('count', ascending=False),
|
| 851 |
-
x='reason_label',
|
| 852 |
-
y='count',
|
| 853 |
-
color='count',
|
| 854 |
-
color_continuous_scale='Oranges',
|
| 855 |
-
text='count'
|
| 856 |
-
)
|
| 857 |
-
fig.update_traces(textposition='outside')
|
| 858 |
-
fig.update_layout(
|
| 859 |
-
template="plotly_dark",
|
| 860 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 861 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 862 |
-
xaxis_title="Rejection Reason",
|
| 863 |
-
yaxis_title="Count",
|
| 864 |
-
showlegend=False
|
| 865 |
-
)
|
| 866 |
-
fig.update_xaxes(tickangle=-45)
|
| 867 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 868 |
-
|
| 869 |
-
# Heatmap across all stages
|
| 870 |
-
st.markdown("#### 🔥 Rejection Heatmap Across All Stages")
|
| 871 |
-
pivot_data = reasons_by_stage_display.pivot_table(
|
| 872 |
-
index='reason_label',
|
| 873 |
-
columns='stage',
|
| 874 |
-
values='count',
|
| 875 |
-
fill_value=0
|
| 876 |
-
)
|
| 877 |
-
|
| 878 |
-
fig = px.imshow(
|
| 879 |
-
pivot_data,
|
| 880 |
-
labels=dict(x="Stage", y="Rejection Reason", color="Count"),
|
| 881 |
-
x=pivot_data.columns,
|
| 882 |
-
y=pivot_data.index,
|
| 883 |
-
color_continuous_scale='Reds',
|
| 884 |
-
aspect='auto'
|
| 885 |
-
)
|
| 886 |
-
fig.update_layout(
|
| 887 |
-
template="plotly_dark",
|
| 888 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 889 |
-
plot_bgcolor='rgba(0,0,0,0)'
|
| 890 |
-
)
|
| 891 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 892 |
-
else:
|
| 893 |
-
st.info("No stage-level rejection reason data available")
|
| 894 |
-
|
| 895 |
-
with tab2:
|
| 896 |
-
# Rejection reasons by model
|
| 897 |
-
if not reasons_by_model_df.empty:
|
| 898 |
-
# Apply labels
|
| 899 |
-
reasons_by_model_display = reasons_by_model_df.copy()
|
| 900 |
-
reasons_by_model_display['reason_label'] = reasons_by_model_display['rejection_reason'].apply(get_rejection_label)
|
| 901 |
-
|
| 902 |
-
# Model selector
|
| 903 |
-
available_models = sorted(reasons_by_model_display['llm_model'].unique())
|
| 904 |
-
|
| 905 |
-
if len(available_models) > 0:
|
| 906 |
-
selected_model = st.selectbox(
|
| 907 |
-
"Select Model to Analyze",
|
| 908 |
-
available_models,
|
| 909 |
-
key='model_selector'
|
| 910 |
-
)
|
| 911 |
-
|
| 912 |
-
# Filter by selected model
|
| 913 |
-
model_reasons = reasons_by_model_display[reasons_by_model_display['llm_model'] == selected_model]
|
| 914 |
-
|
| 915 |
-
col1, col2 = st.columns(2)
|
| 916 |
-
|
| 917 |
-
with col1:
|
| 918 |
-
# Pie chart for selected model
|
| 919 |
-
fig = px.pie(
|
| 920 |
-
model_reasons,
|
| 921 |
-
values='count',
|
| 922 |
-
names='reason_label',
|
| 923 |
-
title=f"Rejection Reasons - {selected_model}",
|
| 924 |
-
color_discrete_sequence=px.colors.qualitative.Set2
|
| 925 |
-
)
|
| 926 |
-
fig.update_layout(
|
| 927 |
-
template="plotly_dark",
|
| 928 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 929 |
-
plot_bgcolor='rgba(0,0,0,0)'
|
| 930 |
-
)
|
| 931 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 932 |
-
|
| 933 |
-
with col2:
|
| 934 |
-
# Bar chart for selected model
|
| 935 |
-
fig = px.bar(
|
| 936 |
-
model_reasons.sort_values('count', ascending=False),
|
| 937 |
-
x='reason_label',
|
| 938 |
-
y='count',
|
| 939 |
-
color='count',
|
| 940 |
-
color_continuous_scale='Blues',
|
| 941 |
-
text='count'
|
| 942 |
-
)
|
| 943 |
-
fig.update_traces(textposition='outside')
|
| 944 |
-
fig.update_layout(
|
| 945 |
-
template="plotly_dark",
|
| 946 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 947 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 948 |
-
xaxis_title="Rejection Reason",
|
| 949 |
-
yaxis_title="Count",
|
| 950 |
-
showlegend=False
|
| 951 |
-
)
|
| 952 |
-
fig.update_xaxes(tickangle=-45)
|
| 953 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 954 |
-
|
| 955 |
-
# Heatmap across all models
|
| 956 |
-
st.markdown("#### 🔥 Rejection Heatmap Across All Models")
|
| 957 |
-
pivot_data = reasons_by_model_display.pivot_table(
|
| 958 |
-
index='reason_label',
|
| 959 |
-
columns='llm_model',
|
| 960 |
-
values='count',
|
| 961 |
-
fill_value=0
|
| 962 |
-
)
|
| 963 |
-
|
| 964 |
-
fig = px.imshow(
|
| 965 |
-
pivot_data,
|
| 966 |
-
labels=dict(x="Model", y="Rejection Reason", color="Count"),
|
| 967 |
-
x=pivot_data.columns,
|
| 968 |
-
y=pivot_data.index,
|
| 969 |
-
color_continuous_scale='Blues',
|
| 970 |
-
aspect='auto'
|
| 971 |
-
)
|
| 972 |
-
fig.update_layout(
|
| 973 |
-
template="plotly_dark",
|
| 974 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 975 |
-
plot_bgcolor='rgba(0,0,0,0)'
|
| 976 |
-
)
|
| 977 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 978 |
-
else:
|
| 979 |
-
st.info("No model-level rejection reason data available")
|
| 980 |
-
else:
|
| 981 |
-
st.info("No rejection reason data available for this campaign")
|
| 982 |
-
|
| 983 |
-
# ============================================================================
|
| 984 |
-
# EXPORT SECTION
|
| 985 |
-
# ============================================================================
|
| 986 |
-
st.markdown("---")
|
| 987 |
-
st.header("💾 Export Data")
|
| 988 |
-
|
| 989 |
-
col1, col2, col3 = st.columns(3)
|
| 990 |
-
|
| 991 |
-
with col1:
|
| 992 |
-
if st.button("📥 Export Campaigns Summary", use_container_width=True):
|
| 993 |
-
csv = campaigns_df.to_csv(index=False, encoding='utf-8')
|
| 994 |
-
st.download_button(
|
| 995 |
-
label="⬇️ Download Campaigns CSV",
|
| 996 |
-
data=csv,
|
| 997 |
-
file_name=f"{brand}_campaigns_summary_{datetime.now().strftime('%Y%m%d')}.csv",
|
| 998 |
-
mime="text/csv",
|
| 999 |
-
use_container_width=True
|
| 1000 |
-
)
|
| 1001 |
-
|
| 1002 |
-
with col2:
|
| 1003 |
-
if st.button("📥 Export Experiments Summary", use_container_width=True):
|
| 1004 |
-
csv = experiments_df.to_csv(index=False, encoding='utf-8')
|
| 1005 |
-
st.download_button(
|
| 1006 |
-
label="⬇️ Download Experiments CSV",
|
| 1007 |
-
data=csv,
|
| 1008 |
-
file_name=f"{brand}_experiments_summary_{datetime.now().strftime('%Y%m%d')}.csv",
|
| 1009 |
-
mime="text/csv",
|
| 1010 |
-
use_container_width=True
|
| 1011 |
-
)
|
| 1012 |
-
|
| 1013 |
-
with col3:
|
| 1014 |
-
if st.session_state.get('selected_campaign') and st.session_state.get('campaign_analysis'):
|
| 1015 |
-
if st.button("📥 Export Campaign Analysis", use_container_width=True):
|
| 1016 |
-
# Combine all analysis dataframes
|
| 1017 |
-
analysis = st.session_state['campaign_analysis']
|
| 1018 |
-
|
| 1019 |
-
# Create a writer object
|
| 1020 |
-
from io import BytesIO
|
| 1021 |
-
output = BytesIO()
|
| 1022 |
-
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
|
| 1023 |
-
if not analysis['by_stage'].empty:
|
| 1024 |
-
analysis['by_stage'].to_excel(writer, sheet_name='By Stage', index=False)
|
| 1025 |
-
if not analysis['by_model'].empty:
|
| 1026 |
-
analysis['by_model'].to_excel(writer, sheet_name='By Model', index=False)
|
| 1027 |
-
if not analysis['rejection_reasons'].empty:
|
| 1028 |
-
analysis['rejection_reasons'].to_excel(writer, sheet_name='Rejection Reasons', index=False)
|
| 1029 |
-
if not analysis['reasons_by_stage'].empty:
|
| 1030 |
-
analysis['reasons_by_stage'].to_excel(writer, sheet_name='Reasons by Stage', index=False)
|
| 1031 |
-
if not analysis['reasons_by_model'].empty:
|
| 1032 |
-
analysis['reasons_by_model'].to_excel(writer, sheet_name='Reasons by Model', index=False)
|
| 1033 |
-
|
| 1034 |
-
st.download_button(
|
| 1035 |
-
label="⬇️ Download Analysis Excel",
|
| 1036 |
-
data=output.getvalue(),
|
| 1037 |
-
file_name=f"{brand}_{selected_campaign}_analysis_{datetime.now().strftime('%Y%m%d')}.xlsx",
|
| 1038 |
-
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 1039 |
-
use_container_width=True
|
| 1040 |
-
)
|
| 1041 |
-
|
| 1042 |
-
st.markdown("---")
|
| 1043 |
-
st.markdown("**💡 Pro Tip:** Use the campaign selector to drill down into specific campaigns and identify patterns in model performance and rejection reasons!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/requirements.txt
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
# AI Messaging System - Visualization Tool Requirements
|
| 2 |
-
|
| 3 |
-
# Core Dependencies
|
| 4 |
-
streamlit>=1.28.0
|
| 5 |
-
pandas>=2.0.0
|
| 6 |
-
python-dotenv>=1.0.0
|
| 7 |
-
|
| 8 |
-
# Data Visualization
|
| 9 |
-
plotly>=5.17.0
|
| 10 |
-
|
| 11 |
-
# Snowflake Integration
|
| 12 |
-
snowflake-snowpark-python>=1.9.0
|
| 13 |
-
|
| 14 |
-
# AI/LLM APIs
|
| 15 |
-
openai>=1.3.0
|
| 16 |
-
google-generativeai>=0.3.0
|
| 17 |
-
|
| 18 |
-
# Utility Libraries
|
| 19 |
-
pathlib>=1.0.1
|
| 20 |
-
tqdm>=4.66.0
|
| 21 |
-
|
| 22 |
-
# Note: The ai_messaging_system_v2 package must be in the parent directory
|
| 23 |
-
# and its dependencies will be inherited from the main project
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/utils/__init__.py
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Utility modules for the AI Messaging System Visualization Tool.
|
| 3 |
-
|
| 4 |
-
Modules:
|
| 5 |
-
- auth: Authentication and access control
|
| 6 |
-
- data_loader: Data loading from Snowflake and CSV files
|
| 7 |
-
- config_manager: Configuration persistence and management
|
| 8 |
-
- theme: Brand-specific theming and styling
|
| 9 |
-
- feedback_manager: Feedback storage and retrieval
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
from .auth import verify_login, check_authentication, AUTHORIZED_EMAILS
|
| 13 |
-
from .theme import get_brand_theme, apply_theme, BRAND_COLORS
|
| 14 |
-
from .data_loader import DataLoader
|
| 15 |
-
from .config_manager import ConfigManager
|
| 16 |
-
from .feedback_manager import FeedbackManager
|
| 17 |
-
|
| 18 |
-
__all__ = [
|
| 19 |
-
'verify_login',
|
| 20 |
-
'check_authentication',
|
| 21 |
-
'AUTHORIZED_EMAILS',
|
| 22 |
-
'get_brand_theme',
|
| 23 |
-
'apply_theme',
|
| 24 |
-
'BRAND_COLORS',
|
| 25 |
-
'DataLoader',
|
| 26 |
-
'ConfigManager',
|
| 27 |
-
'FeedbackManager',
|
| 28 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/utils/auth.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Authentication module for the AI Messaging System Visualization Tool.
|
| 3 |
-
|
| 4 |
-
Handles user authentication and access control.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import os
|
| 8 |
-
import streamlit as st
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
from dotenv import load_dotenv
|
| 11 |
-
|
| 12 |
-
# Load environment variables from .env file in visualization directory
|
| 13 |
-
env_path = Path(__file__).parent.parent / '.env'
|
| 14 |
-
if env_path.exists():
|
| 15 |
-
load_dotenv(env_path)
|
| 16 |
-
else:
|
| 17 |
-
# Try parent directory .env
|
| 18 |
-
parent_env_path = Path(__file__).parent.parent.parent / '.env'
|
| 19 |
-
if parent_env_path.exists():
|
| 20 |
-
load_dotenv(parent_env_path)
|
| 21 |
-
|
| 22 |
-
# Authorized emails - team members only
|
| 23 |
-
AUTHORIZED_EMAILS = {
|
| 24 |
-
"danial@musora.com",
|
| 25 |
-
"danial.ebrat@gmail.com",
|
| 26 |
-
"simon@musora.com",
|
| 27 |
-
"una@musora.com",
|
| 28 |
-
"mark@musora.com",
|
| 29 |
-
"gabriel@musora.com",
|
| 30 |
-
"nikki@musora.com"
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def get_credential(key: str) -> str:
|
| 35 |
-
"""
|
| 36 |
-
Get credential from environment variables.
|
| 37 |
-
|
| 38 |
-
Args:
|
| 39 |
-
key: Credential key
|
| 40 |
-
|
| 41 |
-
Returns:
|
| 42 |
-
str: Credential value
|
| 43 |
-
"""
|
| 44 |
-
return os.getenv(key, "")
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def get_valid_token() -> str:
|
| 48 |
-
"""
|
| 49 |
-
Get the valid access token from environment.
|
| 50 |
-
|
| 51 |
-
Returns:
|
| 52 |
-
str: Valid access token
|
| 53 |
-
"""
|
| 54 |
-
return get_credential("APP_TOKEN")
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def verify_login(email: str, token: str) -> bool:
|
| 58 |
-
"""
|
| 59 |
-
Verify user login credentials.
|
| 60 |
-
|
| 61 |
-
Args:
|
| 62 |
-
email: User email address
|
| 63 |
-
token: Access token
|
| 64 |
-
|
| 65 |
-
Returns:
|
| 66 |
-
bool: True if credentials are valid, False otherwise
|
| 67 |
-
"""
|
| 68 |
-
valid_token = get_valid_token()
|
| 69 |
-
email_normalized = email.lower().strip()
|
| 70 |
-
|
| 71 |
-
return (email_normalized in AUTHORIZED_EMAILS) and (token == valid_token)
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def check_authentication() -> bool:
|
| 75 |
-
"""
|
| 76 |
-
Check if user is authenticated in current session.
|
| 77 |
-
|
| 78 |
-
Returns:
|
| 79 |
-
bool: True if authenticated, False otherwise
|
| 80 |
-
"""
|
| 81 |
-
return st.session_state.get("authenticated", False)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def get_current_user() -> str:
|
| 85 |
-
"""
|
| 86 |
-
Get the currently logged-in user's email.
|
| 87 |
-
|
| 88 |
-
Returns:
|
| 89 |
-
str: User email or empty string if not authenticated
|
| 90 |
-
"""
|
| 91 |
-
return st.session_state.get("user_email", "")
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def logout():
|
| 95 |
-
"""
|
| 96 |
-
Log out the current user by clearing session state.
|
| 97 |
-
"""
|
| 98 |
-
if "authenticated" in st.session_state:
|
| 99 |
-
del st.session_state["authenticated"]
|
| 100 |
-
if "user_email" in st.session_state:
|
| 101 |
-
del st.session_state["user_email"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/utils/config_manager.py
DELETED
|
@@ -1,361 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Configuration management module for the AI Messaging System Visualization Tool.
|
| 3 |
-
|
| 4 |
-
Handles saving, loading, and managing campaign configurations from Snowflake.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import json
|
| 8 |
-
from typing import Dict, List, Optional
|
| 9 |
-
import streamlit as st
|
| 10 |
-
from snowflake.snowpark import Session
|
| 11 |
-
from .db_manager import UIDatabaseManager
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class ConfigManager:
|
| 15 |
-
"""
|
| 16 |
-
Manages campaign configurations from Snowflake with automatic versioning.
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
def __init__(self, session: Optional[Session] = None):
|
| 20 |
-
"""
|
| 21 |
-
Initialize ConfigManager with optional Snowflake session.
|
| 22 |
-
|
| 23 |
-
Args:
|
| 24 |
-
session: Optional Snowflake Snowpark session. If not provided,
|
| 25 |
-
must be set later using set_session()
|
| 26 |
-
"""
|
| 27 |
-
self.session = session
|
| 28 |
-
self.db_manager = None
|
| 29 |
-
|
| 30 |
-
if session:
|
| 31 |
-
self.db_manager = UIDatabaseManager(session)
|
| 32 |
-
|
| 33 |
-
def set_session(self, session: Session):
|
| 34 |
-
"""
|
| 35 |
-
Set Snowflake session for database operations.
|
| 36 |
-
|
| 37 |
-
Args:
|
| 38 |
-
session: Snowflake Snowpark session
|
| 39 |
-
"""
|
| 40 |
-
self.session = session
|
| 41 |
-
self.db_manager = UIDatabaseManager(session)
|
| 42 |
-
|
| 43 |
-
def _ensure_db_manager(self):
|
| 44 |
-
"""Ensure database manager is initialized."""
|
| 45 |
-
if self.db_manager is None:
|
| 46 |
-
raise RuntimeError(
|
| 47 |
-
"ConfigManager requires a Snowflake session. "
|
| 48 |
-
"Call set_session() or provide session in constructor."
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
def load_default_config(self, brand: str, campaign_type: str = "re_engagement") -> Dict:
|
| 52 |
-
"""
|
| 53 |
-
Load default configuration from Snowflake.
|
| 54 |
-
Looks for config named "{brand}_{campaign_type}_test".
|
| 55 |
-
|
| 56 |
-
Args:
|
| 57 |
-
brand: Brand name
|
| 58 |
-
campaign_type: Campaign type (default: re_engagement)
|
| 59 |
-
|
| 60 |
-
Returns:
|
| 61 |
-
dict: Campaign configuration
|
| 62 |
-
"""
|
| 63 |
-
self._ensure_db_manager()
|
| 64 |
-
|
| 65 |
-
try:
|
| 66 |
-
config_name = f"{brand}_{campaign_type}_test"
|
| 67 |
-
all_configs = self.db_manager.load_configs(brand)
|
| 68 |
-
|
| 69 |
-
if config_name in all_configs:
|
| 70 |
-
return all_configs[config_name]
|
| 71 |
-
else:
|
| 72 |
-
st.warning(f"Default config '{config_name}' not found in Snowflake")
|
| 73 |
-
return {}
|
| 74 |
-
|
| 75 |
-
except Exception as e:
|
| 76 |
-
st.error(f"Error loading default config: {e}")
|
| 77 |
-
return {}
|
| 78 |
-
|
| 79 |
-
def save_custom_config(self, brand: str, config_name: str, config: Dict) -> bool:
|
| 80 |
-
"""
|
| 81 |
-
Save custom configuration to Snowflake with automatic versioning.
|
| 82 |
-
If config exists, automatically increments CONFIG_VERSION.
|
| 83 |
-
|
| 84 |
-
Args:
|
| 85 |
-
brand: Brand name
|
| 86 |
-
config_name: Configuration name
|
| 87 |
-
config: Configuration dictionary
|
| 88 |
-
|
| 89 |
-
Returns:
|
| 90 |
-
bool: True if saved successfully, False otherwise
|
| 91 |
-
"""
|
| 92 |
-
self._ensure_db_manager()
|
| 93 |
-
|
| 94 |
-
try:
|
| 95 |
-
# Ensure brand is set in config
|
| 96 |
-
config['brand'] = brand
|
| 97 |
-
|
| 98 |
-
# Save to Snowflake (versioning is automatic)
|
| 99 |
-
success = self.db_manager.save_config(config_name, config, brand)
|
| 100 |
-
|
| 101 |
-
if success:
|
| 102 |
-
version = self.db_manager.get_config_version(config_name, brand)
|
| 103 |
-
st.success(f"✅ Saved '{config_name}' as version {version}")
|
| 104 |
-
|
| 105 |
-
return success
|
| 106 |
-
|
| 107 |
-
except Exception as e:
|
| 108 |
-
st.error(f"Error saving config: {e}")
|
| 109 |
-
return False
|
| 110 |
-
|
| 111 |
-
def load_custom_config(self, brand: str, config_name: str) -> Optional[Dict]:
|
| 112 |
-
"""
|
| 113 |
-
Load custom configuration from Snowflake.
|
| 114 |
-
Returns the latest version.
|
| 115 |
-
|
| 116 |
-
Args:
|
| 117 |
-
brand: Brand name
|
| 118 |
-
config_name: Configuration name
|
| 119 |
-
|
| 120 |
-
Returns:
|
| 121 |
-
dict or None: Configuration dictionary or None if not found
|
| 122 |
-
"""
|
| 123 |
-
self._ensure_db_manager()
|
| 124 |
-
|
| 125 |
-
try:
|
| 126 |
-
all_configs = self.db_manager.load_configs(brand)
|
| 127 |
-
|
| 128 |
-
if config_name in all_configs:
|
| 129 |
-
return all_configs[config_name]
|
| 130 |
-
else:
|
| 131 |
-
return None
|
| 132 |
-
|
| 133 |
-
except Exception as e:
|
| 134 |
-
st.error(f"Error loading config: {e}")
|
| 135 |
-
return None
|
| 136 |
-
|
| 137 |
-
def list_custom_configs(self, brand: str) -> List[str]:
|
| 138 |
-
"""
|
| 139 |
-
List all custom configurations for a brand from Snowflake.
|
| 140 |
-
Returns only the latest version of each config.
|
| 141 |
-
|
| 142 |
-
Args:
|
| 143 |
-
brand: Brand name
|
| 144 |
-
|
| 145 |
-
Returns:
|
| 146 |
-
list: List of configuration names
|
| 147 |
-
"""
|
| 148 |
-
self._ensure_db_manager()
|
| 149 |
-
|
| 150 |
-
try:
|
| 151 |
-
all_configs = self.db_manager.load_configs(brand)
|
| 152 |
-
return sorted(list(all_configs.keys()))
|
| 153 |
-
|
| 154 |
-
except Exception as e:
|
| 155 |
-
st.error(f"Error listing configs: {e}")
|
| 156 |
-
return []
|
| 157 |
-
|
| 158 |
-
def delete_custom_config(self, brand: str, config_name: str) -> bool:
|
| 159 |
-
"""
|
| 160 |
-
Delete configuration (not implemented for Snowflake version).
|
| 161 |
-
Configurations in Snowflake are versioned and should not be deleted.
|
| 162 |
-
|
| 163 |
-
Args:
|
| 164 |
-
brand: Brand name
|
| 165 |
-
config_name: Configuration name
|
| 166 |
-
|
| 167 |
-
Returns:
|
| 168 |
-
bool: Always False (not supported)
|
| 169 |
-
"""
|
| 170 |
-
st.warning("⚠️ Config deletion is not supported in Snowflake version. Configs are versioned.")
|
| 171 |
-
return False
|
| 172 |
-
|
| 173 |
-
def config_exists(self, brand: str, config_name: str) -> bool:
|
| 174 |
-
"""
|
| 175 |
-
Check if a custom configuration exists in Snowflake.
|
| 176 |
-
|
| 177 |
-
Args:
|
| 178 |
-
brand: Brand name
|
| 179 |
-
config_name: Configuration name
|
| 180 |
-
|
| 181 |
-
Returns:
|
| 182 |
-
bool: True if exists, False otherwise
|
| 183 |
-
"""
|
| 184 |
-
self._ensure_db_manager()
|
| 185 |
-
|
| 186 |
-
try:
|
| 187 |
-
all_configs = self.db_manager.load_configs(brand)
|
| 188 |
-
return config_name in all_configs
|
| 189 |
-
|
| 190 |
-
except Exception as e:
|
| 191 |
-
st.error(f"Error checking config existence: {e}")
|
| 192 |
-
return False
|
| 193 |
-
|
| 194 |
-
def get_all_configs(self, brand: str) -> Dict[str, Dict]:
|
| 195 |
-
"""
|
| 196 |
-
Get all configurations (default + custom) for a brand from Snowflake.
|
| 197 |
-
Returns the latest version of each config.
|
| 198 |
-
|
| 199 |
-
Args:
|
| 200 |
-
brand: Brand name
|
| 201 |
-
|
| 202 |
-
Returns:
|
| 203 |
-
dict: Dictionary mapping config names to config dictionaries
|
| 204 |
-
"""
|
| 205 |
-
self._ensure_db_manager()
|
| 206 |
-
|
| 207 |
-
try:
|
| 208 |
-
return self.db_manager.load_configs(brand)
|
| 209 |
-
|
| 210 |
-
except Exception as e:
|
| 211 |
-
st.error(f"Error getting all configs: {e}")
|
| 212 |
-
return {}
|
| 213 |
-
|
| 214 |
-
def create_config_from_ui(
|
| 215 |
-
self,
|
| 216 |
-
brand: str,
|
| 217 |
-
campaign_name: str,
|
| 218 |
-
campaign_type: str,
|
| 219 |
-
num_stages: int,
|
| 220 |
-
campaign_instructions: Optional[str],
|
| 221 |
-
stages_config: Dict[int, Dict]
|
| 222 |
-
) -> Dict:
|
| 223 |
-
"""
|
| 224 |
-
Create a configuration dictionary from UI inputs.
|
| 225 |
-
|
| 226 |
-
Args:
|
| 227 |
-
brand: Brand name
|
| 228 |
-
campaign_name: Campaign name
|
| 229 |
-
campaign_type: Campaign type
|
| 230 |
-
num_stages: Number of stages
|
| 231 |
-
campaign_instructions: Campaign-wide instructions (optional)
|
| 232 |
-
stages_config: Dictionary mapping stage numbers to stage configurations
|
| 233 |
-
|
| 234 |
-
Returns:
|
| 235 |
-
dict: Complete configuration dictionary
|
| 236 |
-
"""
|
| 237 |
-
config = {
|
| 238 |
-
"brand": brand,
|
| 239 |
-
"campaign_type": campaign_type,
|
| 240 |
-
"campaign_name": campaign_name,
|
| 241 |
-
"campaign_instructions": campaign_instructions,
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
# Add stage configurations
|
| 245 |
-
for stage_num in range(1, num_stages + 1):
|
| 246 |
-
if stage_num in stages_config:
|
| 247 |
-
config[str(stage_num)] = stages_config[stage_num]
|
| 248 |
-
|
| 249 |
-
return config
|
| 250 |
-
|
| 251 |
-
def extract_stage_config(self, config: Dict, stage: int) -> Dict:
|
| 252 |
-
"""
|
| 253 |
-
Extract configuration for a specific stage.
|
| 254 |
-
|
| 255 |
-
Args:
|
| 256 |
-
config: Complete configuration dictionary
|
| 257 |
-
stage: Stage number
|
| 258 |
-
|
| 259 |
-
Returns:
|
| 260 |
-
dict: Stage configuration
|
| 261 |
-
"""
|
| 262 |
-
return config.get(str(stage), {})
|
| 263 |
-
|
| 264 |
-
def validate_config(self, config: Dict) -> tuple[bool, str]:
|
| 265 |
-
"""
|
| 266 |
-
Validate configuration structure and required fields.
|
| 267 |
-
|
| 268 |
-
Args:
|
| 269 |
-
config: Configuration dictionary
|
| 270 |
-
|
| 271 |
-
Returns:
|
| 272 |
-
tuple: (is_valid, error_message)
|
| 273 |
-
"""
|
| 274 |
-
required_fields = ["brand", "campaign_name"]
|
| 275 |
-
|
| 276 |
-
# Check required top-level fields
|
| 277 |
-
for field in required_fields:
|
| 278 |
-
if field not in config:
|
| 279 |
-
return False, f"Missing required field: {field}"
|
| 280 |
-
|
| 281 |
-
# Check if at least stage 1 exists
|
| 282 |
-
if "1" not in config:
|
| 283 |
-
return False, "Configuration must have at least stage 1"
|
| 284 |
-
|
| 285 |
-
# Validate stage 1 config
|
| 286 |
-
stage1 = config["1"]
|
| 287 |
-
required_stage_fields = ["stage", "model"]
|
| 288 |
-
|
| 289 |
-
for field in required_stage_fields:
|
| 290 |
-
if field not in stage1:
|
| 291 |
-
return False, f"Stage 1 missing required field: {field}"
|
| 292 |
-
|
| 293 |
-
return True, ""
|
| 294 |
-
|
| 295 |
-
def duplicate_config(self, brand: str, source_name: str, new_name: str) -> bool:
|
| 296 |
-
"""
|
| 297 |
-
Duplicate an existing configuration with a new name.
|
| 298 |
-
The new config will start at version 1.
|
| 299 |
-
|
| 300 |
-
Args:
|
| 301 |
-
brand: Brand name
|
| 302 |
-
source_name: Source configuration name
|
| 303 |
-
new_name: New configuration name
|
| 304 |
-
|
| 305 |
-
Returns:
|
| 306 |
-
bool: True if duplicated successfully, False otherwise
|
| 307 |
-
"""
|
| 308 |
-
self._ensure_db_manager()
|
| 309 |
-
|
| 310 |
-
try:
|
| 311 |
-
# Load source config
|
| 312 |
-
source_config = self.load_custom_config(brand, source_name)
|
| 313 |
-
|
| 314 |
-
if source_config is None:
|
| 315 |
-
st.error(f"Source config '{source_name}' not found")
|
| 316 |
-
return False
|
| 317 |
-
|
| 318 |
-
# Update campaign name in config to match new config name
|
| 319 |
-
source_config['campaign_name'] = new_name
|
| 320 |
-
|
| 321 |
-
# Save as new config (will create version 1)
|
| 322 |
-
return self.save_custom_config(brand, new_name, source_config)
|
| 323 |
-
|
| 324 |
-
except Exception as e:
|
| 325 |
-
st.error(f"Error duplicating config: {e}")
|
| 326 |
-
return False
|
| 327 |
-
|
| 328 |
-
def get_config_version(self, brand: str, config_name: str) -> int:
|
| 329 |
-
"""
|
| 330 |
-
Get the latest version number for a configuration.
|
| 331 |
-
|
| 332 |
-
Args:
|
| 333 |
-
brand: Brand name
|
| 334 |
-
config_name: Configuration name
|
| 335 |
-
|
| 336 |
-
Returns:
|
| 337 |
-
int: Latest version number (0 if config doesn't exist)
|
| 338 |
-
"""
|
| 339 |
-
self._ensure_db_manager()
|
| 340 |
-
|
| 341 |
-
try:
|
| 342 |
-
return self.db_manager.get_config_version(config_name, brand)
|
| 343 |
-
|
| 344 |
-
except Exception as e:
|
| 345 |
-
st.error(f"Error getting config version: {e}")
|
| 346 |
-
return 0
|
| 347 |
-
|
| 348 |
-
def get_config_history(self, brand: str, config_name: str) -> List[int]:
|
| 349 |
-
"""
|
| 350 |
-
Get version history for a configuration.
|
| 351 |
-
|
| 352 |
-
Args:
|
| 353 |
-
brand: Brand name
|
| 354 |
-
config_name: Configuration name
|
| 355 |
-
|
| 356 |
-
Returns:
|
| 357 |
-
list: List of version numbers
|
| 358 |
-
"""
|
| 359 |
-
# Note: This would require additional implementation in db_manager
|
| 360 |
-
# For now, return empty list
|
| 361 |
-
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/utils/data_loader.py
DELETED
|
@@ -1,230 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Data loading module for the AI Messaging System Visualization Tool.
|
| 3 |
-
|
| 4 |
-
Handles data loading from local CSV files only.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import sys
|
| 8 |
-
import pandas as pd
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
from typing import Optional
|
| 11 |
-
import streamlit as st
|
| 12 |
-
|
| 13 |
-
# Add parent directory to path to import from ai_messaging_system_v2
|
| 14 |
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 15 |
-
|
| 16 |
-
try:
|
| 17 |
-
from ai_messaging_system_v2.Messaging_system.Permes import Permes
|
| 18 |
-
except ImportError as e:
|
| 19 |
-
st.error(f"Error importing AI Messaging System modules: {e}")
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
class DataLoader:
|
| 23 |
-
"""
|
| 24 |
-
Handles all data loading operations for the visualization tool.
|
| 25 |
-
"""
|
| 26 |
-
|
| 27 |
-
def __init__(self):
|
| 28 |
-
"""Initialize DataLoader."""
|
| 29 |
-
self.base_path = Path(__file__).parent.parent
|
| 30 |
-
self.ui_users_path = self.base_path / "data" / "UI_users"
|
| 31 |
-
self.ui_output_path = Path(__file__).parent.parent.parent / "ai_messaging_system_v2" / "Data" / "ui_output"
|
| 32 |
-
|
| 33 |
-
# Create directories if they don't exist
|
| 34 |
-
self.ui_users_path.mkdir(parents=True, exist_ok=True)
|
| 35 |
-
self.ui_output_path.mkdir(parents=True, exist_ok=True)
|
| 36 |
-
|
| 37 |
-
def load_brand_users(self, brand: str) -> Optional[pd.DataFrame]:
|
| 38 |
-
"""
|
| 39 |
-
Load all users for a brand from UI_users directory.
|
| 40 |
-
|
| 41 |
-
Args:
|
| 42 |
-
brand: Brand name
|
| 43 |
-
|
| 44 |
-
Returns:
|
| 45 |
-
pd.DataFrame or None: All users for the brand or None if file doesn't exist
|
| 46 |
-
"""
|
| 47 |
-
file_path = self.ui_users_path / f"{brand}_users.csv"
|
| 48 |
-
|
| 49 |
-
if not file_path.exists():
|
| 50 |
-
st.error(f"User file not found: {brand}_users.csv")
|
| 51 |
-
return None
|
| 52 |
-
|
| 53 |
-
try:
|
| 54 |
-
users_df = pd.read_csv(file_path, encoding='utf-8-sig')
|
| 55 |
-
return users_df
|
| 56 |
-
except Exception as e:
|
| 57 |
-
st.error(f"Error loading users from {brand}_users.csv: {e}")
|
| 58 |
-
return None
|
| 59 |
-
|
| 60 |
-
def sample_users_randomly(self, brand: str, sample_size: int) -> Optional[pd.DataFrame]:
|
| 61 |
-
"""
|
| 62 |
-
Randomly sample users from the brand's user file.
|
| 63 |
-
|
| 64 |
-
Args:
|
| 65 |
-
brand: Brand name
|
| 66 |
-
sample_size: Number of users to sample (1-25)
|
| 67 |
-
|
| 68 |
-
Returns:
|
| 69 |
-
pd.DataFrame or None: Sampled users or None if error
|
| 70 |
-
"""
|
| 71 |
-
try:
|
| 72 |
-
# Load all users for the brand
|
| 73 |
-
all_users = self.load_brand_users(brand)
|
| 74 |
-
|
| 75 |
-
if all_users is None or len(all_users) == 0:
|
| 76 |
-
st.error(f"No users available for {brand}")
|
| 77 |
-
return None
|
| 78 |
-
|
| 79 |
-
# Validate sample size
|
| 80 |
-
if sample_size < 1:
|
| 81 |
-
sample_size = 1
|
| 82 |
-
if sample_size > min(25, len(all_users)):
|
| 83 |
-
sample_size = min(25, len(all_users))
|
| 84 |
-
|
| 85 |
-
# Randomly sample
|
| 86 |
-
sampled_users = all_users.sample(n=sample_size, random_state=None)
|
| 87 |
-
|
| 88 |
-
return sampled_users
|
| 89 |
-
|
| 90 |
-
except Exception as e:
|
| 91 |
-
st.error(f"Error sampling users: {e}")
|
| 92 |
-
return None
|
| 93 |
-
|
| 94 |
-
def has_brand_users(self, brand: str) -> bool:
|
| 95 |
-
"""
|
| 96 |
-
Check if user file exists for a brand.
|
| 97 |
-
|
| 98 |
-
Args:
|
| 99 |
-
brand: Brand name
|
| 100 |
-
|
| 101 |
-
Returns:
|
| 102 |
-
bool: True if file exists, False otherwise
|
| 103 |
-
"""
|
| 104 |
-
file_path = self.ui_users_path / f"{brand}_users.csv"
|
| 105 |
-
return file_path.exists()
|
| 106 |
-
|
| 107 |
-
def get_brand_user_count(self, brand: str) -> int:
|
| 108 |
-
"""
|
| 109 |
-
Get the total number of users available for a brand.
|
| 110 |
-
|
| 111 |
-
Args:
|
| 112 |
-
brand: Brand name
|
| 113 |
-
|
| 114 |
-
Returns:
|
| 115 |
-
int: Number of users available
|
| 116 |
-
"""
|
| 117 |
-
users_df = self.load_brand_users(brand)
|
| 118 |
-
if users_df is not None:
|
| 119 |
-
return len(users_df)
|
| 120 |
-
return 0
|
| 121 |
-
|
| 122 |
-
def load_generated_messages(self) -> Optional[pd.DataFrame]:
|
| 123 |
-
"""
|
| 124 |
-
Load generated messages from UI output folder.
|
| 125 |
-
|
| 126 |
-
Returns:
|
| 127 |
-
pd.DataFrame or None: Generated messages or None if file doesn't exist
|
| 128 |
-
"""
|
| 129 |
-
messages_file = self.ui_output_path / "messages.csv"
|
| 130 |
-
|
| 131 |
-
if messages_file.exists():
|
| 132 |
-
return pd.read_csv(messages_file, encoding='utf-8-sig')
|
| 133 |
-
return None
|
| 134 |
-
|
| 135 |
-
def clear_ui_output(self):
|
| 136 |
-
"""
|
| 137 |
-
Clear UI output files (messages and costs).
|
| 138 |
-
"""
|
| 139 |
-
try:
|
| 140 |
-
Permes.clear_ui_output()
|
| 141 |
-
st.success("UI output cleared successfully!")
|
| 142 |
-
except Exception as e:
|
| 143 |
-
st.error(f"Error clearing UI output: {e}")
|
| 144 |
-
|
| 145 |
-
def get_ui_output_path(self) -> Path:
|
| 146 |
-
"""
|
| 147 |
-
Get path to UI output directory.
|
| 148 |
-
|
| 149 |
-
Returns:
|
| 150 |
-
Path: UI output directory path
|
| 151 |
-
"""
|
| 152 |
-
return self.ui_output_path
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
def get_message_stats(self) -> dict:
|
| 156 |
-
"""
|
| 157 |
-
Get statistics about generated messages.
|
| 158 |
-
|
| 159 |
-
Returns:
|
| 160 |
-
dict: Statistics including total messages, stages, users, etc.
|
| 161 |
-
"""
|
| 162 |
-
messages_df = self.load_generated_messages()
|
| 163 |
-
|
| 164 |
-
if messages_df is None or len(messages_df) == 0:
|
| 165 |
-
return {
|
| 166 |
-
"total_messages": 0,
|
| 167 |
-
"total_users": 0,
|
| 168 |
-
"total_stages": 0,
|
| 169 |
-
"campaigns": []
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
stats = {
|
| 173 |
-
"total_messages": len(messages_df),
|
| 174 |
-
"total_users": messages_df['user_id'].nunique() if 'user_id' in messages_df.columns else 0,
|
| 175 |
-
"total_stages": messages_df['stage'].nunique() if 'stage' in messages_df.columns else 0,
|
| 176 |
-
"campaigns": messages_df['campaign_name'].unique().tolist() if 'campaign_name' in messages_df.columns else []
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
return stats
|
| 180 |
-
|
| 181 |
-
def filter_messages_by_stage(self, messages_df: pd.DataFrame, stage: int) -> pd.DataFrame:
|
| 182 |
-
"""
|
| 183 |
-
Filter messages by stage number.
|
| 184 |
-
|
| 185 |
-
Args:
|
| 186 |
-
messages_df: Messages DataFrame
|
| 187 |
-
stage: Stage number
|
| 188 |
-
|
| 189 |
-
Returns:
|
| 190 |
-
pd.DataFrame: Filtered messages
|
| 191 |
-
"""
|
| 192 |
-
if 'stage' in messages_df.columns:
|
| 193 |
-
return messages_df[messages_df['stage'] == stage]
|
| 194 |
-
return pd.DataFrame()
|
| 195 |
-
|
| 196 |
-
def filter_messages_by_user(self, messages_df: pd.DataFrame, user_id: int) -> pd.DataFrame:
|
| 197 |
-
"""
|
| 198 |
-
Filter messages by user ID.
|
| 199 |
-
|
| 200 |
-
Args:
|
| 201 |
-
messages_df: Messages DataFrame
|
| 202 |
-
user_id: User ID
|
| 203 |
-
|
| 204 |
-
Returns:
|
| 205 |
-
pd.DataFrame: Filtered messages
|
| 206 |
-
"""
|
| 207 |
-
if 'user_id' in messages_df.columns:
|
| 208 |
-
return messages_df[messages_df['user_id'] == user_id]
|
| 209 |
-
return pd.DataFrame()
|
| 210 |
-
|
| 211 |
-
def search_messages(self, messages_df: pd.DataFrame, search_term: str) -> pd.DataFrame:
|
| 212 |
-
"""
|
| 213 |
-
Search messages by keyword in message content.
|
| 214 |
-
|
| 215 |
-
Args:
|
| 216 |
-
messages_df: Messages DataFrame
|
| 217 |
-
search_term: Search keyword
|
| 218 |
-
|
| 219 |
-
Returns:
|
| 220 |
-
pd.DataFrame: Filtered messages containing the search term
|
| 221 |
-
"""
|
| 222 |
-
if len(search_term) == 0:
|
| 223 |
-
return messages_df
|
| 224 |
-
|
| 225 |
-
# Search in message column (which contains JSON)
|
| 226 |
-
if 'message' in messages_df.columns:
|
| 227 |
-
mask = messages_df['message'].astype(str).str.contains(search_term, case=False, na=False)
|
| 228 |
-
return messages_df[mask]
|
| 229 |
-
|
| 230 |
-
return pd.DataFrame()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/utils/db_manager.py
DELETED
|
@@ -1,700 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Database Manager for UI Operations
|
| 3 |
-
|
| 4 |
-
Handles all Snowflake operations for the UI including:
|
| 5 |
-
- Campaign configuration management
|
| 6 |
-
- Feedback storage and retrieval
|
| 7 |
-
- Experiment metadata tracking
|
| 8 |
-
- Historical analytics queries
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
import pandas as pd
|
| 12 |
-
import json
|
| 13 |
-
from datetime import datetime, timezone
|
| 14 |
-
from typing import Dict, List, Optional, Tuple
|
| 15 |
-
from snowflake.snowpark import Session
|
| 16 |
-
import streamlit as st
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class UIDatabaseManager:
|
| 20 |
-
"""
|
| 21 |
-
Manages all Snowflake database operations for the UI.
|
| 22 |
-
"""
|
| 23 |
-
|
| 24 |
-
# Table names
|
| 25 |
-
CONFIG_TABLE = "MESSAGING_SYSTEM_V2.UI.CAMPAIGN_CONFIGURATIONS"
|
| 26 |
-
FEEDBACK_TABLE = "MESSAGING_SYSTEM_V2.UI.FEEDBACKS"
|
| 27 |
-
METADATA_TABLE = "MESSAGING_SYSTEM_V2.UI.EXPERIMENT_METADATA"
|
| 28 |
-
|
| 29 |
-
def __init__(self, session: Session):
|
| 30 |
-
"""
|
| 31 |
-
Initialize database manager with Snowflake session.
|
| 32 |
-
|
| 33 |
-
Args:
|
| 34 |
-
session: Active Snowflake Snowpark session
|
| 35 |
-
"""
|
| 36 |
-
self.session = session
|
| 37 |
-
|
| 38 |
-
# ========== CONFIG OPERATIONS ==========
|
| 39 |
-
|
| 40 |
-
def load_configs(self, brand: str) -> Dict[str, Dict]:
|
| 41 |
-
"""
|
| 42 |
-
Load all configurations for a brand from Snowflake.
|
| 43 |
-
Returns the latest version of each config.
|
| 44 |
-
|
| 45 |
-
Args:
|
| 46 |
-
brand: Brand name
|
| 47 |
-
|
| 48 |
-
Returns:
|
| 49 |
-
dict: Dictionary mapping config names to config data
|
| 50 |
-
"""
|
| 51 |
-
try:
|
| 52 |
-
# Get latest version of each config for the brand
|
| 53 |
-
query = f"""
|
| 54 |
-
WITH RankedConfigs AS (
|
| 55 |
-
SELECT
|
| 56 |
-
CONFIG_NAME,
|
| 57 |
-
CONFIG_FILE,
|
| 58 |
-
CONFIG_VERSION,
|
| 59 |
-
BRAND,
|
| 60 |
-
ROW_NUMBER() OVER (PARTITION BY CONFIG_NAME ORDER BY CONFIG_VERSION DESC) as rn
|
| 61 |
-
FROM {self.CONFIG_TABLE}
|
| 62 |
-
WHERE BRAND = '{brand}'
|
| 63 |
-
)
|
| 64 |
-
SELECT CONFIG_NAME, CONFIG_FILE, CONFIG_VERSION
|
| 65 |
-
FROM RankedConfigs
|
| 66 |
-
WHERE rn = 1
|
| 67 |
-
ORDER BY CONFIG_NAME
|
| 68 |
-
"""
|
| 69 |
-
|
| 70 |
-
result_df = self.session.sql(query).to_pandas()
|
| 71 |
-
|
| 72 |
-
configs = {}
|
| 73 |
-
for _, row in result_df.iterrows():
|
| 74 |
-
config_name = row['CONFIG_NAME']
|
| 75 |
-
config_file = row['CONFIG_FILE']
|
| 76 |
-
|
| 77 |
-
# Parse JSON from VARIANT column
|
| 78 |
-
if isinstance(config_file, str):
|
| 79 |
-
config_data = json.loads(config_file)
|
| 80 |
-
else:
|
| 81 |
-
config_data = config_file
|
| 82 |
-
|
| 83 |
-
configs[config_name] = config_data
|
| 84 |
-
|
| 85 |
-
print(f"✅ Loaded {len(configs)} configs for {brand} from Snowflake")
|
| 86 |
-
return configs
|
| 87 |
-
|
| 88 |
-
except Exception as e:
|
| 89 |
-
st.error(f"Error loading configs from Snowflake: {e}")
|
| 90 |
-
return {}
|
| 91 |
-
|
| 92 |
-
def save_config(self, config_name: str, config_data: Dict, brand: str) -> bool:
|
| 93 |
-
"""
|
| 94 |
-
Save configuration to Snowflake with automatic versioning.
|
| 95 |
-
If config exists, increments CONFIG_VERSION automatically.
|
| 96 |
-
|
| 97 |
-
Args:
|
| 98 |
-
config_name: Configuration name
|
| 99 |
-
config_data: Configuration dictionary
|
| 100 |
-
brand: Brand name
|
| 101 |
-
|
| 102 |
-
Returns:
|
| 103 |
-
bool: True if saved successfully
|
| 104 |
-
"""
|
| 105 |
-
try:
|
| 106 |
-
# Get current max version for this config
|
| 107 |
-
query = f"""
|
| 108 |
-
SELECT MAX(CONFIG_VERSION) as MAX_VERSION
|
| 109 |
-
FROM {self.CONFIG_TABLE}
|
| 110 |
-
WHERE CONFIG_NAME = '{config_name}' AND BRAND = '{brand}'
|
| 111 |
-
"""
|
| 112 |
-
|
| 113 |
-
result = self.session.sql(query).to_pandas()
|
| 114 |
-
|
| 115 |
-
if len(result) > 0 and pd.notna(result['MAX_VERSION'].iloc[0]):
|
| 116 |
-
new_version = int(result['MAX_VERSION'].iloc[0]) + 1
|
| 117 |
-
else:
|
| 118 |
-
new_version = 1
|
| 119 |
-
|
| 120 |
-
# Convert config to JSON string
|
| 121 |
-
config_json = json.dumps(config_data, ensure_ascii=False)
|
| 122 |
-
|
| 123 |
-
# Create dataframe with the config data
|
| 124 |
-
df = pd.DataFrame([{
|
| 125 |
-
'CONFIG_NAME': config_name,
|
| 126 |
-
'CONFIG_FILE': config_json,
|
| 127 |
-
'CONFIG_VERSION': new_version,
|
| 128 |
-
'BRAND': brand
|
| 129 |
-
}])
|
| 130 |
-
|
| 131 |
-
# Write to Snowflake using write_pandas (safer than raw SQL for JSON)
|
| 132 |
-
self.session.write_pandas(
|
| 133 |
-
df=df,
|
| 134 |
-
table_name=self.CONFIG_TABLE.split('.')[-1],
|
| 135 |
-
database=self.CONFIG_TABLE.split('.')[0],
|
| 136 |
-
schema=self.CONFIG_TABLE.split('.')[1],
|
| 137 |
-
auto_create_table=True,
|
| 138 |
-
overwrite=False
|
| 139 |
-
)
|
| 140 |
-
|
| 141 |
-
print(f"✅ Saved config '{config_name}' version {new_version} for {brand}")
|
| 142 |
-
return True
|
| 143 |
-
|
| 144 |
-
except Exception as e:
|
| 145 |
-
st.error(f"Error saving config to Snowflake: {e}")
|
| 146 |
-
import traceback
|
| 147 |
-
st.error(f"Traceback: {traceback.format_exc()}")
|
| 148 |
-
return False
|
| 149 |
-
|
| 150 |
-
def get_config_version(self, config_name: str, brand: str) -> int:
|
| 151 |
-
"""
|
| 152 |
-
Get the latest version number for a configuration.
|
| 153 |
-
|
| 154 |
-
Args:
|
| 155 |
-
config_name: Configuration name
|
| 156 |
-
brand: Brand name
|
| 157 |
-
|
| 158 |
-
Returns:
|
| 159 |
-
int: Latest version number (0 if config doesn't exist)
|
| 160 |
-
"""
|
| 161 |
-
try:
|
| 162 |
-
query = f"""
|
| 163 |
-
SELECT MAX(CONFIG_VERSION) as MAX_VERSION
|
| 164 |
-
FROM {self.CONFIG_TABLE}
|
| 165 |
-
WHERE CONFIG_NAME = '{config_name}' AND BRAND = '{brand}'
|
| 166 |
-
"""
|
| 167 |
-
|
| 168 |
-
result = self.session.sql(query).to_pandas()
|
| 169 |
-
|
| 170 |
-
if len(result) > 0 and pd.notna(result['MAX_VERSION'].iloc[0]):
|
| 171 |
-
return int(result['MAX_VERSION'].iloc[0])
|
| 172 |
-
return 0
|
| 173 |
-
|
| 174 |
-
except Exception as e:
|
| 175 |
-
st.error(f"Error getting config version: {e}")
|
| 176 |
-
return 0
|
| 177 |
-
|
| 178 |
-
# ========== EXPERIMENT METADATA OPERATIONS ==========
|
| 179 |
-
|
| 180 |
-
def store_experiment_metadata(self, metadata: Dict) -> bool:
|
| 181 |
-
"""
|
| 182 |
-
Store experiment metadata for a single stage.
|
| 183 |
-
|
| 184 |
-
Args:
|
| 185 |
-
metadata: Dictionary containing:
|
| 186 |
-
- experiment_id: Unique experiment identifier
|
| 187 |
-
- config_name: Configuration name used
|
| 188 |
-
- brand: Brand name
|
| 189 |
-
- campaign_name: Campaign name
|
| 190 |
-
- stage: Stage number
|
| 191 |
-
- llm_model: LLM model used
|
| 192 |
-
- total_messages: Number of messages generated
|
| 193 |
-
- total_users: Number of users
|
| 194 |
-
- platform: Platform (push/app)
|
| 195 |
-
- personalization: Boolean
|
| 196 |
-
- involve_recsys: Boolean
|
| 197 |
-
- recsys_contents: JSON string of content types
|
| 198 |
-
- segment_info: Segment description
|
| 199 |
-
- campaign_instructions: Campaign instructions
|
| 200 |
-
- per_message_instructions: Per-message instructions
|
| 201 |
-
- timestamp: Timestamp
|
| 202 |
-
|
| 203 |
-
Returns:
|
| 204 |
-
bool: True if stored successfully
|
| 205 |
-
"""
|
| 206 |
-
try:
|
| 207 |
-
# Get timestamp and ensure it's a datetime object
|
| 208 |
-
timestamp_value = datetime.now(timezone.utc)
|
| 209 |
-
|
| 210 |
-
# Create dataframe from metadata
|
| 211 |
-
df = pd.DataFrame([{
|
| 212 |
-
'EXPERIMENT_ID': metadata.get('experiment_id'),
|
| 213 |
-
'CONFIG_NAME': metadata.get('config_name'),
|
| 214 |
-
'BRAND': metadata.get('brand'),
|
| 215 |
-
'CAMPAIGN_NAME': metadata.get('campaign_name'),
|
| 216 |
-
'STAGE': metadata.get('stage'),
|
| 217 |
-
'LLM_MODEL': metadata.get('llm_model'),
|
| 218 |
-
'TOTAL_MESSAGES': metadata.get('total_messages'),
|
| 219 |
-
'TOTAL_USERS': metadata.get('total_users'),
|
| 220 |
-
'PLATFORM': metadata.get('platform', 'push'),
|
| 221 |
-
'PERSONALIZATION': metadata.get('personalization', True),
|
| 222 |
-
'INVOLVE_RECSYS': metadata.get('involve_recsys', True),
|
| 223 |
-
'RECSYS_CONTENTS': json.dumps(metadata.get('recsys_contents', [])),
|
| 224 |
-
'SEGMENT_INFO': metadata.get('segment_info'),
|
| 225 |
-
'CAMPAIGN_INSTRUCTIONS': metadata.get('campaign_instructions'),
|
| 226 |
-
'PER_MESSAGE_INSTRUCTIONS': metadata.get('per_message_instructions'),
|
| 227 |
-
'TIMESTAMP': timestamp_value
|
| 228 |
-
}])
|
| 229 |
-
|
| 230 |
-
# Ensure TIMESTAMP column is datetime64
|
| 231 |
-
df['TIMESTAMP'] = pd.to_datetime(df['TIMESTAMP'], utc=True,errors="coerce")
|
| 232 |
-
|
| 233 |
-
# Write to Snowflake
|
| 234 |
-
self.session.write_pandas(
|
| 235 |
-
df=df,
|
| 236 |
-
table_name=self.METADATA_TABLE.split('.')[-1],
|
| 237 |
-
database=self.METADATA_TABLE.split('.')[0],
|
| 238 |
-
schema=self.METADATA_TABLE.split('.')[1],
|
| 239 |
-
auto_create_table=True,
|
| 240 |
-
overwrite=False,
|
| 241 |
-
use_logical_type=True
|
| 242 |
-
)
|
| 243 |
-
|
| 244 |
-
print(f"✅ Stored metadata for experiment {metadata.get('experiment_id')} stage {metadata.get('stage')}")
|
| 245 |
-
return True
|
| 246 |
-
|
| 247 |
-
except Exception as e:
|
| 248 |
-
st.error(f"Error storing experiment metadata: {e}")
|
| 249 |
-
return False
|
| 250 |
-
|
| 251 |
-
def store_experiment_metadata_batch(self, metadata_list: List[Dict]) -> bool:
|
| 252 |
-
"""
|
| 253 |
-
Store multiple experiment metadata records (for multi-stage experiments).
|
| 254 |
-
|
| 255 |
-
Args:
|
| 256 |
-
metadata_list: List of metadata dictionaries
|
| 257 |
-
|
| 258 |
-
Returns:
|
| 259 |
-
bool: True if all stored successfully
|
| 260 |
-
"""
|
| 261 |
-
try:
|
| 262 |
-
for metadata in metadata_list:
|
| 263 |
-
if not self.store_experiment_metadata(metadata):
|
| 264 |
-
return False
|
| 265 |
-
return True
|
| 266 |
-
except Exception as e:
|
| 267 |
-
st.error(f"Error storing batch metadata: {e}")
|
| 268 |
-
return False
|
| 269 |
-
|
| 270 |
-
# ========== FEEDBACK OPERATIONS ==========
|
| 271 |
-
|
| 272 |
-
def store_feedback_batch(self, feedback_list: List[Dict]) -> bool:
|
| 273 |
-
"""
|
| 274 |
-
Store a batch of feedback records to Snowflake.
|
| 275 |
-
|
| 276 |
-
Args:
|
| 277 |
-
feedback_list: List of feedback dictionaries containing:
|
| 278 |
-
- config_name
|
| 279 |
-
- brand
|
| 280 |
-
- experiment_id
|
| 281 |
-
- user_id
|
| 282 |
-
- stage
|
| 283 |
-
- feedback_type
|
| 284 |
-
- rejection_reason
|
| 285 |
-
- rejection_text
|
| 286 |
-
- campaign_name
|
| 287 |
-
- message_header
|
| 288 |
-
- message_body
|
| 289 |
-
- timestamp
|
| 290 |
-
|
| 291 |
-
Returns:
|
| 292 |
-
bool: True if stored successfully
|
| 293 |
-
"""
|
| 294 |
-
try:
|
| 295 |
-
if not feedback_list or len(feedback_list) == 0:
|
| 296 |
-
print("⚠️ No feedback to store")
|
| 297 |
-
return True
|
| 298 |
-
|
| 299 |
-
# Create dataframe from feedback list with proper timestamp handling
|
| 300 |
-
records = []
|
| 301 |
-
for fb in feedback_list:
|
| 302 |
-
# Ensure timestamp is a datetime object
|
| 303 |
-
timestamp_value = datetime.now(timezone.utc)
|
| 304 |
-
|
| 305 |
-
records.append({
|
| 306 |
-
'CONFIG_NAME': fb.get('config_name'),
|
| 307 |
-
'BRAND': fb.get('brand'),
|
| 308 |
-
'EXPERIMENT_ID': fb.get('experiment_id'),
|
| 309 |
-
'USER_ID': fb.get('user_id'),
|
| 310 |
-
'STAGE': fb.get('stage'),
|
| 311 |
-
'FEEDBACK_TYPE': fb.get('feedback_type'),
|
| 312 |
-
'REJECTION_REASON': fb.get('rejection_reason'),
|
| 313 |
-
'REJECTION_TEXT': fb.get('rejection_text'),
|
| 314 |
-
'CAMPAIGN_NAME': fb.get('campaign_name'),
|
| 315 |
-
'MESSAGE_HEADER': fb.get('message_header'),
|
| 316 |
-
'MESSAGE_BODY': fb.get('message_body'),
|
| 317 |
-
'TIMESTAMP': timestamp_value
|
| 318 |
-
})
|
| 319 |
-
|
| 320 |
-
df = pd.DataFrame(records)
|
| 321 |
-
|
| 322 |
-
# Ensure TIMESTAMP column is datetime64
|
| 323 |
-
df['TIMESTAMP'] = pd.to_datetime(df['TIMESTAMP'], utc=True, errors="coerce")
|
| 324 |
-
|
| 325 |
-
# Write to Snowflake
|
| 326 |
-
self.session.write_pandas(
|
| 327 |
-
df=df,
|
| 328 |
-
table_name=self.FEEDBACK_TABLE.split('.')[-1],
|
| 329 |
-
database=self.FEEDBACK_TABLE.split('.')[0],
|
| 330 |
-
schema=self.FEEDBACK_TABLE.split('.')[1],
|
| 331 |
-
auto_create_table=True,
|
| 332 |
-
overwrite=False,
|
| 333 |
-
use_logical_type=True
|
| 334 |
-
)
|
| 335 |
-
|
| 336 |
-
print(f"✅ Stored {len(feedback_list)} feedback records to Snowflake")
|
| 337 |
-
return True
|
| 338 |
-
|
| 339 |
-
except Exception as e:
|
| 340 |
-
st.error(f"Error storing feedback batch: {e}")
|
| 341 |
-
return False
|
| 342 |
-
|
| 343 |
-
def load_historical_feedbacks(self, brand: Optional[str] = None,
|
| 344 |
-
start_date: Optional[datetime] = None,
|
| 345 |
-
end_date: Optional[datetime] = None) -> pd.DataFrame:
|
| 346 |
-
"""
|
| 347 |
-
Load historical feedback records from Snowflake with optional filters.
|
| 348 |
-
|
| 349 |
-
Args:
|
| 350 |
-
brand: Optional brand filter
|
| 351 |
-
start_date: Optional start date filter
|
| 352 |
-
end_date: Optional end date filter
|
| 353 |
-
|
| 354 |
-
Returns:
|
| 355 |
-
pd.DataFrame: Feedback records
|
| 356 |
-
"""
|
| 357 |
-
try:
|
| 358 |
-
where_clauses = []
|
| 359 |
-
|
| 360 |
-
if brand:
|
| 361 |
-
where_clauses.append(f"BRAND = '{brand}'")
|
| 362 |
-
|
| 363 |
-
if start_date:
|
| 364 |
-
where_clauses.append(f"TIMESTAMP >= '{start_date.isoformat()}'")
|
| 365 |
-
|
| 366 |
-
if end_date:
|
| 367 |
-
where_clauses.append(f"TIMESTAMP <= '{end_date.isoformat()}'")
|
| 368 |
-
|
| 369 |
-
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
| 370 |
-
|
| 371 |
-
query = f"""
|
| 372 |
-
SELECT *
|
| 373 |
-
FROM {self.FEEDBACK_TABLE}
|
| 374 |
-
WHERE {where_sql}
|
| 375 |
-
ORDER BY TIMESTAMP DESC
|
| 376 |
-
"""
|
| 377 |
-
|
| 378 |
-
result_df = self.session.sql(query).to_pandas()
|
| 379 |
-
result_df.columns = result_df.columns.str.lower()
|
| 380 |
-
|
| 381 |
-
print(f"✅ Loaded {len(result_df)} historical feedback records")
|
| 382 |
-
return result_df
|
| 383 |
-
|
| 384 |
-
except Exception as e:
|
| 385 |
-
st.error(f"Error loading historical feedbacks: {e}")
|
| 386 |
-
return pd.DataFrame()
|
| 387 |
-
|
| 388 |
-
# ========== HISTORICAL ANALYTICS OPERATIONS ==========
|
| 389 |
-
|
| 390 |
-
def load_historical_experiments(self, brand: Optional[str] = None,
|
| 391 |
-
start_date: Optional[datetime] = None,
|
| 392 |
-
end_date: Optional[datetime] = None) -> pd.DataFrame:
|
| 393 |
-
"""
|
| 394 |
-
Load historical experiment metadata from Snowflake.
|
| 395 |
-
|
| 396 |
-
Args:
|
| 397 |
-
brand: Optional brand filter
|
| 398 |
-
start_date: Optional start date filter
|
| 399 |
-
end_date: Optional end date filter
|
| 400 |
-
|
| 401 |
-
Returns:
|
| 402 |
-
pd.DataFrame: Experiment metadata records
|
| 403 |
-
"""
|
| 404 |
-
try:
|
| 405 |
-
where_clauses = []
|
| 406 |
-
|
| 407 |
-
if brand:
|
| 408 |
-
where_clauses.append(f"BRAND = '{brand}'")
|
| 409 |
-
|
| 410 |
-
if start_date:
|
| 411 |
-
where_clauses.append(f"TIMESTAMP >= '{start_date.isoformat()}'")
|
| 412 |
-
|
| 413 |
-
if end_date:
|
| 414 |
-
where_clauses.append(f"TIMESTAMP <= '{end_date.isoformat()}'")
|
| 415 |
-
|
| 416 |
-
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
| 417 |
-
|
| 418 |
-
query = f"""
|
| 419 |
-
SELECT *
|
| 420 |
-
FROM {self.METADATA_TABLE}
|
| 421 |
-
WHERE {where_sql}
|
| 422 |
-
ORDER BY TIMESTAMP DESC, EXPERIMENT_ID, STAGE
|
| 423 |
-
"""
|
| 424 |
-
|
| 425 |
-
result_df = self.session.sql(query).to_pandas()
|
| 426 |
-
result_df.columns = result_df.columns.str.lower()
|
| 427 |
-
|
| 428 |
-
print(f"✅ Loaded {len(result_df)} historical experiment records")
|
| 429 |
-
return result_df
|
| 430 |
-
|
| 431 |
-
except Exception as e:
|
| 432 |
-
st.error(f"Error loading historical experiments: {e}")
|
| 433 |
-
return pd.DataFrame()
|
| 434 |
-
|
| 435 |
-
def get_experiment_summary(self, brand: Optional[str] = None) -> pd.DataFrame:
|
| 436 |
-
"""
|
| 437 |
-
Get summary statistics for all historical experiments.
|
| 438 |
-
Joins metadata and feedback tables for comprehensive view.
|
| 439 |
-
|
| 440 |
-
Args:
|
| 441 |
-
brand: Optional brand filter
|
| 442 |
-
|
| 443 |
-
Returns:
|
| 444 |
-
pd.DataFrame: Summary statistics per experiment
|
| 445 |
-
"""
|
| 446 |
-
try:
|
| 447 |
-
brand_filter = f"WHERE m.BRAND = '{brand}'" if brand else ""
|
| 448 |
-
|
| 449 |
-
query = f"""
|
| 450 |
-
WITH ExperimentStats AS (
|
| 451 |
-
SELECT
|
| 452 |
-
m.EXPERIMENT_ID,
|
| 453 |
-
m.BRAND,
|
| 454 |
-
m.CAMPAIGN_NAME,
|
| 455 |
-
m.CONFIG_NAME,
|
| 456 |
-
MIN(m.TIMESTAMP) as START_TIME,
|
| 457 |
-
MAX(m.TIMESTAMP) as END_TIME,
|
| 458 |
-
COUNT(DISTINCT m.STAGE) as TOTAL_STAGES,
|
| 459 |
-
SUM(m.TOTAL_MESSAGES) as TOTAL_MESSAGES,
|
| 460 |
-
MAX(m.TOTAL_USERS) as TOTAL_USERS,
|
| 461 |
-
LISTAGG(DISTINCT m.LLM_MODEL, ', ') as MODELS_USED
|
| 462 |
-
FROM {self.METADATA_TABLE} m
|
| 463 |
-
{brand_filter}
|
| 464 |
-
GROUP BY m.EXPERIMENT_ID, m.BRAND, m.CAMPAIGN_NAME, m.CONFIG_NAME
|
| 465 |
-
),
|
| 466 |
-
FeedbackStats AS (
|
| 467 |
-
SELECT
|
| 468 |
-
EXPERIMENT_ID,
|
| 469 |
-
COUNT(*) as TOTAL_FEEDBACK,
|
| 470 |
-
SUM(CASE WHEN FEEDBACK_TYPE = 'reject' THEN 1 ELSE 0 END) as TOTAL_REJECTS
|
| 471 |
-
FROM {self.FEEDBACK_TABLE}
|
| 472 |
-
{brand_filter.replace('m.', '')}
|
| 473 |
-
GROUP BY EXPERIMENT_ID
|
| 474 |
-
)
|
| 475 |
-
SELECT
|
| 476 |
-
e.*,
|
| 477 |
-
COALESCE(f.TOTAL_FEEDBACK, 0) as TOTAL_FEEDBACK,
|
| 478 |
-
COALESCE(f.TOTAL_REJECTS, 0) as TOTAL_REJECTS,
|
| 479 |
-
CASE
|
| 480 |
-
WHEN e.TOTAL_MESSAGES > 0
|
| 481 |
-
THEN ROUND((COALESCE(f.TOTAL_REJECTS, 0) * 100.0 / e.TOTAL_MESSAGES), 2)
|
| 482 |
-
ELSE 0
|
| 483 |
-
END as REJECTION_RATE
|
| 484 |
-
FROM ExperimentStats e
|
| 485 |
-
LEFT JOIN FeedbackStats f ON e.EXPERIMENT_ID = f.EXPERIMENT_ID
|
| 486 |
-
ORDER BY e.START_TIME DESC
|
| 487 |
-
"""
|
| 488 |
-
|
| 489 |
-
result_df = self.session.sql(query).to_pandas()
|
| 490 |
-
result_df.columns = result_df.columns.str.lower()
|
| 491 |
-
|
| 492 |
-
print(f"✅ Generated summary for {len(result_df)} experiments")
|
| 493 |
-
return result_df
|
| 494 |
-
|
| 495 |
-
except Exception as e:
|
| 496 |
-
st.error(f"Error generating experiment summary: {e}")
|
| 497 |
-
return pd.DataFrame()
|
| 498 |
-
|
| 499 |
-
def get_campaign_feedback_analysis(self, campaign_name: str, brand: Optional[str] = None) -> Dict:
|
| 500 |
-
"""
|
| 501 |
-
Get detailed feedback analysis for a specific campaign.
|
| 502 |
-
Returns comprehensive statistics including model performance, stage analysis, and rejection reasons.
|
| 503 |
-
|
| 504 |
-
Args:
|
| 505 |
-
campaign_name: Campaign name to analyze
|
| 506 |
-
brand: Optional brand filter
|
| 507 |
-
|
| 508 |
-
Returns:
|
| 509 |
-
dict: Dictionary containing multiple analysis dataframes
|
| 510 |
-
"""
|
| 511 |
-
try:
|
| 512 |
-
brand_filter = f"AND m.BRAND = '{brand}'" if brand else ""
|
| 513 |
-
|
| 514 |
-
# Query 1: Enhanced rejection distribution by stage (with messages and models)
|
| 515 |
-
stage_query = f"""
|
| 516 |
-
SELECT
|
| 517 |
-
m.STAGE,
|
| 518 |
-
SUM(m.TOTAL_MESSAGES) as TOTAL_MESSAGES,
|
| 519 |
-
COUNT(f.EXPERIMENT_ID) as TOTAL_REJECTIONS,
|
| 520 |
-
ROUND((COUNT(f.EXPERIMENT_ID) * 100.0 / NULLIF(SUM(m.TOTAL_MESSAGES), 0)), 2) as REJECTION_RATE,
|
| 521 |
-
COUNT(DISTINCT f.USER_ID) as UNIQUE_USERS_REJECTED,
|
| 522 |
-
COUNT(DISTINCT m.EXPERIMENT_ID) as EXPERIMENTS_COUNT,
|
| 523 |
-
LISTAGG(DISTINCT m.LLM_MODEL, ', ') WITHIN GROUP (ORDER BY m.LLM_MODEL) as MODELS_USED
|
| 524 |
-
FROM {self.METADATA_TABLE} m
|
| 525 |
-
LEFT JOIN {self.FEEDBACK_TABLE} f
|
| 526 |
-
ON m.EXPERIMENT_ID = f.EXPERIMENT_ID AND m.STAGE = f.STAGE
|
| 527 |
-
WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
|
| 528 |
-
GROUP BY m.STAGE
|
| 529 |
-
ORDER BY m.STAGE
|
| 530 |
-
"""
|
| 531 |
-
|
| 532 |
-
# Query 2: Rejection distribution by LLM model
|
| 533 |
-
model_query = f"""
|
| 534 |
-
SELECT
|
| 535 |
-
m.LLM_MODEL,
|
| 536 |
-
COUNT(f.EXPERIMENT_ID) as TOTAL_REJECTIONS,
|
| 537 |
-
COUNT(DISTINCT f.USER_ID) as UNIQUE_USERS_REJECTED,
|
| 538 |
-
SUM(m.TOTAL_MESSAGES) as TOTAL_MESSAGES_GENERATED,
|
| 539 |
-
ROUND((COUNT(f.EXPERIMENT_ID) * 100.0 / NULLIF(SUM(m.TOTAL_MESSAGES), 0)), 2) as REJECTION_RATE
|
| 540 |
-
FROM {self.METADATA_TABLE} m
|
| 541 |
-
LEFT JOIN {self.FEEDBACK_TABLE} f
|
| 542 |
-
ON m.EXPERIMENT_ID = f.EXPERIMENT_ID AND m.STAGE = f.STAGE
|
| 543 |
-
WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
|
| 544 |
-
GROUP BY m.LLM_MODEL
|
| 545 |
-
ORDER BY REJECTION_RATE DESC
|
| 546 |
-
"""
|
| 547 |
-
|
| 548 |
-
# Query 3: Rejection reasons breakdown
|
| 549 |
-
reasons_query = f"""
|
| 550 |
-
SELECT
|
| 551 |
-
f.REJECTION_REASON,
|
| 552 |
-
COUNT(*) as COUNT,
|
| 553 |
-
COUNT(DISTINCT f.EXPERIMENT_ID) as EXPERIMENTS_AFFECTED
|
| 554 |
-
FROM {self.FEEDBACK_TABLE} f
|
| 555 |
-
JOIN {self.METADATA_TABLE} m
|
| 556 |
-
ON f.EXPERIMENT_ID = m.EXPERIMENT_ID
|
| 557 |
-
WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
|
| 558 |
-
GROUP BY f.REJECTION_REASON
|
| 559 |
-
ORDER BY COUNT DESC
|
| 560 |
-
"""
|
| 561 |
-
|
| 562 |
-
# Query 4: Rejection reasons by stage
|
| 563 |
-
reasons_by_stage_query = f"""
|
| 564 |
-
SELECT
|
| 565 |
-
f.STAGE,
|
| 566 |
-
f.REJECTION_REASON,
|
| 567 |
-
COUNT(*) as COUNT
|
| 568 |
-
FROM {self.FEEDBACK_TABLE} f
|
| 569 |
-
JOIN {self.METADATA_TABLE} m
|
| 570 |
-
ON f.EXPERIMENT_ID = m.EXPERIMENT_ID
|
| 571 |
-
WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
|
| 572 |
-
GROUP BY f.STAGE, f.REJECTION_REASON
|
| 573 |
-
ORDER BY f.STAGE, COUNT DESC
|
| 574 |
-
"""
|
| 575 |
-
|
| 576 |
-
# Query 5: Rejection reasons by model
|
| 577 |
-
reasons_by_model_query = f"""
|
| 578 |
-
SELECT
|
| 579 |
-
m.LLM_MODEL,
|
| 580 |
-
f.REJECTION_REASON,
|
| 581 |
-
COUNT(*) as COUNT
|
| 582 |
-
FROM {self.FEEDBACK_TABLE} f
|
| 583 |
-
JOIN {self.METADATA_TABLE} m
|
| 584 |
-
ON f.EXPERIMENT_ID = m.EXPERIMENT_ID AND f.STAGE = m.STAGE
|
| 585 |
-
WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
|
| 586 |
-
GROUP BY m.LLM_MODEL, f.REJECTION_REASON
|
| 587 |
-
ORDER BY m.LLM_MODEL, COUNT DESC
|
| 588 |
-
"""
|
| 589 |
-
|
| 590 |
-
# Execute all queries
|
| 591 |
-
stage_df = self.session.sql(stage_query).to_pandas()
|
| 592 |
-
stage_df.columns = stage_df.columns.str.lower()
|
| 593 |
-
|
| 594 |
-
model_df = self.session.sql(model_query).to_pandas()
|
| 595 |
-
model_df.columns = model_df.columns.str.lower()
|
| 596 |
-
|
| 597 |
-
reasons_df = self.session.sql(reasons_query).to_pandas()
|
| 598 |
-
reasons_df.columns = reasons_df.columns.str.lower()
|
| 599 |
-
|
| 600 |
-
reasons_by_stage_df = self.session.sql(reasons_by_stage_query).to_pandas()
|
| 601 |
-
reasons_by_stage_df.columns = reasons_by_stage_df.columns.str.lower()
|
| 602 |
-
|
| 603 |
-
reasons_by_model_df = self.session.sql(reasons_by_model_query).to_pandas()
|
| 604 |
-
reasons_by_model_df.columns = reasons_by_model_df.columns.str.lower()
|
| 605 |
-
|
| 606 |
-
print(f"✅ Generated feedback analysis for campaign '{campaign_name}'")
|
| 607 |
-
|
| 608 |
-
return {
|
| 609 |
-
'by_stage': stage_df,
|
| 610 |
-
'by_model': model_df,
|
| 611 |
-
'rejection_reasons': reasons_df,
|
| 612 |
-
'reasons_by_stage': reasons_by_stage_df,
|
| 613 |
-
'reasons_by_model': reasons_by_model_df
|
| 614 |
-
}
|
| 615 |
-
|
| 616 |
-
except Exception as e:
|
| 617 |
-
st.error(f"Error generating campaign feedback analysis: {e}")
|
| 618 |
-
import traceback
|
| 619 |
-
st.error(f"Traceback: {traceback.format_exc()}")
|
| 620 |
-
return {
|
| 621 |
-
'by_stage': pd.DataFrame(),
|
| 622 |
-
'by_model': pd.DataFrame(),
|
| 623 |
-
'rejection_reasons': pd.DataFrame(),
|
| 624 |
-
'reasons_by_stage': pd.DataFrame(),
|
| 625 |
-
'reasons_by_model': pd.DataFrame()
|
| 626 |
-
}
|
| 627 |
-
|
| 628 |
-
def get_all_campaigns_summary(self, brand: Optional[str] = None) -> pd.DataFrame:
|
| 629 |
-
"""
|
| 630 |
-
Get summary statistics for all campaigns (aggregating experiments by campaign).
|
| 631 |
-
|
| 632 |
-
Args:
|
| 633 |
-
brand: Optional brand filter
|
| 634 |
-
|
| 635 |
-
Returns:
|
| 636 |
-
pd.DataFrame: Summary statistics per campaign
|
| 637 |
-
"""
|
| 638 |
-
try:
|
| 639 |
-
brand_filter = f"WHERE m.BRAND = '{brand}'" if brand else ""
|
| 640 |
-
|
| 641 |
-
query = f"""
|
| 642 |
-
WITH CampaignStats AS (
|
| 643 |
-
SELECT
|
| 644 |
-
m.CAMPAIGN_NAME,
|
| 645 |
-
m.BRAND,
|
| 646 |
-
COUNT(DISTINCT m.EXPERIMENT_ID) as TOTAL_EXPERIMENTS,
|
| 647 |
-
COUNT(DISTINCT m.STAGE) as TOTAL_STAGES,
|
| 648 |
-
SUM(m.TOTAL_MESSAGES) as TOTAL_MESSAGES,
|
| 649 |
-
MAX(m.TOTAL_USERS) as TOTAL_USERS,
|
| 650 |
-
LISTAGG(DISTINCT m.LLM_MODEL, ', ') WITHIN GROUP (ORDER BY m.LLM_MODEL) as MODELS_USED,
|
| 651 |
-
MIN(m.TIMESTAMP) as FIRST_EXPERIMENT,
|
| 652 |
-
MAX(m.TIMESTAMP) as LAST_EXPERIMENT
|
| 653 |
-
FROM {self.METADATA_TABLE} m
|
| 654 |
-
{brand_filter}
|
| 655 |
-
GROUP BY m.CAMPAIGN_NAME, m.BRAND
|
| 656 |
-
),
|
| 657 |
-
FeedbackStats AS (
|
| 658 |
-
SELECT
|
| 659 |
-
m.CAMPAIGN_NAME,
|
| 660 |
-
COUNT(*) as TOTAL_FEEDBACK,
|
| 661 |
-
COUNT(DISTINCT f.REJECTION_REASON) as UNIQUE_REJECTION_REASONS
|
| 662 |
-
FROM {self.FEEDBACK_TABLE} f
|
| 663 |
-
JOIN {self.METADATA_TABLE} m
|
| 664 |
-
ON f.EXPERIMENT_ID = m.EXPERIMENT_ID
|
| 665 |
-
{brand_filter.replace('m.', 'm.')}
|
| 666 |
-
GROUP BY m.CAMPAIGN_NAME
|
| 667 |
-
)
|
| 668 |
-
SELECT
|
| 669 |
-
c.*,
|
| 670 |
-
COALESCE(f.TOTAL_FEEDBACK, 0) as TOTAL_REJECTIONS,
|
| 671 |
-
COALESCE(f.UNIQUE_REJECTION_REASONS, 0) as UNIQUE_REJECTION_REASONS,
|
| 672 |
-
CASE
|
| 673 |
-
WHEN c.TOTAL_MESSAGES > 0
|
| 674 |
-
THEN ROUND((COALESCE(f.TOTAL_FEEDBACK, 0) * 100.0 / c.TOTAL_MESSAGES), 2)
|
| 675 |
-
ELSE 0
|
| 676 |
-
END as REJECTION_RATE
|
| 677 |
-
FROM CampaignStats c
|
| 678 |
-
LEFT JOIN FeedbackStats f ON c.CAMPAIGN_NAME = f.CAMPAIGN_NAME
|
| 679 |
-
ORDER BY c.LAST_EXPERIMENT DESC
|
| 680 |
-
"""
|
| 681 |
-
|
| 682 |
-
result_df = self.session.sql(query).to_pandas()
|
| 683 |
-
result_df.columns = result_df.columns.str.lower()
|
| 684 |
-
|
| 685 |
-
print(f"✅ Generated summary for {len(result_df)} campaigns")
|
| 686 |
-
return result_df
|
| 687 |
-
|
| 688 |
-
except Exception as e:
|
| 689 |
-
st.error(f"Error generating campaigns summary: {e}")
|
| 690 |
-
import traceback
|
| 691 |
-
st.error(f"Traceback: {traceback.format_exc()}")
|
| 692 |
-
return pd.DataFrame()
|
| 693 |
-
|
| 694 |
-
def close(self):
|
| 695 |
-
"""Close the Snowflake session."""
|
| 696 |
-
try:
|
| 697 |
-
self.session.close()
|
| 698 |
-
print("✅ Snowflake session closed")
|
| 699 |
-
except Exception as e:
|
| 700 |
-
st.error(f"Error closing session: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/utils/experiment_runner.py
DELETED
|
@@ -1,276 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Experiment Runner Module
|
| 3 |
-
|
| 4 |
-
Handles message generation for single and AB testing experiments.
|
| 5 |
-
Works with session_state for in-memory data storage.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import pandas as pd
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
from typing import Dict, Tuple, Optional
|
| 11 |
-
import streamlit as st
|
| 12 |
-
from snowflake.snowpark import Session
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class ExperimentRunner:
|
| 16 |
-
"""
|
| 17 |
-
Runs message generation experiments and manages in-memory data.
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
def __init__(self, brand: str, system_config: dict):
|
| 21 |
-
"""
|
| 22 |
-
Initialize experiment runner.
|
| 23 |
-
|
| 24 |
-
Args:
|
| 25 |
-
brand: Brand name
|
| 26 |
-
system_config: System configuration dict
|
| 27 |
-
"""
|
| 28 |
-
self.brand = brand
|
| 29 |
-
self.system_config = system_config
|
| 30 |
-
|
| 31 |
-
def run_single_experiment(
|
| 32 |
-
self,
|
| 33 |
-
config: dict,
|
| 34 |
-
sampled_users_df: pd.DataFrame,
|
| 35 |
-
experiment_id: str,
|
| 36 |
-
create_session_func,
|
| 37 |
-
progress_container=None
|
| 38 |
-
) -> Tuple[bool, pd.DataFrame, list]:
|
| 39 |
-
"""
|
| 40 |
-
Run a single experiment across all stages.
|
| 41 |
-
Accumulates results in ui_log_data dataframe.
|
| 42 |
-
|
| 43 |
-
Args:
|
| 44 |
-
config: Campaign configuration
|
| 45 |
-
sampled_users_df: Sampled users dataframe
|
| 46 |
-
experiment_id: Unique experiment identifier
|
| 47 |
-
create_session_func: Function to create Snowflake session
|
| 48 |
-
progress_container: Streamlit container for progress updates
|
| 49 |
-
|
| 50 |
-
Returns:
|
| 51 |
-
tuple: (success, ui_log_data_df, metadata_list)
|
| 52 |
-
"""
|
| 53 |
-
from ai_messaging_system_v2.Messaging_system.Permes import Permes
|
| 54 |
-
|
| 55 |
-
# Determine if we're in threaded mode (no UI updates)
|
| 56 |
-
threaded_mode = progress_container is None
|
| 57 |
-
|
| 58 |
-
if not threaded_mode:
|
| 59 |
-
# Use the provided container for UI updates
|
| 60 |
-
container = progress_container
|
| 61 |
-
container.markdown(f"### 📊 Experiment Progress")
|
| 62 |
-
else:
|
| 63 |
-
# Threaded mode - no UI updates, just print to console
|
| 64 |
-
print(f"Starting experiment: {experiment_id}")
|
| 65 |
-
|
| 66 |
-
permes = Permes()
|
| 67 |
-
|
| 68 |
-
# Get campaign-level configurations
|
| 69 |
-
campaign_name = config.get("campaign_name", "UI-Campaign")
|
| 70 |
-
campaign_instructions = config.get("campaign_instructions", None)
|
| 71 |
-
num_stages = len([k for k in config.keys() if k.isdigit()])
|
| 72 |
-
|
| 73 |
-
# Initialize accumulation variables
|
| 74 |
-
ui_log_data = None # Will accumulate dataframes from all stages
|
| 75 |
-
metadata_list = []
|
| 76 |
-
generation_successful = True
|
| 77 |
-
|
| 78 |
-
for stage in range(1, num_stages + 1):
|
| 79 |
-
if not threaded_mode:
|
| 80 |
-
container.markdown(f"#### Stage {stage} of {num_stages}")
|
| 81 |
-
progress_bar = container.progress(0)
|
| 82 |
-
status_text = container.empty()
|
| 83 |
-
status_text.text(f"Generating messages for stage {stage}...")
|
| 84 |
-
else:
|
| 85 |
-
print(f" Stage {stage} of {num_stages} - Starting...")
|
| 86 |
-
# Create dummy objects that do nothing
|
| 87 |
-
class DummyProgress:
|
| 88 |
-
def progress(self, val): pass
|
| 89 |
-
class DummyStatus:
|
| 90 |
-
def text(self, msg): print(f" {msg}")
|
| 91 |
-
def success(self, msg): print(f" ✅ {msg}")
|
| 92 |
-
def warning(self, msg): print(f" ⚠️ {msg}")
|
| 93 |
-
def error(self, msg): print(f" ❌ {msg}")
|
| 94 |
-
|
| 95 |
-
progress_bar = DummyProgress()
|
| 96 |
-
status_text = DummyStatus()
|
| 97 |
-
|
| 98 |
-
session = None
|
| 99 |
-
try:
|
| 100 |
-
session = create_session_func()
|
| 101 |
-
progress_bar.progress(10)
|
| 102 |
-
|
| 103 |
-
# Get stage configuration
|
| 104 |
-
stage_config = config.get(str(stage), {})
|
| 105 |
-
|
| 106 |
-
if not stage_config:
|
| 107 |
-
status_text.error(f"❌ Stage {stage}: Configuration not found")
|
| 108 |
-
generation_successful = False
|
| 109 |
-
if session:
|
| 110 |
-
session.close()
|
| 111 |
-
continue
|
| 112 |
-
|
| 113 |
-
# Extract stage parameters
|
| 114 |
-
model = stage_config.get("model", "gemini-2.5-flash-lite")
|
| 115 |
-
segment_info = stage_config.get("segment_info", "")
|
| 116 |
-
recsys_contents = stage_config.get("recsys_contents", [])
|
| 117 |
-
involve_recsys_result = stage_config.get("involve_recsys_result", True)
|
| 118 |
-
personalization = stage_config.get("personalization", True)
|
| 119 |
-
sample_example = stage_config.get("sample_examples", "")
|
| 120 |
-
per_message_instructions = stage_config.get("instructions", None)
|
| 121 |
-
specific_content_id = stage_config.get("specific_content_id", None)
|
| 122 |
-
|
| 123 |
-
progress_bar.progress(20)
|
| 124 |
-
|
| 125 |
-
# Call Permes to generate messages
|
| 126 |
-
# For stage > 1, pass accumulated ui_log_data
|
| 127 |
-
users_message = permes.create_personalize_messages(
|
| 128 |
-
session=session,
|
| 129 |
-
model=model,
|
| 130 |
-
users=sampled_users_df.copy(),
|
| 131 |
-
brand=self.brand,
|
| 132 |
-
config_file=self.system_config,
|
| 133 |
-
segment_info=segment_info,
|
| 134 |
-
involve_recsys_result=involve_recsys_result,
|
| 135 |
-
identifier_column="user_id",
|
| 136 |
-
recsys_contents=recsys_contents,
|
| 137 |
-
sample_example=sample_example,
|
| 138 |
-
campaign_name=campaign_name,
|
| 139 |
-
personalization=personalization,
|
| 140 |
-
stage=stage,
|
| 141 |
-
test_mode=False,
|
| 142 |
-
mode="ui",
|
| 143 |
-
campaign_instructions=campaign_instructions,
|
| 144 |
-
per_message_instructions=per_message_instructions,
|
| 145 |
-
specific_content_id=specific_content_id,
|
| 146 |
-
ui_experiment_id=experiment_id,
|
| 147 |
-
ui_log_data=ui_log_data # Pass accumulated data for stage > 1
|
| 148 |
-
)
|
| 149 |
-
|
| 150 |
-
progress_bar.progress(100)
|
| 151 |
-
|
| 152 |
-
if users_message is not None and len(users_message) > 0:
|
| 153 |
-
# Accumulate results
|
| 154 |
-
if ui_log_data is None:
|
| 155 |
-
ui_log_data = users_message.copy()
|
| 156 |
-
else:
|
| 157 |
-
# Append new stage to accumulated data
|
| 158 |
-
ui_log_data = pd.concat([ui_log_data, users_message], ignore_index=True)
|
| 159 |
-
|
| 160 |
-
# Track metadata for this stage
|
| 161 |
-
metadata = {
|
| 162 |
-
"experiment_id": experiment_id,
|
| 163 |
-
"config_name": config.get("campaign_name", "Unknown"),
|
| 164 |
-
"brand": self.brand,
|
| 165 |
-
"campaign_name": campaign_name,
|
| 166 |
-
"stage": stage,
|
| 167 |
-
"llm_model": model,
|
| 168 |
-
"total_messages": len(users_message),
|
| 169 |
-
"total_users": sampled_users_df['user_id'].nunique() if 'user_id' in sampled_users_df.columns else len(sampled_users_df),
|
| 170 |
-
"platform": stage_config.get("platform", "push"),
|
| 171 |
-
"personalization": personalization,
|
| 172 |
-
"involve_recsys": involve_recsys_result,
|
| 173 |
-
"recsys_contents": recsys_contents,
|
| 174 |
-
"segment_info": segment_info,
|
| 175 |
-
"campaign_instructions": campaign_instructions,
|
| 176 |
-
"per_message_instructions": per_message_instructions,
|
| 177 |
-
"timestamp": datetime.now()
|
| 178 |
-
}
|
| 179 |
-
metadata_list.append(metadata)
|
| 180 |
-
|
| 181 |
-
status_text.success(
|
| 182 |
-
f"✅ Stage {stage}: Generated {len(users_message)} messages"
|
| 183 |
-
)
|
| 184 |
-
else:
|
| 185 |
-
status_text.warning(f"⚠️ Stage {stage}: No messages generated")
|
| 186 |
-
generation_successful = False
|
| 187 |
-
|
| 188 |
-
except Exception as e:
|
| 189 |
-
progress_bar.progress(0)
|
| 190 |
-
import traceback
|
| 191 |
-
error_details = traceback.format_exc()
|
| 192 |
-
status_text.error(f"❌ Stage {stage}: Error - {str(e)}")
|
| 193 |
-
if not threaded_mode:
|
| 194 |
-
st.error(f"**Detailed Error Information:**\n```\n{error_details}\n```")
|
| 195 |
-
else:
|
| 196 |
-
print(f"Error details: {error_details}")
|
| 197 |
-
generation_successful = False
|
| 198 |
-
|
| 199 |
-
finally:
|
| 200 |
-
if session:
|
| 201 |
-
try:
|
| 202 |
-
session.close()
|
| 203 |
-
except:
|
| 204 |
-
pass
|
| 205 |
-
|
| 206 |
-
return generation_successful, ui_log_data, metadata_list
|
| 207 |
-
|
| 208 |
-
def run_ab_test_parallel(
|
| 209 |
-
self,
|
| 210 |
-
config_a: dict,
|
| 211 |
-
config_b: dict,
|
| 212 |
-
sampled_users_df: pd.DataFrame,
|
| 213 |
-
experiment_a_id: str,
|
| 214 |
-
experiment_b_id: str,
|
| 215 |
-
create_session_func
|
| 216 |
-
) -> Dict:
|
| 217 |
-
"""
|
| 218 |
-
Run two experiments (A and B) in parallel threads.
|
| 219 |
-
Returns results for both experiments.
|
| 220 |
-
|
| 221 |
-
Args:
|
| 222 |
-
config_a: Configuration for experiment A
|
| 223 |
-
config_b: Configuration for experiment B
|
| 224 |
-
sampled_users_df: Sampled users (same for both)
|
| 225 |
-
experiment_a_id: Experiment A ID
|
| 226 |
-
experiment_b_id: Experiment B ID
|
| 227 |
-
create_session_func: Function to create Snowflake session
|
| 228 |
-
|
| 229 |
-
Returns:
|
| 230 |
-
dict: {
|
| 231 |
-
'a': {'success': bool, 'ui_log_data': df, 'metadata': list, 'error': str},
|
| 232 |
-
'b': {...}
|
| 233 |
-
}
|
| 234 |
-
"""
|
| 235 |
-
import threading
|
| 236 |
-
|
| 237 |
-
results = {
|
| 238 |
-
'a': {'success': False, 'ui_log_data': None, 'metadata': [], 'error': None},
|
| 239 |
-
'b': {'success': False, 'ui_log_data': None, 'metadata': [], 'error': None}
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
def run_experiment(exp_key: str, config: dict, exp_id: str):
|
| 243 |
-
"""Run single experiment in thread."""
|
| 244 |
-
try:
|
| 245 |
-
success, ui_log_data, metadata = self.run_single_experiment(
|
| 246 |
-
config=config,
|
| 247 |
-
sampled_users_df=sampled_users_df,
|
| 248 |
-
experiment_id=exp_id,
|
| 249 |
-
create_session_func=create_session_func,
|
| 250 |
-
progress_container=None # No UI updates from thread
|
| 251 |
-
)
|
| 252 |
-
|
| 253 |
-
results[exp_key]['success'] = success
|
| 254 |
-
results[exp_key]['ui_log_data'] = ui_log_data
|
| 255 |
-
results[exp_key]['metadata'] = metadata
|
| 256 |
-
|
| 257 |
-
except Exception as e:
|
| 258 |
-
import traceback
|
| 259 |
-
error_trace = traceback.format_exc()
|
| 260 |
-
results[exp_key]['error'] = f"{str(e)}\n\nTraceback:\n{error_trace}"
|
| 261 |
-
results[exp_key]['success'] = False
|
| 262 |
-
print(f"Error in experiment {exp_key}: {error_trace}")
|
| 263 |
-
|
| 264 |
-
# Create threads
|
| 265 |
-
thread_a = threading.Thread(target=run_experiment, args=('a', config_a, experiment_a_id))
|
| 266 |
-
thread_b = threading.Thread(target=run_experiment, args=('b', config_b, experiment_b_id))
|
| 267 |
-
|
| 268 |
-
# Start both threads
|
| 269 |
-
thread_a.start()
|
| 270 |
-
thread_b.start()
|
| 271 |
-
|
| 272 |
-
# Wait for both to complete
|
| 273 |
-
thread_a.join()
|
| 274 |
-
thread_b.join()
|
| 275 |
-
|
| 276 |
-
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/utils/feedback_manager.py
DELETED
|
@@ -1,414 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Feedback management module for the AI Messaging System Visualization Tool.
|
| 3 |
-
|
| 4 |
-
Handles storage and retrieval of user feedback on generated messages.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import pandas as pd
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
from typing import Optional, List, Dict
|
| 11 |
-
import streamlit as st
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# Rejection reason categories
|
| 15 |
-
REJECTION_REASONS = {
|
| 16 |
-
"poor_header": "Poor Header",
|
| 17 |
-
"poor_body": "Poor Body/Content",
|
| 18 |
-
"grammar_issues": "Grammar Issues",
|
| 19 |
-
"emoji_problems": "Emoji Problems",
|
| 20 |
-
"recommendation_issues": "Recommendation Issues",
|
| 21 |
-
"wrong_information": "Wrong/Inaccurate Information",
|
| 22 |
-
"tone_issues": "Tone Issues",
|
| 23 |
-
"similarity": "Similar To Previous Header/Messages",
|
| 24 |
-
"other": "Other"
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
class FeedbackManager:
|
| 29 |
-
"""
|
| 30 |
-
Manages feedback storage and retrieval for generated messages.
|
| 31 |
-
"""
|
| 32 |
-
|
| 33 |
-
def __init__(self):
|
| 34 |
-
"""Initialize FeedbackManager."""
|
| 35 |
-
self.base_path = Path(__file__).parent.parent
|
| 36 |
-
self.feedback_path = self.base_path / "data" / "feedback"
|
| 37 |
-
|
| 38 |
-
# Create directory if it doesn't exist
|
| 39 |
-
self.feedback_path.mkdir(parents=True, exist_ok=True)
|
| 40 |
-
|
| 41 |
-
def get_feedback_file_path(self, experiment_id: str) -> Path:
|
| 42 |
-
"""
|
| 43 |
-
Get path to feedback file for an experiment.
|
| 44 |
-
|
| 45 |
-
Args:
|
| 46 |
-
experiment_id: Experiment ID
|
| 47 |
-
|
| 48 |
-
Returns:
|
| 49 |
-
Path: Feedback file path
|
| 50 |
-
"""
|
| 51 |
-
return self.feedback_path / f"{experiment_id}_feedback.csv"
|
| 52 |
-
|
| 53 |
-
def save_feedback(
|
| 54 |
-
self,
|
| 55 |
-
experiment_id: str,
|
| 56 |
-
user_id: int,
|
| 57 |
-
stage: int,
|
| 58 |
-
feedback_type: str,
|
| 59 |
-
rejection_reason: Optional[str] = None,
|
| 60 |
-
rejection_text: Optional[str] = None,
|
| 61 |
-
campaign_name: Optional[str] = None,
|
| 62 |
-
message_header: Optional[str] = None,
|
| 63 |
-
message_body: Optional[str] = None
|
| 64 |
-
) -> bool:
|
| 65 |
-
"""
|
| 66 |
-
Save feedback for a message.
|
| 67 |
-
|
| 68 |
-
Args:
|
| 69 |
-
experiment_id: Experiment ID
|
| 70 |
-
user_id: User ID
|
| 71 |
-
stage: Stage number
|
| 72 |
-
feedback_type: 'reject' (we focus on rejects)
|
| 73 |
-
rejection_reason: Category of rejection (optional)
|
| 74 |
-
rejection_text: Additional text for rejection (optional)
|
| 75 |
-
campaign_name: Campaign name (optional)
|
| 76 |
-
message_header: Message header text (optional)
|
| 77 |
-
message_body: Message body text (optional)
|
| 78 |
-
|
| 79 |
-
Returns:
|
| 80 |
-
bool: True if saved successfully, False otherwise
|
| 81 |
-
"""
|
| 82 |
-
try:
|
| 83 |
-
feedback_file = self.get_feedback_file_path(experiment_id)
|
| 84 |
-
|
| 85 |
-
# Create feedback record
|
| 86 |
-
feedback_record = {
|
| 87 |
-
"experiment_id": experiment_id,
|
| 88 |
-
"user_id": user_id,
|
| 89 |
-
"stage": stage,
|
| 90 |
-
"feedback_type": feedback_type,
|
| 91 |
-
"rejection_reason": rejection_reason,
|
| 92 |
-
"rejection_text": rejection_text,
|
| 93 |
-
"campaign_name": campaign_name,
|
| 94 |
-
"message_header": message_header,
|
| 95 |
-
"message_body": message_body,
|
| 96 |
-
"timestamp": datetime.now().isoformat()
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
# Convert to DataFrame
|
| 100 |
-
new_feedback = pd.DataFrame([feedback_record])
|
| 101 |
-
|
| 102 |
-
# Append to existing file or create new
|
| 103 |
-
if feedback_file.exists():
|
| 104 |
-
existing_feedback = pd.read_csv(feedback_file, encoding='utf-8-sig')
|
| 105 |
-
|
| 106 |
-
# Check if this exact feedback already exists
|
| 107 |
-
duplicate = existing_feedback[
|
| 108 |
-
(existing_feedback['user_id'] == user_id) &
|
| 109 |
-
(existing_feedback['stage'] == stage) &
|
| 110 |
-
(existing_feedback['experiment_id'] == experiment_id)
|
| 111 |
-
]
|
| 112 |
-
|
| 113 |
-
if len(duplicate) > 0:
|
| 114 |
-
# Update existing feedback
|
| 115 |
-
existing_feedback.loc[
|
| 116 |
-
(existing_feedback['user_id'] == user_id) &
|
| 117 |
-
(existing_feedback['stage'] == stage) &
|
| 118 |
-
(existing_feedback['experiment_id'] == experiment_id),
|
| 119 |
-
['feedback_type', 'rejection_reason', 'rejection_text', 'message_header', 'message_body', 'timestamp']
|
| 120 |
-
] = [feedback_type, rejection_reason, rejection_text, message_header, message_body, datetime.now().isoformat()]
|
| 121 |
-
|
| 122 |
-
updated_feedback = existing_feedback
|
| 123 |
-
else:
|
| 124 |
-
# Append new feedback
|
| 125 |
-
updated_feedback = pd.concat([existing_feedback, new_feedback], ignore_index=True)
|
| 126 |
-
else:
|
| 127 |
-
updated_feedback = new_feedback
|
| 128 |
-
|
| 129 |
-
# Save to CSV
|
| 130 |
-
updated_feedback.to_csv(feedback_file, index=False, encoding='utf-8-sig')
|
| 131 |
-
return True
|
| 132 |
-
|
| 133 |
-
except Exception as e:
|
| 134 |
-
st.error(f"Error saving feedback: {e}")
|
| 135 |
-
return False
|
| 136 |
-
|
| 137 |
-
def load_feedback(self, experiment_id: str) -> Optional[pd.DataFrame]:
|
| 138 |
-
"""
|
| 139 |
-
Load feedback for an experiment.
|
| 140 |
-
|
| 141 |
-
Args:
|
| 142 |
-
experiment_id: Experiment ID
|
| 143 |
-
|
| 144 |
-
Returns:
|
| 145 |
-
pd.DataFrame or None: Feedback DataFrame or None if not found
|
| 146 |
-
"""
|
| 147 |
-
try:
|
| 148 |
-
feedback_file = self.get_feedback_file_path(experiment_id)
|
| 149 |
-
|
| 150 |
-
if not feedback_file.exists():
|
| 151 |
-
return pd.DataFrame(columns=[
|
| 152 |
-
'experiment_id', 'user_id', 'stage', 'feedback_type',
|
| 153 |
-
'rejection_reason', 'rejection_text', 'campaign_name',
|
| 154 |
-
'message_header', 'message_body', 'timestamp'
|
| 155 |
-
])
|
| 156 |
-
|
| 157 |
-
return pd.read_csv(feedback_file, encoding='utf-8-sig')
|
| 158 |
-
|
| 159 |
-
except Exception as e:
|
| 160 |
-
st.error(f"Error loading feedback: {e}")
|
| 161 |
-
return None
|
| 162 |
-
|
| 163 |
-
def get_feedback_for_message(
|
| 164 |
-
self,
|
| 165 |
-
experiment_id: str,
|
| 166 |
-
user_id: int,
|
| 167 |
-
stage: int
|
| 168 |
-
) -> Optional[Dict]:
|
| 169 |
-
"""
|
| 170 |
-
Get feedback for a specific message.
|
| 171 |
-
|
| 172 |
-
Args:
|
| 173 |
-
experiment_id: Experiment ID
|
| 174 |
-
user_id: User ID
|
| 175 |
-
stage: Stage number
|
| 176 |
-
|
| 177 |
-
Returns:
|
| 178 |
-
dict or None: Feedback record or None if not found
|
| 179 |
-
"""
|
| 180 |
-
feedback_df = self.load_feedback(experiment_id)
|
| 181 |
-
|
| 182 |
-
if feedback_df is None or len(feedback_df) == 0:
|
| 183 |
-
return None
|
| 184 |
-
|
| 185 |
-
# Filter for specific message
|
| 186 |
-
message_feedback = feedback_df[
|
| 187 |
-
(feedback_df['user_id'] == user_id) &
|
| 188 |
-
(feedback_df['stage'] == stage) &
|
| 189 |
-
(feedback_df['experiment_id'] == experiment_id)
|
| 190 |
-
]
|
| 191 |
-
|
| 192 |
-
if len(message_feedback) > 0:
|
| 193 |
-
return message_feedback.iloc[0].to_dict()
|
| 194 |
-
|
| 195 |
-
return None
|
| 196 |
-
|
| 197 |
-
def delete_feedback(
|
| 198 |
-
self,
|
| 199 |
-
experiment_id: str,
|
| 200 |
-
user_id: int,
|
| 201 |
-
stage: int
|
| 202 |
-
) -> bool:
|
| 203 |
-
"""
|
| 204 |
-
Delete feedback for a specific message.
|
| 205 |
-
|
| 206 |
-
Args:
|
| 207 |
-
experiment_id: Experiment ID
|
| 208 |
-
user_id: User ID
|
| 209 |
-
stage: Stage number
|
| 210 |
-
|
| 211 |
-
Returns:
|
| 212 |
-
bool: True if deleted successfully, False otherwise
|
| 213 |
-
"""
|
| 214 |
-
try:
|
| 215 |
-
feedback_df = self.load_feedback(experiment_id)
|
| 216 |
-
|
| 217 |
-
if feedback_df is None or len(feedback_df) == 0:
|
| 218 |
-
return False
|
| 219 |
-
|
| 220 |
-
# Remove the feedback
|
| 221 |
-
updated_feedback = feedback_df[
|
| 222 |
-
~((feedback_df['user_id'] == user_id) &
|
| 223 |
-
(feedback_df['stage'] == stage) &
|
| 224 |
-
(feedback_df['experiment_id'] == experiment_id))
|
| 225 |
-
]
|
| 226 |
-
|
| 227 |
-
# Save updated feedback
|
| 228 |
-
feedback_file = self.get_feedback_file_path(experiment_id)
|
| 229 |
-
updated_feedback.to_csv(feedback_file, index=False, encoding='utf-8-sig')
|
| 230 |
-
|
| 231 |
-
return True
|
| 232 |
-
|
| 233 |
-
except Exception as e:
|
| 234 |
-
st.error(f"Error deleting feedback: {e}")
|
| 235 |
-
return False
|
| 236 |
-
|
| 237 |
-
def get_feedback_stats(self, experiment_id: str, total_messages: int = None) -> Dict:
|
| 238 |
-
"""
|
| 239 |
-
Get feedback statistics for an experiment.
|
| 240 |
-
|
| 241 |
-
Args:
|
| 242 |
-
experiment_id: Experiment ID
|
| 243 |
-
total_messages: Total number of messages generated (optional, for accurate rejection rate)
|
| 244 |
-
|
| 245 |
-
Returns:
|
| 246 |
-
dict: Feedback statistics
|
| 247 |
-
"""
|
| 248 |
-
feedback_df = self.load_feedback(experiment_id)
|
| 249 |
-
|
| 250 |
-
if feedback_df is None or len(feedback_df) == 0:
|
| 251 |
-
return {
|
| 252 |
-
"total_feedback": 0,
|
| 253 |
-
"total_rejects": 0,
|
| 254 |
-
"reject_rate": 0.0,
|
| 255 |
-
"rejection_reasons": {}
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
total_feedback = len(feedback_df)
|
| 259 |
-
total_rejects = len(feedback_df[feedback_df['feedback_type'] == 'reject'])
|
| 260 |
-
|
| 261 |
-
# Count rejection reasons
|
| 262 |
-
rejection_reasons = {}
|
| 263 |
-
if total_rejects > 0:
|
| 264 |
-
reason_counts = feedback_df[
|
| 265 |
-
feedback_df['feedback_type'] == 'reject'
|
| 266 |
-
]['rejection_reason'].value_counts().to_dict()
|
| 267 |
-
|
| 268 |
-
rejection_reasons = {
|
| 269 |
-
REJECTION_REASONS.get(k, k): v
|
| 270 |
-
for k, v in reason_counts.items()
|
| 271 |
-
if k is not None and pd.notna(k)
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
# Calculate rejection rate based on total messages (not total feedback)
|
| 275 |
-
# Since users only provide feedback for rejected messages
|
| 276 |
-
if total_messages is not None and total_messages > 0:
|
| 277 |
-
reject_rate = (total_rejects / total_messages * 100)
|
| 278 |
-
else:
|
| 279 |
-
# Fallback to old calculation if total_messages not provided
|
| 280 |
-
reject_rate = (total_rejects / total_feedback * 100) if total_feedback > 0 else 0.0
|
| 281 |
-
|
| 282 |
-
stats = {
|
| 283 |
-
"total_feedback": total_feedback,
|
| 284 |
-
"total_rejects": total_rejects,
|
| 285 |
-
"reject_rate": reject_rate,
|
| 286 |
-
"rejection_reasons": rejection_reasons
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
return stats
|
| 290 |
-
|
| 291 |
-
def get_stage_feedback_stats(self, experiment_id: str, messages_df: pd.DataFrame = None) -> pd.DataFrame:
|
| 292 |
-
"""
|
| 293 |
-
Get feedback statistics by stage.
|
| 294 |
-
|
| 295 |
-
Args:
|
| 296 |
-
experiment_id: Experiment ID
|
| 297 |
-
messages_df: DataFrame of all messages (optional, for accurate rejection rate per stage)
|
| 298 |
-
|
| 299 |
-
Returns:
|
| 300 |
-
pd.DataFrame: Stage-wise feedback statistics
|
| 301 |
-
"""
|
| 302 |
-
feedback_df = self.load_feedback(experiment_id)
|
| 303 |
-
|
| 304 |
-
if feedback_df is None or len(feedback_df) == 0:
|
| 305 |
-
return pd.DataFrame(columns=['stage', 'total_messages', 'total_feedback', 'rejects', 'reject_rate'])
|
| 306 |
-
|
| 307 |
-
# Get total messages per stage from messages_df if provided
|
| 308 |
-
stage_message_counts = {}
|
| 309 |
-
if messages_df is not None and 'stage' in messages_df.columns:
|
| 310 |
-
stage_message_counts = messages_df.groupby('stage').size().to_dict()
|
| 311 |
-
|
| 312 |
-
# Group by stage
|
| 313 |
-
stage_stats = []
|
| 314 |
-
for stage in sorted(feedback_df['stage'].unique()):
|
| 315 |
-
stage_feedback = feedback_df[feedback_df['stage'] == stage]
|
| 316 |
-
total_feedback = len(stage_feedback)
|
| 317 |
-
rejects = len(stage_feedback[stage_feedback['feedback_type'] == 'reject'])
|
| 318 |
-
|
| 319 |
-
# Calculate rejection rate based on total messages for this stage
|
| 320 |
-
total_messages_for_stage = stage_message_counts.get(stage, total_feedback)
|
| 321 |
-
reject_rate = (rejects / total_messages_for_stage * 100) if total_messages_for_stage > 0 else 0.0
|
| 322 |
-
|
| 323 |
-
stage_stats.append({
|
| 324 |
-
'stage': stage,
|
| 325 |
-
'total_messages': total_messages_for_stage,
|
| 326 |
-
'total_feedback': total_feedback,
|
| 327 |
-
'rejects': rejects,
|
| 328 |
-
'reject_rate': reject_rate
|
| 329 |
-
})
|
| 330 |
-
|
| 331 |
-
return pd.DataFrame(stage_stats)
|
| 332 |
-
|
| 333 |
-
def compare_experiments(
|
| 334 |
-
self,
|
| 335 |
-
experiment_id_a: str,
|
| 336 |
-
experiment_id_b: str,
|
| 337 |
-
total_messages_a: int = None,
|
| 338 |
-
total_messages_b: int = None
|
| 339 |
-
) -> Dict:
|
| 340 |
-
"""
|
| 341 |
-
Compare feedback statistics between two experiments.
|
| 342 |
-
|
| 343 |
-
Args:
|
| 344 |
-
experiment_id_a: First experiment ID
|
| 345 |
-
experiment_id_b: Second experiment ID
|
| 346 |
-
total_messages_a: Total messages in experiment A (optional, for accurate rejection rate)
|
| 347 |
-
total_messages_b: Total messages in experiment B (optional, for accurate rejection rate)
|
| 348 |
-
|
| 349 |
-
Returns:
|
| 350 |
-
dict: Comparison statistics
|
| 351 |
-
"""
|
| 352 |
-
stats_a = self.get_feedback_stats(experiment_id_a, total_messages=total_messages_a)
|
| 353 |
-
stats_b = self.get_feedback_stats(experiment_id_b, total_messages=total_messages_b)
|
| 354 |
-
|
| 355 |
-
comparison = {
|
| 356 |
-
"experiment_a": {
|
| 357 |
-
"id": experiment_id_a,
|
| 358 |
-
"stats": stats_a
|
| 359 |
-
},
|
| 360 |
-
"experiment_b": {
|
| 361 |
-
"id": experiment_id_b,
|
| 362 |
-
"stats": stats_b
|
| 363 |
-
},
|
| 364 |
-
"difference": {
|
| 365 |
-
"reject_rate": stats_a['reject_rate'] - stats_b['reject_rate']
|
| 366 |
-
}
|
| 367 |
-
}
|
| 368 |
-
|
| 369 |
-
return comparison
|
| 370 |
-
|
| 371 |
-
def export_feedback(self, experiment_id: str, export_path: Path) -> bool:
|
| 372 |
-
"""
|
| 373 |
-
Export feedback to CSV file.
|
| 374 |
-
|
| 375 |
-
Args:
|
| 376 |
-
experiment_id: Experiment ID
|
| 377 |
-
export_path: Export file path
|
| 378 |
-
|
| 379 |
-
Returns:
|
| 380 |
-
bool: True if exported successfully, False otherwise
|
| 381 |
-
"""
|
| 382 |
-
try:
|
| 383 |
-
feedback_df = self.load_feedback(experiment_id)
|
| 384 |
-
|
| 385 |
-
if feedback_df is None:
|
| 386 |
-
return False
|
| 387 |
-
|
| 388 |
-
feedback_df.to_csv(export_path, index=False, encoding='utf-8-sig')
|
| 389 |
-
return True
|
| 390 |
-
|
| 391 |
-
except Exception as e:
|
| 392 |
-
st.error(f"Error exporting feedback: {e}")
|
| 393 |
-
return False
|
| 394 |
-
|
| 395 |
-
def get_rejection_reason_label(self, reason_key: str) -> str:
|
| 396 |
-
"""
|
| 397 |
-
Get human-readable label for rejection reason key.
|
| 398 |
-
|
| 399 |
-
Args:
|
| 400 |
-
reason_key: Rejection reason key
|
| 401 |
-
|
| 402 |
-
Returns:
|
| 403 |
-
str: Human-readable label
|
| 404 |
-
"""
|
| 405 |
-
return REJECTION_REASONS.get(reason_key, reason_key)
|
| 406 |
-
|
| 407 |
-
def get_all_rejection_reasons(self) -> Dict[str, str]:
|
| 408 |
-
"""
|
| 409 |
-
Get all available rejection reasons.
|
| 410 |
-
|
| 411 |
-
Returns:
|
| 412 |
-
dict: Dictionary mapping keys to labels
|
| 413 |
-
"""
|
| 414 |
-
return REJECTION_REASONS.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/utils/session_feedback_manager.py
DELETED
|
@@ -1,310 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Session-based Feedback Manager
|
| 3 |
-
|
| 4 |
-
Manages feedback in streamlit session_state instead of local files.
|
| 5 |
-
Works with the new in-memory architecture.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import pandas as pd
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
from typing import Optional, List, Dict
|
| 11 |
-
import streamlit as st
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# Rejection reason categories
|
| 15 |
-
REJECTION_REASONS = {
|
| 16 |
-
"poor_header": "Poor Header",
|
| 17 |
-
"poor_body": "Poor Body/Content",
|
| 18 |
-
"grammar_issues": "Grammar Issues",
|
| 19 |
-
"emoji_problems": "Emoji Problems",
|
| 20 |
-
"recommendation_issues": "Recommendation Issues",
|
| 21 |
-
"wrong_information": "Wrong/Inaccurate Information",
|
| 22 |
-
"tone_issues": "Tone Issues",
|
| 23 |
-
"similarity": "Similar To Previous Header/Messages",
|
| 24 |
-
"other": "Other"
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
class SessionFeedbackManager:
|
| 29 |
-
"""
|
| 30 |
-
Manages feedback in streamlit session_state for in-memory operations.
|
| 31 |
-
"""
|
| 32 |
-
|
| 33 |
-
@staticmethod
|
| 34 |
-
def add_feedback(
|
| 35 |
-
experiment_id: str,
|
| 36 |
-
user_id: int,
|
| 37 |
-
stage: int,
|
| 38 |
-
feedback_type: str,
|
| 39 |
-
rejection_reason: Optional[str] = None,
|
| 40 |
-
rejection_text: Optional[str] = None,
|
| 41 |
-
campaign_name: Optional[str] = None,
|
| 42 |
-
config_name: Optional[str] = None,
|
| 43 |
-
brand: Optional[str] = None,
|
| 44 |
-
message_header: Optional[str] = None,
|
| 45 |
-
message_body: Optional[str] = None,
|
| 46 |
-
feedback_list_key: str = "current_feedbacks"
|
| 47 |
-
) -> bool:
|
| 48 |
-
"""
|
| 49 |
-
Add feedback to session_state list.
|
| 50 |
-
|
| 51 |
-
Args:
|
| 52 |
-
experiment_id: Experiment ID
|
| 53 |
-
user_id: User ID
|
| 54 |
-
stage: Stage number
|
| 55 |
-
feedback_type: 'reject' or other
|
| 56 |
-
rejection_reason: Category of rejection (optional)
|
| 57 |
-
rejection_text: Additional text for rejection (optional)
|
| 58 |
-
campaign_name: Campaign name (optional)
|
| 59 |
-
config_name: Config name (optional)
|
| 60 |
-
brand: Brand name (optional)
|
| 61 |
-
message_header: Message header text (optional)
|
| 62 |
-
message_body: Message body text (optional)
|
| 63 |
-
feedback_list_key: Key in session_state for feedback list (default: "current_feedbacks")
|
| 64 |
-
|
| 65 |
-
Returns:
|
| 66 |
-
bool: True if added successfully
|
| 67 |
-
"""
|
| 68 |
-
try:
|
| 69 |
-
# Create feedback record
|
| 70 |
-
feedback_record = {
|
| 71 |
-
"experiment_id": experiment_id,
|
| 72 |
-
"user_id": user_id,
|
| 73 |
-
"stage": stage,
|
| 74 |
-
"feedback_type": feedback_type,
|
| 75 |
-
"rejection_reason": rejection_reason,
|
| 76 |
-
"rejection_text": rejection_text,
|
| 77 |
-
"campaign_name": campaign_name,
|
| 78 |
-
"config_name": config_name,
|
| 79 |
-
"brand": brand,
|
| 80 |
-
"message_header": message_header,
|
| 81 |
-
"message_body": message_body,
|
| 82 |
-
"timestamp": datetime.now()
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
# Initialize feedback list if not exists
|
| 86 |
-
if feedback_list_key not in st.session_state:
|
| 87 |
-
st.session_state[feedback_list_key] = []
|
| 88 |
-
|
| 89 |
-
# Check if feedback already exists for this (experiment_id, user_id, stage)
|
| 90 |
-
existing_idx = None
|
| 91 |
-
for idx, fb in enumerate(st.session_state[feedback_list_key]):
|
| 92 |
-
if (fb['experiment_id'] == experiment_id and
|
| 93 |
-
fb['user_id'] == user_id and
|
| 94 |
-
fb['stage'] == stage):
|
| 95 |
-
existing_idx = idx
|
| 96 |
-
break
|
| 97 |
-
|
| 98 |
-
if existing_idx is not None:
|
| 99 |
-
# Update existing feedback
|
| 100 |
-
st.session_state[feedback_list_key][existing_idx] = feedback_record
|
| 101 |
-
else:
|
| 102 |
-
# Add new feedback
|
| 103 |
-
st.session_state[feedback_list_key].append(feedback_record)
|
| 104 |
-
|
| 105 |
-
return True
|
| 106 |
-
|
| 107 |
-
except Exception as e:
|
| 108 |
-
st.error(f"Error adding feedback: {e}")
|
| 109 |
-
return False
|
| 110 |
-
|
| 111 |
-
@staticmethod
|
| 112 |
-
def get_feedback(
|
| 113 |
-
experiment_id: str,
|
| 114 |
-
user_id: int,
|
| 115 |
-
stage: int,
|
| 116 |
-
feedback_list_key: str = "current_feedbacks"
|
| 117 |
-
) -> Optional[Dict]:
|
| 118 |
-
"""
|
| 119 |
-
Get feedback for a specific message from session_state.
|
| 120 |
-
|
| 121 |
-
Args:
|
| 122 |
-
experiment_id: Experiment ID
|
| 123 |
-
user_id: User ID
|
| 124 |
-
stage: Stage number
|
| 125 |
-
feedback_list_key: Key in session_state for feedback list
|
| 126 |
-
|
| 127 |
-
Returns:
|
| 128 |
-
dict or None: Feedback record or None if not found
|
| 129 |
-
"""
|
| 130 |
-
if feedback_list_key not in st.session_state:
|
| 131 |
-
return None
|
| 132 |
-
|
| 133 |
-
for fb in st.session_state[feedback_list_key]:
|
| 134 |
-
if (fb['experiment_id'] == experiment_id and
|
| 135 |
-
fb['user_id'] == user_id and
|
| 136 |
-
fb['stage'] == stage):
|
| 137 |
-
return fb
|
| 138 |
-
|
| 139 |
-
return None
|
| 140 |
-
|
| 141 |
-
@staticmethod
|
| 142 |
-
def delete_feedback(
|
| 143 |
-
experiment_id: str,
|
| 144 |
-
user_id: int,
|
| 145 |
-
stage: int,
|
| 146 |
-
feedback_list_key: str = "current_feedbacks"
|
| 147 |
-
) -> bool:
|
| 148 |
-
"""
|
| 149 |
-
Delete feedback for a specific message from session_state.
|
| 150 |
-
|
| 151 |
-
Args:
|
| 152 |
-
experiment_id: Experiment ID
|
| 153 |
-
user_id: User ID
|
| 154 |
-
stage: Stage number
|
| 155 |
-
feedback_list_key: Key in session_state for feedback list
|
| 156 |
-
|
| 157 |
-
Returns:
|
| 158 |
-
bool: True if deleted successfully
|
| 159 |
-
"""
|
| 160 |
-
try:
|
| 161 |
-
if feedback_list_key not in st.session_state:
|
| 162 |
-
return False
|
| 163 |
-
|
| 164 |
-
# Find and remove feedback
|
| 165 |
-
st.session_state[feedback_list_key] = [
|
| 166 |
-
fb for fb in st.session_state[feedback_list_key]
|
| 167 |
-
if not (fb['experiment_id'] == experiment_id and
|
| 168 |
-
fb['user_id'] == user_id and
|
| 169 |
-
fb['stage'] == stage)
|
| 170 |
-
]
|
| 171 |
-
|
| 172 |
-
return True
|
| 173 |
-
|
| 174 |
-
except Exception as e:
|
| 175 |
-
st.error(f"Error deleting feedback: {e}")
|
| 176 |
-
return False
|
| 177 |
-
|
| 178 |
-
@staticmethod
|
| 179 |
-
def get_feedback_stats(
|
| 180 |
-
experiment_id: str,
|
| 181 |
-
total_messages: int,
|
| 182 |
-
feedback_list_key: str = "current_feedbacks"
|
| 183 |
-
) -> Dict:
|
| 184 |
-
"""
|
| 185 |
-
Get feedback statistics for an experiment from session_state.
|
| 186 |
-
|
| 187 |
-
Args:
|
| 188 |
-
experiment_id: Experiment ID
|
| 189 |
-
total_messages: Total number of messages generated
|
| 190 |
-
feedback_list_key: Key in session_state for feedback list
|
| 191 |
-
|
| 192 |
-
Returns:
|
| 193 |
-
dict: Feedback statistics
|
| 194 |
-
"""
|
| 195 |
-
if feedback_list_key not in st.session_state:
|
| 196 |
-
return {
|
| 197 |
-
"total_feedback": 0,
|
| 198 |
-
"total_rejects": 0,
|
| 199 |
-
"reject_rate": 0.0,
|
| 200 |
-
"rejection_reasons": {}
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
# Filter feedback for this experiment
|
| 204 |
-
experiment_feedback = [
|
| 205 |
-
fb for fb in st.session_state[feedback_list_key]
|
| 206 |
-
if fb['experiment_id'] == experiment_id
|
| 207 |
-
]
|
| 208 |
-
|
| 209 |
-
total_feedback = len(experiment_feedback)
|
| 210 |
-
total_rejects = len([fb for fb in experiment_feedback if fb['feedback_type'] == 'reject'])
|
| 211 |
-
|
| 212 |
-
# Count rejection reasons
|
| 213 |
-
rejection_reasons = {}
|
| 214 |
-
for fb in experiment_feedback:
|
| 215 |
-
if fb['feedback_type'] == 'reject' and fb['rejection_reason']:
|
| 216 |
-
reason_label = REJECTION_REASONS.get(fb['rejection_reason'], fb['rejection_reason'])
|
| 217 |
-
rejection_reasons[reason_label] = rejection_reasons.get(reason_label, 0) + 1
|
| 218 |
-
|
| 219 |
-
# Calculate rejection rate based on total messages
|
| 220 |
-
reject_rate = (total_rejects / total_messages * 100) if total_messages > 0 else 0.0
|
| 221 |
-
|
| 222 |
-
return {
|
| 223 |
-
"total_feedback": total_feedback,
|
| 224 |
-
"total_rejects": total_rejects,
|
| 225 |
-
"reject_rate": reject_rate,
|
| 226 |
-
"rejection_reasons": rejection_reasons
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
@staticmethod
|
| 230 |
-
def get_stage_feedback_stats(
|
| 231 |
-
experiment_id: str,
|
| 232 |
-
messages_df: pd.DataFrame,
|
| 233 |
-
feedback_list_key: str = "current_feedbacks"
|
| 234 |
-
) -> pd.DataFrame:
|
| 235 |
-
"""
|
| 236 |
-
Get feedback statistics by stage from session_state.
|
| 237 |
-
|
| 238 |
-
Args:
|
| 239 |
-
experiment_id: Experiment ID
|
| 240 |
-
messages_df: DataFrame of all messages
|
| 241 |
-
feedback_list_key: Key in session_state for feedback list
|
| 242 |
-
|
| 243 |
-
Returns:
|
| 244 |
-
pd.DataFrame: Stage-wise feedback statistics
|
| 245 |
-
"""
|
| 246 |
-
if feedback_list_key not in st.session_state or 'stage' not in messages_df.columns:
|
| 247 |
-
return pd.DataFrame(columns=['stage', 'total_messages', 'total_feedback', 'rejects', 'reject_rate'])
|
| 248 |
-
|
| 249 |
-
# Filter feedback for this experiment
|
| 250 |
-
experiment_feedback = [
|
| 251 |
-
fb for fb in st.session_state[feedback_list_key]
|
| 252 |
-
if fb['experiment_id'] == experiment_id
|
| 253 |
-
]
|
| 254 |
-
|
| 255 |
-
# Get total messages per stage
|
| 256 |
-
stage_message_counts = messages_df.groupby('stage').size().to_dict()
|
| 257 |
-
|
| 258 |
-
# Group feedback by stage
|
| 259 |
-
stage_stats = []
|
| 260 |
-
for stage in sorted(messages_df['stage'].unique()):
|
| 261 |
-
stage_feedback = [fb for fb in experiment_feedback if fb['stage'] == stage]
|
| 262 |
-
total_feedback = len(stage_feedback)
|
| 263 |
-
rejects = len([fb for fb in stage_feedback if fb['feedback_type'] == 'reject'])
|
| 264 |
-
|
| 265 |
-
total_messages_for_stage = stage_message_counts.get(stage, 0)
|
| 266 |
-
reject_rate = (rejects / total_messages_for_stage * 100) if total_messages_for_stage > 0 else 0.0
|
| 267 |
-
|
| 268 |
-
stage_stats.append({
|
| 269 |
-
'stage': stage,
|
| 270 |
-
'total_messages': total_messages_for_stage,
|
| 271 |
-
'total_feedback': total_feedback,
|
| 272 |
-
'rejects': rejects,
|
| 273 |
-
'reject_rate': reject_rate
|
| 274 |
-
})
|
| 275 |
-
|
| 276 |
-
return pd.DataFrame(stage_stats)
|
| 277 |
-
|
| 278 |
-
@staticmethod
|
| 279 |
-
def get_all_rejection_reasons() -> Dict[str, str]:
|
| 280 |
-
"""
|
| 281 |
-
Get all available rejection reasons.
|
| 282 |
-
|
| 283 |
-
Returns:
|
| 284 |
-
dict: Dictionary mapping keys to labels
|
| 285 |
-
"""
|
| 286 |
-
return REJECTION_REASONS.copy()
|
| 287 |
-
|
| 288 |
-
@staticmethod
|
| 289 |
-
def get_rejection_reason_label(reason_key: str) -> str:
|
| 290 |
-
"""
|
| 291 |
-
Get human-readable label for rejection reason key.
|
| 292 |
-
|
| 293 |
-
Args:
|
| 294 |
-
reason_key: Rejection reason key
|
| 295 |
-
|
| 296 |
-
Returns:
|
| 297 |
-
str: Human-readable label
|
| 298 |
-
"""
|
| 299 |
-
return REJECTION_REASONS.get(reason_key, reason_key)
|
| 300 |
-
|
| 301 |
-
@staticmethod
|
| 302 |
-
def clear_all_feedbacks(feedback_list_key: str = "current_feedbacks"):
|
| 303 |
-
"""
|
| 304 |
-
Clear all feedbacks from session_state.
|
| 305 |
-
|
| 306 |
-
Args:
|
| 307 |
-
feedback_list_key: Key in session_state for feedback list
|
| 308 |
-
"""
|
| 309 |
-
if feedback_list_key in st.session_state:
|
| 310 |
-
st.session_state[feedback_list_key] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visualization/utils/theme.py
DELETED
|
@@ -1,335 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Brand-specific theming module for the AI Messaging System Visualization Tool.
|
| 3 |
-
|
| 4 |
-
Provides dynamic color schemes and styling based on selected brand.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import streamlit as st
|
| 8 |
-
from typing import Dict, Tuple
|
| 9 |
-
|
| 10 |
-
# Brand color definitions (primary, secondary, accent)
|
| 11 |
-
BRAND_COLORS = {
|
| 12 |
-
"base": {
|
| 13 |
-
"primary": "#B8860B", # Dark Goldenrod
|
| 14 |
-
"secondary": "#2C2C2C", # Dark gray
|
| 15 |
-
"accent": "#1A1A1A", # Black
|
| 16 |
-
"text": "#FFFFFF", # White
|
| 17 |
-
"background": "#0D0D0D", # Very dark
|
| 18 |
-
"sidebar_bg": "#7A5C00" # Darker gold for sidebar
|
| 19 |
-
},
|
| 20 |
-
"drumeo": {
|
| 21 |
-
"primary": "#5DADE2", # Light blue
|
| 22 |
-
"secondary": "#B8860B", # Dark Goldenrod
|
| 23 |
-
"accent": "#2874A6", # Darker blue
|
| 24 |
-
"text": "#FFFFFF", # White
|
| 25 |
-
"background": "#1C2833", # Dark blue-gray
|
| 26 |
-
"sidebar_bg": "#1A4D6B" # Darker blue for sidebar
|
| 27 |
-
},
|
| 28 |
-
"pianote": {
|
| 29 |
-
"primary": "#EC7063", # Light red
|
| 30 |
-
"secondary": "#B8860B", # Dark Goldenrod
|
| 31 |
-
"accent": "#C0392B", # Darker red
|
| 32 |
-
"text": "#FFFFFF", # White
|
| 33 |
-
"background": "#1C1C1C", # Dark
|
| 34 |
-
"sidebar_bg": "#8B2500" # Darker red for sidebar
|
| 35 |
-
},
|
| 36 |
-
"guitareo": {
|
| 37 |
-
"primary": "#58D68D", # Light green
|
| 38 |
-
"secondary": "#B8860B", # Dark Goldenrod
|
| 39 |
-
"accent": "#229954", # Darker green
|
| 40 |
-
"text": "#FFFFFF", # White
|
| 41 |
-
"background": "#1C2E1F", # Dark green-gray
|
| 42 |
-
"sidebar_bg": "#1B5E20" # Darker green for sidebar
|
| 43 |
-
},
|
| 44 |
-
"singeo": {
|
| 45 |
-
"primary": "#BB8FCE", # Light purple
|
| 46 |
-
"secondary": "#B8860B", # Dark Goldenrod
|
| 47 |
-
"accent": "#7D3C98", # Darker purple
|
| 48 |
-
"text": "#FFFFFF", # White
|
| 49 |
-
"background": "#1F1926", # Dark purple-gray
|
| 50 |
-
"sidebar_bg": "#4A148C" # Darker purple for sidebar
|
| 51 |
-
}
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
# Brand emojis
|
| 55 |
-
BRAND_EMOJIS = {
|
| 56 |
-
"drumeo": "🥁",
|
| 57 |
-
"pianote": "🎹",
|
| 58 |
-
"guitareo": "🎸",
|
| 59 |
-
"singeo": "🎤"
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def get_brand_theme(brand: str) -> Dict[str, str]:
|
| 64 |
-
"""
|
| 65 |
-
Get color theme for specified brand.
|
| 66 |
-
|
| 67 |
-
Args:
|
| 68 |
-
brand: Brand name (drumeo, pianote, guitareo, singeo)
|
| 69 |
-
|
| 70 |
-
Returns:
|
| 71 |
-
dict: Color theme dictionary
|
| 72 |
-
"""
|
| 73 |
-
brand_lower = brand.lower() if brand else "base"
|
| 74 |
-
return BRAND_COLORS.get(brand_lower, BRAND_COLORS["base"])
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def get_brand_emoji(brand: str) -> str:
|
| 78 |
-
"""
|
| 79 |
-
Get emoji for specified brand.
|
| 80 |
-
|
| 81 |
-
Args:
|
| 82 |
-
brand: Brand name
|
| 83 |
-
|
| 84 |
-
Returns:
|
| 85 |
-
str: Brand emoji
|
| 86 |
-
"""
|
| 87 |
-
brand_lower = brand.lower() if brand else ""
|
| 88 |
-
return BRAND_EMOJIS.get(brand_lower, "🎵")
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def apply_theme(brand: str = None):
|
| 92 |
-
"""
|
| 93 |
-
Apply brand-specific theme to Streamlit app.
|
| 94 |
-
|
| 95 |
-
Args:
|
| 96 |
-
brand: Brand name (optional, uses session state if not provided)
|
| 97 |
-
"""
|
| 98 |
-
if brand is None:
|
| 99 |
-
brand = st.session_state.get("selected_brand", "base")
|
| 100 |
-
|
| 101 |
-
theme = get_brand_theme(brand)
|
| 102 |
-
|
| 103 |
-
# Custom CSS for brand theming
|
| 104 |
-
css = f"""
|
| 105 |
-
<style>
|
| 106 |
-
/* Global styles */
|
| 107 |
-
.stApp {{
|
| 108 |
-
background-color: {theme['background']};
|
| 109 |
-
}}
|
| 110 |
-
|
| 111 |
-
/* Text colors */
|
| 112 |
-
h1, h2, h3, h4, h5, h6 {{
|
| 113 |
-
color: {theme['primary']} !important;
|
| 114 |
-
}}
|
| 115 |
-
|
| 116 |
-
p, span, div {{
|
| 117 |
-
color: {theme['text']} !important;
|
| 118 |
-
}}
|
| 119 |
-
|
| 120 |
-
/* Button styles */
|
| 121 |
-
.stButton > button {{
|
| 122 |
-
background-color: {theme['primary']};
|
| 123 |
-
color: {theme['background']};
|
| 124 |
-
border: none;
|
| 125 |
-
border-radius: 8px;
|
| 126 |
-
padding: 0.5rem 1rem;
|
| 127 |
-
font-weight: 600;
|
| 128 |
-
transition: all 0.3s ease;
|
| 129 |
-
}}
|
| 130 |
-
|
| 131 |
-
.stButton > button:hover {{
|
| 132 |
-
background-color: {theme['secondary']};
|
| 133 |
-
transform: translateY(-2px);
|
| 134 |
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
| 135 |
-
}}
|
| 136 |
-
|
| 137 |
-
/* Download button */
|
| 138 |
-
.stDownloadButton > button {{
|
| 139 |
-
background-color: {theme['accent']};
|
| 140 |
-
color: {theme['text']};
|
| 141 |
-
border-radius: 8px;
|
| 142 |
-
font-weight: 600;
|
| 143 |
-
}}
|
| 144 |
-
|
| 145 |
-
/* Input fields */
|
| 146 |
-
.stTextInput > div > div > input,
|
| 147 |
-
.stTextArea > div > div > textarea,
|
| 148 |
-
.stNumberInput > div > div > input {{
|
| 149 |
-
background-color: {theme['secondary']};
|
| 150 |
-
color: {theme['text']};
|
| 151 |
-
border: 1px solid {theme['primary']};
|
| 152 |
-
border-radius: 5px;
|
| 153 |
-
}}
|
| 154 |
-
|
| 155 |
-
/* Select boxes */
|
| 156 |
-
.stSelectbox > div > div {{
|
| 157 |
-
background-color: {theme['secondary']};
|
| 158 |
-
color: {theme['text']};
|
| 159 |
-
border-radius: 5px;
|
| 160 |
-
}}
|
| 161 |
-
|
| 162 |
-
/* Multiselect */
|
| 163 |
-
.stMultiSelect > div > div {{
|
| 164 |
-
background-color: {theme['secondary']};
|
| 165 |
-
border-radius: 5px;
|
| 166 |
-
}}
|
| 167 |
-
|
| 168 |
-
/* Tabs */
|
| 169 |
-
.stTabs [data-baseweb="tab-list"] {{
|
| 170 |
-
gap: 8px;
|
| 171 |
-
}}
|
| 172 |
-
|
| 173 |
-
.stTabs [data-baseweb="tab"] {{
|
| 174 |
-
background-color: {theme['secondary']};
|
| 175 |
-
color: {theme['text']};
|
| 176 |
-
border-radius: 5px 5px 0 0;
|
| 177 |
-
padding: 10px 20px;
|
| 178 |
-
font-weight: 600;
|
| 179 |
-
}}
|
| 180 |
-
|
| 181 |
-
.stTabs [aria-selected="true"] {{
|
| 182 |
-
background-color: {theme['primary']};
|
| 183 |
-
color: {theme['background']};
|
| 184 |
-
}}
|
| 185 |
-
|
| 186 |
-
/* Progress bar */
|
| 187 |
-
.stProgress > div > div > div {{
|
| 188 |
-
background-color: {theme['primary']};
|
| 189 |
-
}}
|
| 190 |
-
|
| 191 |
-
/* Expanders */
|
| 192 |
-
.streamlit-expanderHeader {{
|
| 193 |
-
background-color: {theme['secondary']};
|
| 194 |
-
color: {theme['primary']};
|
| 195 |
-
border-radius: 5px;
|
| 196 |
-
font-weight: 600;
|
| 197 |
-
}}
|
| 198 |
-
|
| 199 |
-
/* Cards/Containers */
|
| 200 |
-
.element-container {{
|
| 201 |
-
background-color: transparent;
|
| 202 |
-
}}
|
| 203 |
-
|
| 204 |
-
/* Sidebar */
|
| 205 |
-
[data-testid="stSidebar"] {{
|
| 206 |
-
background-color: {theme['sidebar_bg']};
|
| 207 |
-
}}
|
| 208 |
-
|
| 209 |
-
[data-testid="stSidebar"] h1,
|
| 210 |
-
[data-testid="stSidebar"] h2,
|
| 211 |
-
[data-testid="stSidebar"] h3 {{
|
| 212 |
-
color: {theme['text']} !important;
|
| 213 |
-
}}
|
| 214 |
-
|
| 215 |
-
[data-testid="stSidebar"] p,
|
| 216 |
-
[data-testid="stSidebar"] span,
|
| 217 |
-
[data-testid="stSidebar"] div,
|
| 218 |
-
[data-testid="stSidebar"] label {{
|
| 219 |
-
color: {theme['text']} !important;
|
| 220 |
-
}}
|
| 221 |
-
|
| 222 |
-
/* Metrics */
|
| 223 |
-
[data-testid="stMetricValue"] {{
|
| 224 |
-
color: {theme['primary']} !important;
|
| 225 |
-
font-size: 2rem;
|
| 226 |
-
font-weight: bold;
|
| 227 |
-
}}
|
| 228 |
-
|
| 229 |
-
/* Divider */
|
| 230 |
-
hr {{
|
| 231 |
-
border-color: {theme['primary']};
|
| 232 |
-
opacity: 0.3;
|
| 233 |
-
}}
|
| 234 |
-
|
| 235 |
-
/* Checkbox */
|
| 236 |
-
.stCheckbox {{
|
| 237 |
-
color: {theme['text']};
|
| 238 |
-
}}
|
| 239 |
-
|
| 240 |
-
/* Radio */
|
| 241 |
-
.stRadio > div {{
|
| 242 |
-
color: {theme['text']};
|
| 243 |
-
}}
|
| 244 |
-
|
| 245 |
-
/* Success/Info/Warning/Error boxes */
|
| 246 |
-
.stSuccess {{
|
| 247 |
-
background-color: rgba(88, 214, 141, 0.1);
|
| 248 |
-
color: {theme['text']};
|
| 249 |
-
}}
|
| 250 |
-
|
| 251 |
-
.stInfo {{
|
| 252 |
-
background-color: rgba(93, 173, 226, 0.1);
|
| 253 |
-
color: {theme['text']};
|
| 254 |
-
}}
|
| 255 |
-
|
| 256 |
-
.stWarning {{
|
| 257 |
-
background-color: rgba(184, 134, 11, 0.1);
|
| 258 |
-
color: {theme['text']};
|
| 259 |
-
}}
|
| 260 |
-
|
| 261 |
-
.stError {{
|
| 262 |
-
background-color: rgba(236, 112, 99, 0.1);
|
| 263 |
-
color: {theme['text']};
|
| 264 |
-
}}
|
| 265 |
-
|
| 266 |
-
/* DataFrames */
|
| 267 |
-
.dataframe {{
|
| 268 |
-
border: 1px solid {theme['primary']};
|
| 269 |
-
}}
|
| 270 |
-
|
| 271 |
-
/* Custom card styling */
|
| 272 |
-
.message-card {{
|
| 273 |
-
background-color: {theme['secondary']};
|
| 274 |
-
border-left: 4px solid {theme['primary']};
|
| 275 |
-
border-radius: 8px;
|
| 276 |
-
padding: 1rem;
|
| 277 |
-
margin: 0.5rem 0;
|
| 278 |
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 279 |
-
}}
|
| 280 |
-
|
| 281 |
-
.message-card:hover {{
|
| 282 |
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
| 283 |
-
transform: translateX(4px);
|
| 284 |
-
transition: all 0.3s ease;
|
| 285 |
-
}}
|
| 286 |
-
|
| 287 |
-
/* Small text */
|
| 288 |
-
.small {{
|
| 289 |
-
font-size: 0.85rem;
|
| 290 |
-
opacity: 0.7;
|
| 291 |
-
}}
|
| 292 |
-
|
| 293 |
-
/* Badge styles */
|
| 294 |
-
.badge {{
|
| 295 |
-
display: inline-block;
|
| 296 |
-
padding: 0.25rem 0.5rem;
|
| 297 |
-
border-radius: 12px;
|
| 298 |
-
font-size: 0.75rem;
|
| 299 |
-
font-weight: 600;
|
| 300 |
-
background-color: {theme['accent']};
|
| 301 |
-
color: {theme['text']};
|
| 302 |
-
}}
|
| 303 |
-
</style>
|
| 304 |
-
"""
|
| 305 |
-
|
| 306 |
-
st.markdown(css, unsafe_allow_html=True)
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
def create_card(content: str, key: str = None) -> None:
|
| 310 |
-
"""
|
| 311 |
-
Create a styled card container.
|
| 312 |
-
|
| 313 |
-
Args:
|
| 314 |
-
content: HTML content to display in card
|
| 315 |
-
key: Optional unique key for the card
|
| 316 |
-
"""
|
| 317 |
-
st.markdown(
|
| 318 |
-
f'<div class="message-card">{content}</div>',
|
| 319 |
-
unsafe_allow_html=True
|
| 320 |
-
)
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
def create_badge(text: str, color: str = None) -> str:
|
| 324 |
-
"""
|
| 325 |
-
Create a styled badge element.
|
| 326 |
-
|
| 327 |
-
Args:
|
| 328 |
-
text: Badge text
|
| 329 |
-
color: Optional custom color
|
| 330 |
-
|
| 331 |
-
Returns:
|
| 332 |
-
str: HTML for badge
|
| 333 |
-
"""
|
| 334 |
-
style = f'background-color: {color};' if color else ''
|
| 335 |
-
return f'<span class="badge" style="{style}">{text}</span>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|