Commit ·
10b0429
1
Parent(s): f69d731
Updating UI with all changes
Browse files- README.md +0 -11
- app.py +10 -375
- data/UI_users/drumeo_users.csv +0 -101
- data/UI_users/guitareo_users.csv +0 -101
- data/UI_users/pianote_users.csv +0 -101
- data/UI_users/singeo_users.csv +0 -101
- pages/1_Campaign_Builder.py +17 -1090
- pages/2_Message_Viewer.py +17 -929
- pages/4_Analytics.py +17 -876
- pages/5_Historical_Analytics.py +17 -371
- utils/__init__.py +0 -28
- utils/auth.py +0 -101
- utils/config_manager.py +0 -361
- utils/data_loader.py +0 -230
- utils/db_manager.py +0 -505
- utils/experiment_runner.py +0 -276
- utils/feedback_manager.py +0 -414
- utils/session_feedback_manager.py +0 -310
- utils/theme.py +0 -335
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
|
| 3 |
-
|
| 4 |
"""
|
| 5 |
|
| 6 |
-
import streamlit as st
|
| 7 |
import sys
|
| 8 |
from pathlib import Path
|
| 9 |
-
from dotenv import load_dotenv
|
| 10 |
|
| 11 |
-
#
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 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 |
-
#
|
| 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 |
-
#
|
| 32 |
-
|
| 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 |
-
|
| 3 |
-
|
| 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 |
-
#
|
| 31 |
-
|
| 32 |
-
|
| 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 |
-
|
|
|
|
| 544 |
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
original_output_file = permes.UI_OUTPUT_FILE
|
| 548 |
-
ui_experiment_id = None
|
| 549 |
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 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 |
-
|
| 3 |
-
|
| 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 |
-
#
|
| 27 |
-
|
| 28 |
-
|
| 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 |
-
|
| 447 |
-
|
| 448 |
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
stage_message = None
|
| 452 |
-
for msg in message_content:
|
| 453 |
-
if isinstance(msg, dict):
|
| 454 |
-
stage_message = msg
|
| 455 |
-
break
|
| 456 |
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 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 |
-
|
| 3 |
-
|
| 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
|
| 12 |
|
| 13 |
-
#
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
import plotly.graph_objects as go
|
| 17 |
-
except ModuleNotFoundError:
|
| 18 |
-
st.error("""
|
| 19 |
-
⚠️ **Missing Dependency: plotly**
|
| 20 |
|
| 21 |
-
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
2. Install plotly: `pip install plotly>=5.17.0`
|
| 26 |
-
3. Or install all requirements: `pip install -r visualization/requirements.txt`
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 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 |
-
|
| 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 |
-
|
| 3 |
-
|
| 4 |
"""
|
| 5 |
|
| 6 |
-
import streamlit as st
|
| 7 |
import sys
|
| 8 |
-
import os
|
| 9 |
from pathlib import Path
|
| 10 |
-
import
|
| 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 |
-
#
|
| 212 |
-
|
| 213 |
-
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
'total_users', 'total_stages', 'total_rejects', 'rejection_rate']
|
| 218 |
|
| 219 |
-
#
|
| 220 |
-
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 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 |
-
|
| 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>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|