Danialebrat's picture
- Keeping memory of previous instructions
5700222
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(
"""
<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
)
# ──────────────────────────────────────────────────────────────────────────────
# 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("---")