import json, os from io import StringIO import pandas as pd import streamlit as st from snowflake.snowpark import Session from bs4 import BeautifulSoup from Messaging_system.Permes import Permes from dotenv import load_dotenv load_dotenv() # ────────────────────────────────────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────────────────────────────────────── @st.cache_data def load_data(buf) -> pd.DataFrame: return pd.read_csv(buf) def load_config_(file_path: str) -> dict: with open(file_path) as f: return json.load(f) def get_credential(key): return os.getenv(key) or st.secrets.get(key) # Authorized emails AUTHORIZED_EMAILS = { "danial@musora.com", "danial.ebrat@gmail.com", "simon@musora.com", "una@musora.com", "mark@musora.com", "gabriel@musora.com", "nikki@musora.com" } # --- one env var that holds the shared password / token --------------------- VALID_TOKEN = get_credential("APP_TOKEN") # --- small utility ----------------------------------------------------------- def verify_login(email: str, token: str) -> bool: """True ⇢ both the address and the token are valid.""" return (email.lower().strip() in AUTHORIZED_EMAILS) and (token == VALID_TOKEN) # --- UI: show a form until the user is authenticated ------------------------ if not st.session_state.get("authenticated", False): st.title("🔐 Sign-in") st.markdown( "Access is limited to authorised users. " "Enter your **e-mail** and the shared **access token**." ) with st.form("login"): email = st.text_input("e-mail") token = st.text_input("access token", type="password") submitted = st.form_submit_button("Log in") if submitted: if verify_login(email, token): st.session_state.authenticated = True st.session_state.user_email = email st.success("Login successful – redirecting…") st.rerun() # reloads the app, now “inside” else: st.error("⛔ Invalid e-mail or token") # IMPORTANT: stop executing the rest of the app while not logged-in st.stop() def init_state() -> None: defaults = dict( involve_recsys_result=False, involve_last_interaction=False, valid_instructions="", invalid_instructions="", messaging_type="push", generated=False, include_recommendation=False, data=None, brand=None, recsys_contents=[], csv_output=None, users_message=None, messaging_mode=None, target_column=None, ugc_column=None, identifier_column=None, input_validator=None, selected_input_features=None, selected_features=None, additional_instructions=None, segment_info="", message_style="", sample_example="", CTA="", all_features=None, number_of_messages=1, instructionset={}, segment_name="", number_of_samples=10, selected_source_features=[], platform=None, generate_clicked=False, ) for k, v in defaults.items(): st.session_state.setdefault(k, v) # ────────────────────────────────────────────────────────────────────────────── # PAGE CONFIG + THEME # ────────────────────────────────────────────────────────────────────────────── st.set_page_config( page_title="Personalized Message Generator", page_icon="📬", layout="wide", initial_sidebar_state="expanded" ) st.markdown( """ """, unsafe_allow_html=True ) # ────────────────────────────────────────────────────────────────────────────── # SIDEBAR – the “control panel” # ────────────────────────────────────────────────────────────────────────────── init_state() with st.sidebar: # st.header("📂 Upload your CSV") # uploaded_file = st.file_uploader("Choose file", type="csv") uploaded_file = "Data/Singeo_Camp.csv" if uploaded_file: st.session_state.data = load_data(uploaded_file) st.success("File loaded!") st.markdown("---") if st.session_state.data is not None: # ─ Identifier # id_col = st.selectbox( # "Identifier column", # st.session_state.data.columns, # key="identifier_column" # ) id_col = "user_id" st.session_state.identifier_column = id_col # ─ Brand st.selectbox( "Brand *", ["drumeo", "pianote", "guitareo", "singeo"], key="brand", ) # ─ Personalisation st.text_area("Segment info *", key="segment_info") st.text_area("CTA (Call to Action) *", key="CTA") with st.expander("🔧 Optional tone & examples"): st.text_area("Message style", key="message_style", placeholder="Be kind and friendly…") st.text_area("Additional instructions", key="additional_instructions", placeholder="e.g. Mention the number weeks since their last practice") st.text_area("Sample example", key="sample_example", placeholder="Hello! We have crafted…") st.number_input("Number of samples (default = 10)", 1, 50, key="number_of_samples") # ─ Sequential messages st.number_input("Sequential messages / user", 1, 12, 1, key="number_of_messages") st.text_input("Segment name", key="segment_name", placeholder="no_recent_activity") if st.session_state.number_of_messages > 1: st.caption("Additional per-message instructions") for i in range(1, st.session_state.number_of_messages + 1): st.text_input(f"Message {i} instruction", key=f"instr_{i}") # ─ Source feature selection st.multiselect( "Source features", ["instrument", "weeks_since_last_interaction", "birthday_reminder"], default=["instrument"], key="selected_source_features" ) # ─ Rec-sys st.checkbox("Include content recommendation", key="include_recommendation") if st.session_state.include_recommendation: st.multiselect( "Recommendation types", ["song", "workout", "quick_tips", "course"], key="recsys_contents" ) st.markdown("---") if st.button("🚀 Generate messages", key="generate"): st.session_state.generate_clicked = True # ask for a new run st.session_state.generated = False # forget old results # generate = st.button("🚀 Generate messages") # st.session_state["generate_clicked"] = generate # ────────────────────────────────────────────────────────────────────────────── # MAIN AREA – three tabs # ────────────────────────────────────────────────────────────────────────────── tab0, tab2 = st.tabs( ["📊 Data preview", "📨 Results"]) # ------------------------------------------------------------------ TAB 0 ---# with tab0: st.header("📊 Data preview") if st.session_state.data is not None: st.dataframe(st.session_state.data.head(100)) else: st.info("Upload a CSV to preview it here.") # ------------------------------------------------------------------ TAB 2 ---# with tab2: st.header("📨 Generated messages") # Run generation only once per click if st.session_state.generate_clicked and not st.session_state.generated: # ─ simple validation if not st.session_state.CTA.strip() or not st.session_state.segment_info.strip() or not st.session_state.brand.strip(): st.error("CTA, Segment info, and brand are mandatory 🚫") st.stop() if st.session_state.get("generate_clicked", False): # ─ build Snowflake session conn = dict( user=get_credential("snowflake_user"), password=get_credential("snowflake_password"), account=get_credential("snowflake_account"), role=get_credential("snowflake_role"), database=get_credential("snowflake_database"), warehouse=get_credential("snowflake_warehouse"), schema=get_credential("snowflake_schema") ) config = load_config_("Config_files/message_system_config.json") session = Session.builder.configs(conn).create() # ─ prepare parameters st.session_state.messaging_mode = ( "recsys_result" if st.session_state.include_recommendation else "message" ) st.session_state.involve_recsys_result = st.session_state.include_recommendation st.session_state.instructionset = { i: st.session_state.get(f"instr_{i}") for i in range(1, st.session_state.number_of_messages + 1) if st.session_state.get(f"instr_{i}", "").strip() } # ─ progress callback prog = st.progress(0) status = st.empty() def cb(done, total): pct = int(done / total * 100) prog.progress(pct) status.write(f"{pct}%") permes = Permes() df_msg = permes.create_personalize_messages( session=session, users=st.session_state.data, brand=st.session_state.brand, config_file=config, openai_api_key=get_credential("OPENAI_API"), CTA=st.session_state.CTA, segment_info=st.session_state.segment_info, number_of_samples=st.session_state.number_of_samples, message_style=st.session_state.message_style, sample_example=st.session_state.sample_example, selected_input_features=st.session_state.selected_features, selected_source_features=st.session_state.selected_source_features, additional_instructions=st.session_state.additional_instructions, platform=st.session_state.messaging_type, involve_recsys_result=st.session_state.involve_recsys_result, messaging_mode=st.session_state.messaging_mode, identifier_column=st.session_state.identifier_column, target_column=st.session_state.target_column, recsys_contents=st.session_state.recsys_contents, progress_callback=cb, number_of_messages=st.session_state.number_of_messages, instructionset=st.session_state.instructionset, segment_name=st.session_state.segment_name ) # ─ cache output st.session_state.users_message = df_msg st.session_state.csv_output = df_msg.to_csv( index=False, encoding="utf-8-sig") st.session_state.generated = True st.session_state.generate_clicked = False prog.empty(); status.empty() st.balloons() # ============================================================= if st.session_state.get("generated", False): df = st.session_state.users_message id_col = st.session_state.identifier_column or "" id_col_lower = id_col.lower() # expandable per-user cards for i, (_, row) in enumerate(df.iterrows(), start=1): user_id = row.get(id_col_lower, "(no ID)") with st.expander(f"{i}. User ID: {user_id}", expanded=(i == 1)): # --- Features st.write("##### 👤 Features") feats = st.session_state.selected_source_features or [] cols = st.columns(3) for idx, feature in enumerate(feats): val = row.get(feature, "—") cols[idx % 3].markdown(f"**{feature}**: {val}") st.markdown("---") # --- Messages st.write("##### ✉️ Messages") raw = row.get("message", "") # try to parse JSON if it's a str if isinstance(raw, str): try: blob = json.loads(raw) except json.JSONDecodeError: st.error(f"Could not parse JSON for user {user_id}") continue elif isinstance(raw, dict) or isinstance(raw, list): blob = raw else: blob = {} # extract sequence if isinstance(blob, dict): seq = blob.get("messages_sequence", []) elif isinstance(blob, list): seq = blob else: seq = [] # make sure it's a list if not isinstance(seq, list): seq = [seq] # render each message for j, msg in enumerate(seq, start=1): if not isinstance(msg, dict): # if it's just a string or number, render it plainly st.markdown(f"**{j}. (no header)**") st.markdown(str(msg)) st.markdown("---") continue header = msg.get("header", "(no header)") st.markdown(f"**{j}. {header}**") # optional title title = msg.get("title") if title: st.markdown(f"**Title:** {title}") # thumbnail (per-message or fallback per-user) thumb = msg.get("thumbnail_url") or row.get("thumbnail_url") if thumb: st.image(thumb, width=150) # the main message body body = msg.get("message", "") st.markdown(body) # optional "read more" link url = msg.get("web_url_path") if url: st.markdown(f"[Read more]({url})") st.markdown("---")