Danialebrat commited on
Commit
10b0429
·
1 Parent(s): f69d731

Updating UI with all changes

Browse files
README.md CHANGED
@@ -74,17 +74,6 @@ visualization/
74
  └── ARCHITECTURE_REFACTOR_GUIDE.md # Technical refactoring guide
75
  ```
76
 
77
- ### Deprecated Files (No Longer Used)
78
-
79
- These files are legacy and no longer part of the active codebase:
80
- - ~~`utils/data_loader.py`~~ - Replaced by session_state loading
81
- - ~~`utils/feedback_manager.py`~~ - Replaced by SessionFeedbackManager
82
- - ~~`data/configs/`~~ - Configs now cached locally but loaded from Snowflake
83
- - ~~`data/feedback/`~~ - Feedback now in session_state → Snowflake
84
- - ~~`ai_messaging_system_v2/Data/ui_output/`~~ - No more file outputs
85
-
86
- ---
87
-
88
  ## 🗄️ Snowflake Database Schema
89
 
90
  ### Tables
 
74
  └── ARCHITECTURE_REFACTOR_GUIDE.md # Technical refactoring guide
75
  ```
76
 
 
 
 
 
 
 
 
 
 
 
 
77
  ## 🗄️ Snowflake Database Schema
78
 
79
  ### Tables
app.py CHANGED
@@ -1,384 +1,19 @@
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**")
 
1
  """
2
+ Hugging Face Spaces wrapper for AI Messaging System Visualization Tool
3
+ This wrapper imports and runs the main app from the visualization folder.
4
  """
5
 
 
6
  import sys
7
  from pathlib import Path
 
8
 
9
+ # Add the visualization directory to Python path
10
+ visualization_dir = Path(__file__).parent / "visualization"
11
+ sys.path.insert(0, str(visualization_dir))
12
+ sys.path.insert(0, str(Path(__file__).parent))
 
 
 
 
 
13
 
14
+ # Change to visualization directory context for relative imports
 
 
 
 
 
 
 
15
  import os
16
+ os.chdir(visualization_dir)
17
 
18
+ # Import and run the main app
19
+ from visualization import app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/1_Campaign_Builder.py CHANGED
@@ -1,1100 +1,27 @@
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!")
 
1
  """
2
+ Hugging Face Spaces wrapper for Campaign Builder page
3
+ This wrapper imports and runs the page from the visualization folder.
4
  """
5
 
 
6
  import sys
7
  from pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  import os
9
 
10
+ # Add the project root and visualization directory to Python path
11
+ project_root = Path(__file__).parent.parent
12
+ visualization_dir = project_root / "visualization"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ sys.path.insert(0, str(visualization_dir))
15
+ sys.path.insert(0, str(project_root))
16
 
17
+ # Change to visualization directory for relative imports
18
+ os.chdir(visualization_dir)
 
 
19
 
20
+ # Import the actual page module
21
+ import importlib.util
22
+ spec = importlib.util.spec_from_file_location(
23
+ "campaign_builder",
24
+ visualization_dir / "pages" / "1_Campaign_Builder.py"
25
+ )
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/2_Message_Viewer.py CHANGED
@@ -1,939 +1,27 @@
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
- # Display thumbnail as clickable image
370
- col1, col2 = st.columns([1, 2])
371
-
372
- with col1:
373
- st.image(thumbnail_url, use_container_width=True)
374
-
375
- with col2:
376
- st.markdown(f"**{title}**")
377
- if web_url and web_url != '#':
378
- st.markdown(f"[🔗 View Content]({web_url})")
379
- else:
380
- # No thumbnail, just show title and link
381
- st.markdown(f"**{title}**")
382
- if web_url and web_url != '#':
383
- st.markdown(f"[🔗 View Content]({web_url})")
384
-
385
-
386
- def display_user_centric_messages(filtered_df, exp_id, key_suffix, show_feedback_option, msgs_per_page):
387
- """Display messages in user-centric view for a single experiment."""
388
- if 'user_id' not in filtered_df.columns:
389
- st.warning("No user_id column found in messages.")
390
- return
391
-
392
- unique_users = filtered_df['user_id'].unique()
393
-
394
- # Pagination
395
- total_users = len(unique_users)
396
- users_per_page = msgs_per_page
397
-
398
- if total_users > users_per_page:
399
- page = st.number_input(
400
- f"Page",
401
- min_value=1,
402
- max_value=(total_users // users_per_page) + 1,
403
- value=1,
404
- key=f"page_{key_suffix}"
405
- )
406
- start_idx = (page - 1) * users_per_page
407
- end_idx = min(start_idx + users_per_page, total_users)
408
- users_to_display = unique_users[start_idx:end_idx]
409
-
410
- st.markdown(f"*Showing users {start_idx + 1}-{end_idx} of {total_users}*")
411
- else:
412
- users_to_display = unique_users
413
-
414
- # Display each user
415
- for user_id in users_to_display:
416
- user_messages = filtered_df[filtered_df['user_id'] == user_id].sort_values('stage')
417
-
418
- if len(user_messages) == 0:
419
- continue
420
-
421
- # Get user info from first message
422
- first_msg = user_messages.iloc[0]
423
- user_email = first_msg.get('email', 'N/A')
424
- user_first_name = first_msg.get('first_name', 'N/A')
425
-
426
- # User expander
427
- with st.expander(f"👤 User {user_id} - {user_first_name} ({user_email})", expanded=False):
428
- # User profile
429
- st.markdown("##### 📋 User Profile")
430
-
431
- profile_cols = st.columns(4)
432
- profile_fields = ['first_name', 'country', 'instrument', 'subscription_status']
433
-
434
- for idx, field in enumerate(profile_fields):
435
- if field in first_msg:
436
- profile_cols[idx % 4].markdown(f"**{field.replace('_', ' ').title()}:** {first_msg[field]}")
437
-
438
- st.markdown("---")
439
-
440
- # Display all stages for this user
441
- st.markdown("##### 📨 Messages Across All Stages")
442
-
443
- for _, row in user_messages.iterrows():
444
- stage = row['stage']
445
 
446
- # Parse message content
447
- message_content = parse_message_content(row.get('message', ''))
448
 
449
- if message_content:
450
- # Find the message for this stage
451
- stage_message = None
452
- for msg in message_content:
453
- if isinstance(msg, dict):
454
- stage_message = msg
455
- break
456
 
457
- if stage_message:
458
- # Create message card
459
- st.markdown(f"**Stage {stage}**")
460
-
461
- msg_col1, msg_col2 = st.columns([4, 1])
462
-
463
- with msg_col1:
464
- header = stage_message.get('header', 'No header')
465
- message_text = stage_message.get('message', 'No message')
466
-
467
- st.markdown(f"**📧 Header:** {header}")
468
- st.markdown(f"**💬 Message:** {message_text}")
469
-
470
- # Show recommendation if available
471
- if 'thumbnail_url' in stage_message or 'web_url_path' in stage_message:
472
- rec_data = {
473
- 'title': stage_message.get('title', 'Recommended Content'),
474
- 'web_url_path': stage_message.get('web_url_path', '#'),
475
- 'thumbnail_url': stage_message.get('thumbnail_url', '')
476
- }
477
- display_recommendation(rec_data)
478
- elif 'recommendation' in row and pd.notna(row['recommendation']):
479
- display_recommendation(row['recommendation'])
480
-
481
- with msg_col2:
482
- # Feedback section
483
- if show_feedback_option:
484
- # Determine feedback list key based on mode
485
- if is_ab_mode:
486
- feedback_key = "feedbacks_a" if "exp_a" in key_suffix else "feedbacks_b"
487
- else:
488
- feedback_key = "current_feedbacks"
489
-
490
- existing_feedback = SessionFeedbackManager.get_feedback(
491
- exp_id, user_id, stage, feedback_list_key=feedback_key
492
- )
493
-
494
- if existing_feedback:
495
- st.error("❌ Rejected")
496
- if st.button(f"Undo", key=f"undo_{user_id}_{stage}_{key_suffix}"):
497
- SessionFeedbackManager.delete_feedback(exp_id, user_id, stage, feedback_list_key=feedback_key)
498
- st.rerun()
499
- else:
500
- if st.button("🚫 Reject", key=f"reject_{user_id}_{stage}_{key_suffix}"):
501
- st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}"] = True
502
- st.rerun()
503
-
504
- # Feedback form
505
- if st.session_state.get(f"show_feedback_form_{user_id}_{stage}_{key_suffix}", False):
506
- with st.form(f"feedback_form_{user_id}_{stage}_{key_suffix}"):
507
- st.markdown("**Why is this message rejected?**")
508
-
509
- rejection_reasons = SessionFeedbackManager.get_all_rejection_reasons()
510
- selected_reason = st.selectbox(
511
- "Rejection Reason",
512
- list(rejection_reasons.keys()),
513
- format_func=lambda x: rejection_reasons[x],
514
- key=f"reason_{user_id}_{stage}_{key_suffix}"
515
- )
516
-
517
- rejection_text = st.text_area(
518
- "Additional Comments (Optional)",
519
- key=f"text_{user_id}_{stage}_{key_suffix}"
520
- )
521
-
522
- col1, col2 = st.columns(2)
523
-
524
- with col1:
525
- if st.form_submit_button("💾 Save", use_container_width=True):
526
- # Determine feedback list key based on mode
527
- if is_ab_mode:
528
- feedback_key = "feedbacks_a" if "exp_a" in key_suffix else "feedbacks_b"
529
- else:
530
- feedback_key = "current_feedbacks"
531
-
532
- # Get config name from session_state
533
- config_name = row.get('campaign_name', 'Unknown')
534
-
535
- SessionFeedbackManager.add_feedback(
536
- experiment_id=exp_id,
537
- user_id=user_id,
538
- stage=stage,
539
- feedback_type="reject",
540
- rejection_reason=selected_reason,
541
- rejection_text=rejection_text,
542
- campaign_name=row.get('campaign_name'),
543
- config_name=config_name,
544
- brand=brand,
545
- message_header=stage_message.get('header', 'N/A'),
546
- message_body=stage_message.get('message', 'N/A'),
547
- feedback_list_key=feedback_key
548
- )
549
- st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}"] = False
550
- st.success("✅ Feedback saved to memory")
551
- st.rerun()
552
-
553
- with col2:
554
- if st.form_submit_button("❌ Cancel", use_container_width=True):
555
- st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}"] = False
556
- st.rerun()
557
-
558
- st.markdown("---")
559
-
560
-
561
- def display_stage_centric_messages(filtered_df, exp_id, key_suffix, show_feedback_option, msgs_per_page, stages_to_display):
562
- """Display messages in stage-centric view for a single experiment."""
563
- # Group by stage
564
- for stage in sorted(stages_to_display if stages_to_display else filtered_df['stage'].unique()):
565
- stage_messages = filtered_df[filtered_df['stage'] == stage]
566
-
567
- if len(stage_messages) == 0:
568
- continue
569
-
570
- with st.expander(f"📨 Stage {stage} - {len(stage_messages)} messages", expanded=(stage == 1)):
571
- st.markdown(f"##### Stage {stage} Messages")
572
-
573
- # Pagination for this stage
574
- total_stage_messages = len(stage_messages)
575
- stage_messages_per_page = msgs_per_page
576
-
577
- if total_stage_messages > stage_messages_per_page:
578
- page = st.number_input(
579
- f"Page for Stage {stage}",
580
- min_value=1,
581
- max_value=(total_stage_messages // stage_messages_per_page) + 1,
582
- value=1,
583
- key=f"page_stage_{stage}_{key_suffix}"
584
- )
585
- start_idx = (page - 1) * stage_messages_per_page
586
- end_idx = min(start_idx + stage_messages_per_page, total_stage_messages)
587
- messages_to_display = stage_messages.iloc[start_idx:end_idx]
588
-
589
- st.markdown(f"*Showing messages {start_idx + 1}-{end_idx} of {total_stage_messages}*")
590
- else:
591
- messages_to_display = stage_messages
592
-
593
- # Display messages
594
- for idx, row in messages_to_display.iterrows():
595
- user_id = row.get('user_id', 'N/A')
596
- user_email = row.get('email', 'N/A')
597
- user_name = row.get('first_name', 'N/A')
598
-
599
- # Parse message content
600
- message_content = parse_message_content(row.get('message', ''))
601
-
602
- if message_content:
603
- stage_message = None
604
- for msg in message_content:
605
- if isinstance(msg, dict):
606
- stage_message = msg
607
- break
608
-
609
- if stage_message:
610
- # Message card
611
- msg_col1, msg_col2 = st.columns([4, 1])
612
-
613
- with msg_col1:
614
- st.markdown(f"**User:** {user_name} ({user_email}) - ID: {user_id}")
615
-
616
- header = stage_message.get('header', 'No header')
617
- message_text = stage_message.get('message', 'No message')
618
-
619
- st.markdown(f"**📧 Header:** {header}")
620
- st.markdown(f"**💬 Message:** {message_text}")
621
-
622
- # Show recommendation if available
623
- if 'thumbnail_url' in stage_message or 'web_url_path' in stage_message:
624
- rec_data = {
625
- 'title': stage_message.get('title', 'Recommended Content'),
626
- 'web_url_path': stage_message.get('web_url_path', '#'),
627
- 'thumbnail_url': stage_message.get('thumbnail_url', '')
628
- }
629
- display_recommendation(rec_data)
630
- elif 'recommendation' in row and pd.notna(row['recommendation']):
631
- display_recommendation(row['recommendation'])
632
-
633
- with msg_col2:
634
- # Feedback section
635
- if show_feedback_option:
636
- # Determine feedback list key based on mode
637
- if is_ab_mode:
638
- feedback_key = "feedbacks_a" if "exp_a" in key_suffix else "feedbacks_b"
639
- else:
640
- feedback_key = "current_feedbacks"
641
-
642
- existing_feedback = SessionFeedbackManager.get_feedback(
643
- exp_id, user_id, stage, feedback_list_key=feedback_key
644
- )
645
-
646
- if existing_feedback:
647
- st.error("❌ Rejected")
648
- if st.button(f"Undo", key=f"undo_{user_id}_{stage}_{key_suffix}_sc"):
649
- SessionFeedbackManager.delete_feedback(exp_id, user_id, stage, feedback_list_key=feedback_key)
650
- st.rerun()
651
- else:
652
- if st.button("🚫 Reject", key=f"reject_{user_id}_{stage}_{key_suffix}_sc"):
653
- st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc"] = True
654
- st.rerun()
655
-
656
- # Feedback form
657
- if st.session_state.get(f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc", False):
658
- with st.form(f"feedback_form_{user_id}_{stage}_{key_suffix}_sc"):
659
- st.markdown("**Why is this message rejected?**")
660
-
661
- rejection_reasons = SessionFeedbackManager.get_all_rejection_reasons()
662
- selected_reason = st.selectbox(
663
- "Rejection Reason",
664
- list(rejection_reasons.keys()),
665
- format_func=lambda x: rejection_reasons[x],
666
- key=f"reason_{user_id}_{stage}_{key_suffix}_sc"
667
- )
668
-
669
- rejection_text = st.text_area(
670
- "Additional Comments (Optional)",
671
- key=f"text_{user_id}_{stage}_{key_suffix}_sc"
672
- )
673
-
674
- col1, col2 = st.columns(2)
675
-
676
- with col1:
677
- if st.form_submit_button("💾 Save", use_container_width=True):
678
- # Determine feedback list key based on mode
679
- if is_ab_mode:
680
- feedback_key = "feedbacks_a" if "exp_a" in key_suffix else "feedbacks_b"
681
- else:
682
- feedback_key = "current_feedbacks"
683
-
684
- # Get config name from session_state
685
- config_name = row.get('campaign_name', 'Unknown')
686
-
687
- SessionFeedbackManager.add_feedback(
688
- experiment_id=exp_id,
689
- user_id=user_id,
690
- stage=stage,
691
- feedback_type="reject",
692
- rejection_reason=selected_reason,
693
- rejection_text=rejection_text,
694
- campaign_name=row.get('campaign_name'),
695
- config_name=config_name,
696
- brand=brand,
697
- message_header=stage_message.get('header', 'N/A'),
698
- message_body=stage_message.get('message', 'N/A'),
699
- feedback_list_key=feedback_key
700
- )
701
- st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc"] = False
702
- st.success("✅ Feedback saved to memory")
703
- st.rerun()
704
-
705
- with col2:
706
- if st.form_submit_button("❌ Cancel", use_container_width=True):
707
- st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc"] = False
708
- st.rerun()
709
-
710
- st.markdown("---")
711
-
712
- # Display messages based on mode and view
713
- if is_ab_mode:
714
- # A/B Testing Mode - Show both experiments side by side
715
- if view_mode == "User-Centric":
716
- st.subheader("👤 User-Centric View")
717
- st.markdown("*All stages for each user - comparing both experiments*")
718
- else:
719
- st.subheader("📊 Stage-Centric View")
720
- st.markdown("*All users for each stage - comparing both experiments*")
721
-
722
- # Create two columns for A/B comparison
723
- col_a, col_b = st.columns(2)
724
-
725
- with col_a:
726
- st.markdown("### 🅰️ Experiment A")
727
- if view_mode == "User-Centric":
728
- display_user_centric_messages(
729
- filtered_messages_a,
730
- experiment_a_id,
731
- "exp_a",
732
- show_feedback,
733
- messages_per_page
734
- )
735
- else:
736
- display_stage_centric_messages(
737
- filtered_messages_a,
738
- experiment_a_id,
739
- "exp_a",
740
- show_feedback,
741
- messages_per_page,
742
- selected_stages
743
- )
744
-
745
- with col_b:
746
- st.markdown("### 🅱️ Experiment B")
747
- if view_mode == "User-Centric":
748
- display_user_centric_messages(
749
- filtered_messages_b,
750
- experiment_b_id,
751
- "exp_b",
752
- show_feedback,
753
- messages_per_page
754
- )
755
- else:
756
- display_stage_centric_messages(
757
- filtered_messages_b,
758
- experiment_b_id,
759
- "exp_b",
760
- show_feedback,
761
- messages_per_page,
762
- selected_stages
763
- )
764
-
765
- else:
766
- # Single Experiment Mode - Original behavior
767
- if view_mode == "User-Centric":
768
- st.subheader("👤 User-Centric View")
769
- st.markdown("*All stages for each user*")
770
- display_user_centric_messages(
771
- filtered_messages,
772
- experiment_id,
773
- "single",
774
- show_feedback,
775
- messages_per_page
776
- )
777
- else:
778
- st.subheader("📊 Stage-Centric View")
779
- st.markdown("*All users for each stage*")
780
- display_stage_centric_messages(
781
- filtered_messages,
782
- experiment_id,
783
- "single",
784
- show_feedback,
785
- messages_per_page,
786
- selected_stages
787
- )
788
-
789
- st.markdown("---")
790
-
791
- # ============================================================================
792
- # STORE RESULTS TO SNOWFLAKE SECTION (CRITICAL NEW FEATURE)
793
- # ============================================================================
794
-
795
- st.subheader("💾 Store Experiment Results to Snowflake")
796
-
797
- st.markdown("""
798
- **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.
799
- """)
800
-
801
- if is_ab_mode:
802
- # AB Testing Mode
803
- st.info(f"🔬 Ready to store A/B test results for:\n- **Experiment A**: {experiment_a_id}\n- **Experiment B**: {experiment_b_id}")
804
-
805
- col1, col2 = st.columns(2)
806
-
807
- with col1:
808
- st.metric("Experiment A Feedback", len(st.session_state.get('feedbacks_a', [])))
809
- st.metric("Experiment A Metadata", len(st.session_state.get('experiment_a_metadata', [])))
810
-
811
- with col2:
812
- st.metric("Experiment B Feedback", len(st.session_state.get('feedbacks_b', [])))
813
- st.metric("Experiment B Metadata", len(st.session_state.get('experiment_b_metadata', [])))
814
-
815
- else:
816
- # Single Mode
817
- st.info(f"📊 Ready to store experiment results for: **{experiment_id}**")
818
-
819
- col1, col2 = st.columns(2)
820
-
821
- with col1:
822
- st.metric("Total Feedback", len(st.session_state.get('current_feedbacks', [])))
823
-
824
- with col2:
825
- st.metric("Metadata Records", len(st.session_state.get('current_experiment_metadata', [])))
826
-
827
- st.markdown("---")
828
-
829
- if st.button("💾 Store Results to Snowflake", type="primary", use_container_width=True):
830
- with st.spinner("Storing results to Snowflake..."):
831
- try:
832
- # Create Snowflake session
833
- session = create_snowflake_session()
834
- db_manager = UIDatabaseManager(session)
835
-
836
- if is_ab_mode:
837
- # Store A/B Test Results
838
- st.write("Storing Experiment A metadata...")
839
- for meta in st.session_state.get('experiment_a_metadata', []):
840
- db_manager.store_experiment_metadata(meta)
841
-
842
- st.write("Storing Experiment B metadata...")
843
- for meta in st.session_state.get('experiment_b_metadata', []):
844
- db_manager.store_experiment_metadata(meta)
845
-
846
- st.write("Storing Experiment A feedback...")
847
- feedbacks_a = st.session_state.get('feedbacks_a', [])
848
- if feedbacks_a:
849
- db_manager.store_feedback_batch(feedbacks_a)
850
-
851
- st.write("Storing Experiment B feedback...")
852
- feedbacks_b = st.session_state.get('feedbacks_b', [])
853
- if feedbacks_b:
854
- db_manager.store_feedback_batch(feedbacks_b)
855
-
856
- session.close()
857
-
858
- st.success(f"""
859
- ✅ **Successfully stored A/B test results!**
860
-
861
- - **Experiment A** ({experiment_a_id}): {len(st.session_state.get('experiment_a_metadata', []))} metadata records, {len(feedbacks_a)} feedback records
862
- - **Experiment B** ({experiment_b_id}): {len(st.session_state.get('experiment_b_metadata', []))} metadata records, {len(feedbacks_b)} feedback records
863
-
864
- Results are now available in Historical Analytics.
865
- """)
866
-
867
- else:
868
- # Store Single Experiment Results
869
- st.write("Storing experiment metadata...")
870
- for meta in st.session_state.get('current_experiment_metadata', []):
871
- db_manager.store_experiment_metadata(meta)
872
-
873
- st.write("Storing feedback...")
874
- feedbacks = st.session_state.get('current_feedbacks', [])
875
- if feedbacks:
876
- db_manager.store_feedback_batch(feedbacks)
877
-
878
- session.close()
879
-
880
- st.success(f"""
881
- ✅ **Successfully stored experiment results!**
882
-
883
- - **Experiment** ({experiment_id}): {len(st.session_state.get('current_experiment_metadata', []))} metadata records, {len(feedbacks)} feedback records
884
-
885
- Results are now available in Historical Analytics.
886
- """)
887
-
888
- st.balloons()
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
- )
 
1
  """
2
+ Hugging Face Spaces wrapper for Message Viewer page
3
+ This wrapper imports and runs the page from the visualization folder.
 
4
  """
5
 
 
6
  import sys
7
  from pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  import os
9
 
10
+ # Add the project root and visualization directory to Python path
11
+ project_root = Path(__file__).parent.parent
12
+ visualization_dir = project_root / "visualization"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ sys.path.insert(0, str(visualization_dir))
15
+ sys.path.insert(0, str(project_root))
16
 
17
+ # Change to visualization directory for relative imports
18
+ os.chdir(visualization_dir)
 
 
 
 
 
19
 
20
+ # Import the actual page module
21
+ import importlib.util
22
+ spec = importlib.util.spec_from_file_location(
23
+ "message_viewer",
24
+ visualization_dir / "pages" / "2_Message_Viewer.py"
25
+ )
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/4_Analytics.py CHANGED
@@ -1,886 +1,27 @@
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("---")
 
1
  """
2
+ Hugging Face Spaces wrapper for Analytics page
3
+ This wrapper imports and runs the page from the visualization folder.
 
 
4
  """
5
 
 
6
  import sys
7
  from pathlib import Path
8
+ import os
9
 
10
+ # Add the project root and visualization directory to Python path
11
+ project_root = Path(__file__).parent.parent
12
+ visualization_dir = project_root / "visualization"
 
 
 
 
13
 
14
+ sys.path.insert(0, str(visualization_dir))
15
+ sys.path.insert(0, str(project_root))
16
 
17
+ # Change to visualization directory for relative imports
18
+ os.chdir(visualization_dir)
 
 
19
 
20
+ # Import the actual page module
21
+ import importlib.util
22
+ spec = importlib.util.spec_from_file_location(
23
+ "analytics",
24
+ visualization_dir / "pages" / "4_Analytics.py"
 
 
 
 
 
 
 
 
 
 
 
25
  )
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/5_Historical_Analytics.py CHANGED
@@ -1,381 +1,27 @@
1
  """
2
- Page 5: Historical Analytics
3
- View performance metrics and insights from all previous experiments stored in Snowflake.
4
  """
5
 
6
- import streamlit as st
7
  import sys
8
- import os
9
  from pathlib import Path
10
- import pandas as pd
11
- from datetime import datetime, timedelta
12
- from dotenv import load_dotenv
13
-
14
- # Load environment variables
15
- env_path = Path(__file__).parent.parent.parent / '.env'
16
- if env_path.exists():
17
- load_dotenv(env_path)
18
- else:
19
- # Try parent directory
20
- parent_env_path = Path(__file__).parent.parent.parent.parent / '.env'
21
- if parent_env_path.exists():
22
- load_dotenv(parent_env_path)
23
-
24
- # Try to import plotly with helpful error message
25
- try:
26
- import plotly.express as px
27
- import plotly.graph_objects as go
28
- except ModuleNotFoundError:
29
- st.error("""
30
- ⚠️ **Missing Dependency: plotly**
31
-
32
- Plotly is not installed in the current Python environment.
33
-
34
- **To fix this:**
35
- 1. Make sure you're running Streamlit from the correct Python environment
36
- 2. Install plotly: `pip install plotly>=5.17.0`
37
- 3. Or install all requirements: `pip install -r visualization/requirements.txt`
38
-
39
- **Current Python:** {}
40
- """.format(sys.executable))
41
- st.stop()
42
-
43
- # Add parent directories to path
44
- sys.path.insert(0, str(Path(__file__).parent.parent.parent))
45
-
46
- from utils.auth import check_authentication
47
- from utils.theme import apply_theme, get_brand_emoji, get_brand_theme
48
- from utils.db_manager import UIDatabaseManager
49
-
50
- # Helper function to create Snowflake session
51
- def create_snowflake_session():
52
- """Create a Snowflake session using environment variables."""
53
- try:
54
- from snowflake.snowpark import Session
55
- connection_parameters = {
56
- "user": os.getenv("SNOWFLAKE_USER"),
57
- "password": os.getenv("SNOWFLAKE_PASSWORD"),
58
- "account": os.getenv("SNOWFLAKE_ACCOUNT"),
59
- "role": os.getenv("SNOWFLAKE_ROLE"),
60
- "database": os.getenv("SNOWFLAKE_DATABASE"),
61
- "warehouse": os.getenv("SNOWFLAKE_WAREHOUSE"),
62
- "schema": os.getenv("SNOWFLAKE_SCHEMA"),
63
- }
64
- return Session.builder.configs(connection_parameters).create()
65
- except Exception as e:
66
- st.error(f"Failed to create Snowflake session: {e}")
67
- import traceback
68
- st.error(f"Traceback: {traceback.format_exc()}")
69
- return None
70
-
71
- # Page configuration
72
- st.set_page_config(
73
- page_title="Historical Analytics",
74
- page_icon="📚",
75
- layout="wide"
76
- )
77
-
78
- # Check authentication
79
- if not check_authentication():
80
- st.error("🔒 Please login first")
81
- st.stop()
82
-
83
- # Check if brand is selected
84
- if "selected_brand" not in st.session_state or not st.session_state.selected_brand:
85
- st.error("⚠️ Please select a brand from the home page first")
86
- st.stop()
87
-
88
- brand = st.session_state.selected_brand
89
- apply_theme(brand)
90
- theme = get_brand_theme(brand)
91
-
92
- # Page Header
93
- emoji = get_brand_emoji(brand)
94
- st.title(f"📚 Historical Analytics - {emoji} {brand.title()}")
95
- st.markdown("**View performance metrics from all previous experiments stored in Snowflake**")
96
-
97
- st.markdown("---")
98
-
99
- # Load Data Button
100
- col1, col2, col3 = st.columns([1, 2, 1])
101
- with col2:
102
- if st.button("📊 Load Historical Data from Snowflake", use_container_width=True, type="primary"):
103
- with st.spinner("Loading historical data from Snowflake..."):
104
- # Create Snowflake session
105
- session = create_snowflake_session()
106
-
107
- if session:
108
- try:
109
- db_manager = UIDatabaseManager(session)
110
-
111
- # Load experiment summary (joins metadata + feedback)
112
- experiments_df = db_manager.get_experiment_summary(brand=brand)
113
-
114
- # Store in session_state
115
- st.session_state['historical_experiments'] = experiments_df
116
- st.session_state['historical_data_loaded'] = True
117
-
118
- # Close session
119
- db_manager.close()
120
-
121
- st.success(f"✅ Loaded {len(experiments_df)} experiments from Snowflake!")
122
- st.rerun()
123
-
124
- except Exception as e:
125
- st.error(f"Error loading historical data: {e}")
126
- if session:
127
- session.close()
128
- else:
129
- st.error("Failed to connect to Snowflake. Please check your credentials.")
130
-
131
- st.markdown("---")
132
-
133
- # Check if data is loaded
134
- if not st.session_state.get('historical_data_loaded', False):
135
- st.info("👆 Click the button above to load historical experiment data from Snowflake")
136
- st.stop()
137
-
138
- # Get loaded data
139
- experiments_df = st.session_state.get('historical_experiments', pd.DataFrame())
140
-
141
- if experiments_df is None or len(experiments_df) == 0:
142
- st.warning("⚠️ No historical experiments found in Snowflake. Generate and store experiments first using Campaign Builder.")
143
- st.stop()
144
-
145
- # Sidebar - Filters
146
- with st.sidebar:
147
- st.header("🔍 Filters")
148
-
149
- # Date range filter
150
- st.subheader("Date Range")
151
- show_all = st.checkbox("Show All Experiments", value=True)
152
-
153
- start_date = None
154
- end_date = None
155
-
156
- if not show_all:
157
- # Use actual timestamps from data
158
- if 'start_time' in experiments_df.columns:
159
- experiments_df['start_time'] = pd.to_datetime(experiments_df['start_time'])
160
- min_date = experiments_df['start_time'].min().date()
161
- max_date = experiments_df['start_time'].max().date()
162
-
163
- start_date = st.date_input("Start Date", min_date)
164
- end_date = st.date_input("End Date", max_date)
165
-
166
- # Filter by date range
167
- experiments_df = experiments_df[
168
- (experiments_df['start_time'].dt.date >= start_date) &
169
- (experiments_df['start_time'].dt.date <= end_date)
170
- ]
171
-
172
- st.markdown("---")
173
-
174
- # Refresh button
175
- if st.button("🔄 Reload Data", use_container_width=True):
176
- st.session_state['historical_data_loaded'] = False
177
- st.rerun()
178
-
179
- # ============================================================================
180
- # OVERVIEW STATISTICS
181
- # ============================================================================
182
- st.header("📊 Overview Statistics")
183
-
184
- col1, col2, col3, col4 = st.columns(4)
185
-
186
- with col1:
187
- st.metric("Total Experiments", len(experiments_df))
188
-
189
- with col2:
190
- total_messages = experiments_df['total_messages'].sum() if 'total_messages' in experiments_df.columns else 0
191
- st.metric("Total Messages", f"{total_messages:,}")
192
-
193
- with col3:
194
- total_rejects = experiments_df['total_rejects'].sum() if 'total_rejects' in experiments_df.columns else 0
195
- st.metric("Total Rejections", f"{total_rejects:,}")
196
-
197
- with col4:
198
- avg_rejection_rate = experiments_df['rejection_rate'].mean() if 'rejection_rate' in experiments_df.columns else 0
199
- st.metric("Avg Rejection Rate", f"{avg_rejection_rate:.1f}%")
200
-
201
- st.markdown("---")
202
-
203
- # ============================================================================
204
- # EXPERIMENTS SUMMARY TABLE
205
- # ============================================================================
206
- st.header("📈 All Experiments")
207
-
208
- # Display summary table
209
- display_df = experiments_df.copy()
210
 
211
- # Format columns for display
212
- if 'start_time' in display_df.columns:
213
- display_df['start_time'] = pd.to_datetime(display_df['start_time']).dt.strftime('%Y-%m-%d %H:%M')
214
 
215
- # Select columns to display
216
- display_columns = ['campaign_name', 'config_name', 'start_time', 'total_messages',
217
- 'total_users', 'total_stages', 'total_rejects', 'rejection_rate']
218
 
219
- # Only show columns that exist
220
- display_columns = [col for col in display_columns if col in display_df.columns]
221
 
222
- st.dataframe(
223
- display_df[display_columns],
224
- use_container_width=True,
225
- hide_index=True,
226
- column_config={
227
- "campaign_name": "Campaign",
228
- "config_name": "Config",
229
- "start_time": "Date",
230
- "total_messages": "Messages",
231
- "total_users": "Users",
232
- "total_stages": "Stages",
233
- "total_rejects": "Rejects",
234
- "rejection_rate": st.column_config.NumberColumn(
235
- "Reject Rate (%)",
236
- format="%.1f%%"
237
- )
238
- }
239
  )
240
-
241
- st.markdown("---")
242
-
243
- # ============================================================================
244
- # REJECTION RATE TREND OVER TIME
245
- # ============================================================================
246
- st.header("📉 Rejection Rate Trend Over Time")
247
-
248
- if 'start_time' in experiments_df.columns and 'rejection_rate' in experiments_df.columns:
249
- # Sort by time
250
- trend_df = experiments_df.copy()
251
- trend_df['start_time'] = pd.to_datetime(trend_df['start_time'])
252
- trend_df = trend_df.sort_values('start_time')
253
-
254
- fig = go.Figure()
255
-
256
- fig.add_trace(go.Scatter(
257
- x=trend_df['start_time'],
258
- y=trend_df['rejection_rate'],
259
- mode='lines+markers',
260
- name='Rejection Rate',
261
- line=dict(color=theme['accent'], width=3),
262
- marker=dict(size=10),
263
- text=trend_df['campaign_name'],
264
- hovertemplate='<b>%{text}</b><br>Date: %{x}<br>Rejection Rate: %{y:.1f}%<extra></extra>'
265
- ))
266
-
267
- fig.update_layout(
268
- xaxis_title="Experiment Date",
269
- yaxis_title="Rejection Rate (%)",
270
- template="plotly_dark",
271
- paper_bgcolor='rgba(0,0,0,0)',
272
- plot_bgcolor='rgba(0,0,0,0)',
273
- hovermode='closest'
274
- )
275
-
276
- st.plotly_chart(fig, use_container_width=True)
277
- else:
278
- st.info("Not enough data for trend visualization")
279
-
280
- st.markdown("---")
281
-
282
- # ============================================================================
283
- # PERFORMANCE COMPARISON
284
- # ============================================================================
285
- st.header("📊 Performance Comparison")
286
-
287
- if len(experiments_df) > 1 and 'config_name' in experiments_df.columns:
288
- # Group by config and compare
289
- config_summary = experiments_df.groupby('config_name').agg({
290
- 'rejection_rate': 'mean',
291
- 'total_messages': 'sum',
292
- 'total_rejects': 'sum'
293
- }).reset_index()
294
-
295
- config_summary = config_summary.sort_values('rejection_rate')
296
-
297
- col1, col2 = st.columns(2)
298
-
299
- with col1:
300
- # Bar chart comparing configs
301
- fig = px.bar(
302
- config_summary,
303
- x='config_name',
304
- y='rejection_rate',
305
- title="Average Rejection Rate by Configuration",
306
- color='rejection_rate',
307
- color_continuous_scale='RdYlGn_r',
308
- text='rejection_rate'
309
- )
310
-
311
- fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
312
- fig.update_layout(
313
- template="plotly_dark",
314
- paper_bgcolor='rgba(0,0,0,0)',
315
- plot_bgcolor='rgba(0,0,0,0)',
316
- xaxis_title="Configuration",
317
- yaxis_title="Rejection Rate (%)"
318
- )
319
-
320
- st.plotly_chart(fig, use_container_width=True)
321
-
322
- with col2:
323
- # Summary metrics
324
- best_config = config_summary.iloc[0]
325
- worst_config = config_summary.iloc[-1]
326
-
327
- st.markdown("### 🏆 Best Performing Config")
328
- st.metric(
329
- best_config['config_name'],
330
- f"{best_config['rejection_rate']:.1f}%",
331
- delta=f"{worst_config['rejection_rate'] - best_config['rejection_rate']:.1f}% better than worst"
332
- )
333
-
334
- st.markdown("### 📊 Config Comparison")
335
- st.dataframe(
336
- config_summary,
337
- use_container_width=True,
338
- hide_index=True,
339
- column_config={
340
- "config_name": "Configuration",
341
- "rejection_rate": st.column_config.NumberColumn(
342
- "Avg Rejection Rate (%)",
343
- format="%.1f%%"
344
- ),
345
- "total_messages": st.column_config.NumberColumn(
346
- "Total Messages",
347
- format="%d"
348
- ),
349
- "total_rejects": st.column_config.NumberColumn(
350
- "Total Rejects",
351
- format="%d"
352
- )
353
- }
354
- )
355
- else:
356
- st.info("Need at least 2 experiments for performance comparison")
357
-
358
- st.markdown("---")
359
-
360
- # Export options
361
- st.header("💾 Export Historical Data")
362
-
363
- col1, col2 = st.columns(2)
364
-
365
- with col1:
366
- if st.button("📥 Export Experiments Summary", use_container_width=True):
367
- csv = experiments_df.to_csv(index=False, encoding='utf-8')
368
- st.download_button(
369
- label="⬇️ Download Summary CSV",
370
- data=csv,
371
- file_name=f"{brand}_experiments_summary_{datetime.now().strftime('%Y%m%d')}.csv",
372
- mime="text/csv",
373
- use_container_width=True
374
- )
375
-
376
- with col2:
377
- st.button("📥 Export Detailed Feedback", use_container_width=True, disabled=True)
378
- st.caption("💡 Detailed feedback export coming soon. Use Snowflake queries to access feedback data directly.")
379
-
380
- st.markdown("---")
381
- st.markdown("**💡 Tip:** Use historical analytics to track improvements over time and identify which configurations perform best!")
 
1
  """
2
+ Hugging Face Spaces wrapper for Historical Analytics page
3
+ This wrapper imports and runs the page from the visualization folder.
4
  """
5
 
 
6
  import sys
 
7
  from pathlib import Path
8
+ import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # Add the project root and visualization directory to Python path
11
+ project_root = Path(__file__).parent.parent
12
+ visualization_dir = project_root / "visualization"
13
 
14
+ sys.path.insert(0, str(visualization_dir))
15
+ sys.path.insert(0, str(project_root))
 
16
 
17
+ # Change to visualization directory for relative imports
18
+ os.chdir(visualization_dir)
19
 
20
+ # Import the actual page module
21
+ import importlib.util
22
+ spec = importlib.util.spec_from_file_location(
23
+ "historical_analytics",
24
+ visualization_dir / "pages" / "5_Historical_Analytics.py"
 
 
 
 
 
 
 
 
 
 
 
 
25
  )
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/db_manager.py DELETED
@@ -1,505 +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 close(self):
500
- """Close the Snowflake session."""
501
- try:
502
- self.session.close()
503
- print("✅ Snowflake session closed")
504
- except Exception as e:
505
- st.error(f"Error closing session: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": "#FFD700", # Gold
14
- "secondary": "#2C2C2C", # Dark gray
15
- "accent": "#1A1A1A", # Black
16
- "text": "#FFFFFF", # White
17
- "background": "#0D0D0D", # Very dark
18
- "sidebar_bg": "#8B7500" # Darker gold for sidebar
19
- },
20
- "drumeo": {
21
- "primary": "#5DADE2", # Light blue
22
- "secondary": "#FFD700", # Gold
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": "#FFD700", # Gold
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": "#FFD700", # Gold
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": "#FFD700", # Gold
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(255, 215, 0, 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>'