Danialebrat commited on
Commit
5ae6520
·
verified ·
1 Parent(s): 3766ea9

Delete visualization

Browse files
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>'