| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| | @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 = { |
| | "danial@musora.com", |
| | "danial.ebrat@gmail.com", |
| | "simon@musora.com", |
| | "una@musora.com", |
| | "mark@musora.com", |
| | "gabriel@musora.com", |
| | "nikki@musora.com" |
| | } |
| |
|
| |
|
| | |
| | VALID_TOKEN = get_credential("APP_TOKEN") |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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() |
| | else: |
| | st.error("β Invalid e-mail or token") |
| |
|
| | |
| | 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) |
| |
|
| | |
| | |
| | |
| | st.set_page_config( |
| | page_title="Personalized Message Generator", |
| | page_icon="π¬", |
| | layout="wide", |
| | initial_sidebar_state="expanded" |
| | ) |
| |
|
| | st.markdown( |
| | """ |
| | <style> |
| | html, body, [class*="css"] { |
| | background-color:#0d0d0d; |
| | color:#ffd700; |
| | } |
| | .stButton>button, .stDownloadButton>button { |
| | border-radius:8px; |
| | background:#ffd700; |
| | color:#0d0d0d; |
| | font-weight:600; |
| | } |
| | .stTabs [data-baseweb="tab"] { |
| | font-weight:600; |
| | } |
| | .stTabs [aria-selected="true"] { |
| | color:#ffd700; |
| | } |
| | h1, h2, h3 {color:#ffd700;} |
| | .small {font-size:0.85rem; opacity:0.7;} |
| | </style> |
| | """, |
| | unsafe_allow_html=True |
| | ) |
| |
|
| | |
| | |
| | |
| | init_state() |
| | with st.sidebar: |
| | |
| | |
| | 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: |
| | |
| | |
| | |
| | |
| | |
| | |
| | id_col = "user_id" |
| | st.session_state.identifier_column = id_col |
| |
|
| |
|
| | |
| | st.selectbox( |
| | "Brand *", |
| | ["drumeo", "pianote", "guitareo", "singeo"], |
| | key="brand", |
| | ) |
| |
|
| | |
| | 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") |
| |
|
| | |
| | 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}") |
| |
|
| | |
| | st.multiselect( |
| | "Source features", |
| | ["instrument", "weeks_since_last_interaction", |
| | "birthday_reminder"], |
| | default=["instrument"], |
| | key="selected_source_features" |
| | ) |
| |
|
| | |
| | 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 |
| | st.session_state.generated = False |
| |
|
| | |
| | |
| |
|
| |
|
| | |
| | |
| | |
| | tab0, tab2 = st.tabs( |
| | ["π Data preview", "π¨ Results"]) |
| |
|
| | |
| | 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.") |
| |
|
| |
|
| | |
| | with tab2: |
| | st.header("π¨ Generated messages") |
| | |
| | if st.session_state.generate_clicked and not st.session_state.generated: |
| |
|
| | |
| | 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): |
| |
|
| | |
| | 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() |
| |
|
| | |
| | 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() |
| | } |
| |
|
| | |
| | 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 |
| | ) |
| |
|
| | |
| | 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() |
| |
|
| | |
| | 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)): |
| | |
| | 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("---") |
| |
|
| | |
| | st.write("##### βοΈ Messages") |
| | raw = row.get("message", "") |
| | |
| | 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 = {} |
| |
|
| | |
| | if isinstance(blob, dict): |
| | seq = blob.get("messages_sequence", []) |
| | elif isinstance(blob, list): |
| | seq = blob |
| | else: |
| | seq = [] |
| |
|
| | |
| | if not isinstance(seq, list): |
| | seq = [seq] |
| |
|
| | |
| | for j, msg in enumerate(seq, start=1): |
| | if not isinstance(msg, dict): |
| | |
| | st.markdown(f"**{j}. (no header)**") |
| | st.markdown(str(msg)) |
| | st.markdown("---") |
| | continue |
| |
|
| | header = msg.get("header", "(no header)") |
| | st.markdown(f"**{j}. {header}**") |
| |
|
| | |
| | title = msg.get("title") |
| | if title: |
| | st.markdown(f"**Title:** {title}") |
| |
|
| | |
| | thumb = msg.get("thumbnail_url") or row.get("thumbnail_url") |
| | if thumb: |
| | st.image(thumb, width=150) |
| |
|
| | |
| | body = msg.get("message", "") |
| | st.markdown(body) |
| |
|
| | |
| | url = msg.get("web_url_path") |
| | if url: |
| | st.markdown(f"[Read more]({url})") |
| |
|
| | st.markdown("---") |
| |
|
| |
|