Commit ·
514a1ba
1
Parent(s): 29b98c7
- Adding For you section as default
Browse files- Data/not_active_drumeo_camp.csv +0 -0
- Messaging_system/Homepage_Recommender.py +32 -0
- Messaging_system/LLMR.py +6 -2
- Messaging_system/Message_generator.py +3 -3
- Messaging_system/Permes.py +7 -1
- Messaging_system/PromptGenerator.py +19 -3
- app.py +73 -30
- app_V1.py +527 -0
- messaging_main_test.py +205 -0
Data/not_active_drumeo_camp.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Messaging_system/Homepage_Recommender.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This class is a Default recommender that redirect the user to For You Section.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# -----------------------------------------------------------------------
|
| 8 |
+
class DefaultRec:
|
| 9 |
+
|
| 10 |
+
def __init__(self, CoreConfig):
|
| 11 |
+
|
| 12 |
+
self.Core = CoreConfig
|
| 13 |
+
self.user = None
|
| 14 |
+
self.for_you_url = f"https://www.musora.com/{self.Core.brand.lower()}/lessons/recommended"
|
| 15 |
+
self.recommendation = "for_you"
|
| 16 |
+
self.recommendation_info = "Redirecting user to their personalized Recommendations"
|
| 17 |
+
|
| 18 |
+
def get_recommendations(self):
|
| 19 |
+
"""
|
| 20 |
+
selecting the recommended content for each user
|
| 21 |
+
:return:
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
self.Core.users_df["recommendation"] = self.recommendation
|
| 25 |
+
self.Core.users_df["recommendation_info"] = self.recommendation_info
|
| 26 |
+
self.Core.users_df["recsys_result"] = self.for_you_url # URL to for you section
|
| 27 |
+
|
| 28 |
+
return self.Core.users_df
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
|
Messaging_system/LLMR.py
CHANGED
|
@@ -11,7 +11,7 @@ from dotenv import load_dotenv
|
|
| 11 |
import time
|
| 12 |
import streamlit as st
|
| 13 |
from tqdm import tqdm
|
| 14 |
-
|
| 15 |
load_dotenv()
|
| 16 |
|
| 17 |
|
|
@@ -29,6 +29,7 @@ class LLMR:
|
|
| 29 |
selecting the recommended content for each user
|
| 30 |
:return:
|
| 31 |
"""
|
|
|
|
| 32 |
|
| 33 |
self.Core.users_df["recommendation"] = None
|
| 34 |
self.Core.users_df["recommendation_info"] = None
|
|
@@ -48,7 +49,10 @@ class LLMR:
|
|
| 48 |
content_id, content_info, recsys_json, token = self._get_recommendation()
|
| 49 |
|
| 50 |
if content_id is None: # error in selecting a content to recommend
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
else:
|
| 54 |
# updating tokens
|
|
|
|
| 11 |
import time
|
| 12 |
import streamlit as st
|
| 13 |
from tqdm import tqdm
|
| 14 |
+
from Messaging_system.Homepage_Recommender import DefaultRec
|
| 15 |
load_dotenv()
|
| 16 |
|
| 17 |
|
|
|
|
| 29 |
selecting the recommended content for each user
|
| 30 |
:return:
|
| 31 |
"""
|
| 32 |
+
default = DefaultRec(self.Core)
|
| 33 |
|
| 34 |
self.Core.users_df["recommendation"] = None
|
| 35 |
self.Core.users_df["recommendation_info"] = None
|
|
|
|
| 49 |
content_id, content_info, recsys_json, token = self._get_recommendation()
|
| 50 |
|
| 51 |
if content_id is None: # error in selecting a content to recommend
|
| 52 |
+
self.Core.users_df.at[idx, "recommendation"] = default.recommendation
|
| 53 |
+
self.Core.users_df.at[idx, "recommendation_info"] = default.recommendation_info
|
| 54 |
+
self.Core.users_df.at[idx, "recsys_result"] = default.for_you_url
|
| 55 |
+
|
| 56 |
|
| 57 |
else:
|
| 58 |
# updating tokens
|
Messaging_system/Message_generator.py
CHANGED
|
@@ -103,7 +103,7 @@ class MessageGenerator:
|
|
| 103 |
:param user: The user row
|
| 104 |
:return: Parsed and enriched output as a JSON object
|
| 105 |
"""
|
| 106 |
-
if self.Core.
|
| 107 |
output_message = self.fetch_recommendation_data(user, message)
|
| 108 |
elif self.Core.messaging_mode == "recommend_playlist":
|
| 109 |
# adding playlist url to the message
|
|
@@ -119,11 +119,11 @@ class MessageGenerator:
|
|
| 119 |
}
|
| 120 |
|
| 121 |
else:
|
| 122 |
-
# Only "message" is expected when
|
| 123 |
if "message" not in message or "header" not in message:
|
| 124 |
print("LLM output is missing 'message'.")
|
| 125 |
return None
|
| 126 |
-
output_message = {"header": message["header"], "message": message["message"]}
|
| 127 |
|
| 128 |
return json.dumps(output_message, ensure_ascii=False)
|
| 129 |
|
|
|
|
| 103 |
:param user: The user row
|
| 104 |
:return: Parsed and enriched output as a JSON object
|
| 105 |
"""
|
| 106 |
+
if self.Core.messaging_mode == "recsys_result":
|
| 107 |
output_message = self.fetch_recommendation_data(user, message)
|
| 108 |
elif self.Core.messaging_mode == "recommend_playlist":
|
| 109 |
# adding playlist url to the message
|
|
|
|
| 119 |
}
|
| 120 |
|
| 121 |
else:
|
| 122 |
+
# Only "message" is expected when messaging mode is message and we are not recommending any other content from input
|
| 123 |
if "message" not in message or "header" not in message:
|
| 124 |
print("LLM output is missing 'message'.")
|
| 125 |
return None
|
| 126 |
+
output_message = {"header": message["header"], "message": message["message"], "web_url_path": user["recsys_result"]}
|
| 127 |
|
| 128 |
return json.dumps(output_message, ensure_ascii=False)
|
| 129 |
|
Messaging_system/Permes.py
CHANGED
|
@@ -12,6 +12,7 @@ import streamlit as st
|
|
| 12 |
from Messaging_system.Message_generator import MessageGenerator
|
| 13 |
from Messaging_system.PromptGenerator import PromptGenerator
|
| 14 |
from Messaging_system.SnowFlakeConnection import SnowFlakeConn
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
|
|
@@ -135,10 +136,15 @@ class Permes:
|
|
| 135 |
CoreConfig = datacollect.gather_data()
|
| 136 |
|
| 137 |
# generating recommendations for users, if we want to include recommendations in the message
|
| 138 |
-
if CoreConfig.involve_recsys_result:
|
| 139 |
Recommender = LLMR(CoreConfig)
|
| 140 |
CoreConfig = Recommender.get_recommendations(progress_callback)
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
# generating proper prompt for each user
|
| 143 |
prompt = PromptGenerator(CoreConfig)
|
| 144 |
CoreConfig = prompt.generate_prompts()
|
|
|
|
| 12 |
from Messaging_system.Message_generator import MessageGenerator
|
| 13 |
from Messaging_system.PromptGenerator import PromptGenerator
|
| 14 |
from Messaging_system.SnowFlakeConnection import SnowFlakeConn
|
| 15 |
+
from Messaging_system.Homepage_Recommender import DefaultRec
|
| 16 |
|
| 17 |
|
| 18 |
|
|
|
|
| 136 |
CoreConfig = datacollect.gather_data()
|
| 137 |
|
| 138 |
# generating recommendations for users, if we want to include recommendations in the message
|
| 139 |
+
if CoreConfig.involve_recsys_result and CoreConfig.messaging_mode != "message":
|
| 140 |
Recommender = LLMR(CoreConfig)
|
| 141 |
CoreConfig = Recommender.get_recommendations(progress_callback)
|
| 142 |
|
| 143 |
+
else:
|
| 144 |
+
# We only want to generate the message and redirect them to For You section or Homepage
|
| 145 |
+
Recommender = DefaultRec(CoreConfig)
|
| 146 |
+
CoreConfig = Recommender.get_recommendations()
|
| 147 |
+
|
| 148 |
# generating proper prompt for each user
|
| 149 |
prompt = PromptGenerator(CoreConfig)
|
| 150 |
CoreConfig = prompt.generate_prompts()
|
Messaging_system/PromptGenerator.py
CHANGED
|
@@ -62,11 +62,13 @@ class PromptGenerator:
|
|
| 62 |
context = self.input_context()
|
| 63 |
cta = self.CTA_instructions()
|
| 64 |
|
| 65 |
-
if self.Core.involve_recsys_result or self.Core.target_content is not None:
|
| 66 |
if user["recommendation"] is not None or user["recommendation_info"] is not None:
|
| 67 |
recommendations_instructions = self.recommendations_instructions(user=user) + "\n"
|
|
|
|
|
|
|
| 68 |
else:
|
| 69 |
-
recommendations_instructions =
|
| 70 |
|
| 71 |
user_info = self.get_user_profile(user=user)
|
| 72 |
|
|
@@ -413,7 +415,7 @@ class PromptGenerator:
|
|
| 413 |
:return:
|
| 414 |
"""
|
| 415 |
|
| 416 |
-
if self.Core.involve_recsys_result:
|
| 417 |
recsys_task = """
|
| 418 |
- Create a perfect message and the header following the instructions, using the user's information and the content that we want to recommend.
|
| 419 |
- Use the instructions to include the recommended content in the message.
|
|
@@ -432,3 +434,17 @@ class PromptGenerator:
|
|
| 432 |
"""
|
| 433 |
|
| 434 |
return instructions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
context = self.input_context()
|
| 63 |
cta = self.CTA_instructions()
|
| 64 |
|
| 65 |
+
if (self.Core.involve_recsys_result and self.Core.messaging_mode !="message") or self.Core.target_content is not None:
|
| 66 |
if user["recommendation"] is not None or user["recommendation_info"] is not None:
|
| 67 |
recommendations_instructions = self.recommendations_instructions(user=user) + "\n"
|
| 68 |
+
else:
|
| 69 |
+
recommendations_instructions = self.redirect_to_for_you()
|
| 70 |
else:
|
| 71 |
+
recommendations_instructions = self.redirect_to_for_you()
|
| 72 |
|
| 73 |
user_info = self.get_user_profile(user=user)
|
| 74 |
|
|
|
|
| 415 |
:return:
|
| 416 |
"""
|
| 417 |
|
| 418 |
+
if self.Core.involve_recsys_result and self.Core.messaging_mode != "message":
|
| 419 |
recsys_task = """
|
| 420 |
- Create a perfect message and the header following the instructions, using the user's information and the content that we want to recommend.
|
| 421 |
- Use the instructions to include the recommended content in the message.
|
|
|
|
| 434 |
"""
|
| 435 |
|
| 436 |
return instructions
|
| 437 |
+
|
| 438 |
+
# =======================================================
|
| 439 |
+
def redirect_to_for_you(self):
|
| 440 |
+
"""
|
| 441 |
+
instructions to redirect the user to For you section
|
| 442 |
+
:return:
|
| 443 |
+
"""
|
| 444 |
+
|
| 445 |
+
instructions = f"""
|
| 446 |
+
** Note: **
|
| 447 |
+
We don't recommend a specific conten and by opening the message, the user will be redirected to a page where we have personalized content recommendations for them.
|
| 448 |
+
\n
|
| 449 |
+
"""
|
| 450 |
+
return instructions
|
app.py
CHANGED
|
@@ -105,14 +105,14 @@ with st.sidebar:
|
|
| 105 |
|
| 106 |
# ─ Brand
|
| 107 |
st.selectbox(
|
| 108 |
-
"Brand",
|
| 109 |
["drumeo", "pianote", "guitareo", "singeo"],
|
| 110 |
-
key="brand"
|
| 111 |
)
|
| 112 |
|
| 113 |
# ─ Personalisation
|
| 114 |
st.text_area("Segment info *", key="segment_info")
|
| 115 |
-
st.text_area("CTA *", key="CTA")
|
| 116 |
with st.expander("🔧 Optional tone & examples"):
|
| 117 |
st.text_area("Message style", key="message_style",
|
| 118 |
placeholder="Be kind and friendly…")
|
|
@@ -188,8 +188,8 @@ with tab2:
|
|
| 188 |
if st.session_state.generate_clicked and not st.session_state.generated:
|
| 189 |
|
| 190 |
# ─ simple validation
|
| 191 |
-
if not st.session_state.CTA.strip() or not st.session_state.segment_info.strip():
|
| 192 |
-
st.error("CTA
|
| 193 |
st.stop()
|
| 194 |
|
| 195 |
# ─ build Snowflake session
|
|
@@ -261,40 +261,83 @@ with tab2:
|
|
| 261 |
prog.empty(); status.empty()
|
| 262 |
st.balloons()
|
| 263 |
|
| 264 |
-
#
|
| 265 |
-
# -------- show results (if any)
|
| 266 |
if st.session_state.generated:
|
| 267 |
df = st.session_state.users_message
|
| 268 |
-
id_col = st.session_state.identifier_column
|
|
|
|
| 269 |
|
| 270 |
# expandable per-user cards
|
| 271 |
-
for i, (_, row) in enumerate(df.iterrows(), 1):
|
| 272 |
-
|
|
|
|
|
|
|
| 273 |
st.write("##### 👤 Features")
|
| 274 |
-
feats = st.session_state.selected_source_features
|
| 275 |
cols = st.columns(3)
|
| 276 |
-
for idx,
|
| 277 |
-
|
|
|
|
| 278 |
|
| 279 |
st.markdown("---")
|
|
|
|
|
|
|
| 280 |
st.write("##### ✉️ Messages")
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
st.
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
st.markdown("---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
-
|
| 299 |
-
st.error(f"Failed to parse JSON: {e}")
|
| 300 |
|
|
|
|
| 105 |
|
| 106 |
# ─ Brand
|
| 107 |
st.selectbox(
|
| 108 |
+
"Brand *",
|
| 109 |
["drumeo", "pianote", "guitareo", "singeo"],
|
| 110 |
+
key="brand",
|
| 111 |
)
|
| 112 |
|
| 113 |
# ─ Personalisation
|
| 114 |
st.text_area("Segment info *", key="segment_info")
|
| 115 |
+
st.text_area("CTA (Call to Action) *", key="CTA")
|
| 116 |
with st.expander("🔧 Optional tone & examples"):
|
| 117 |
st.text_area("Message style", key="message_style",
|
| 118 |
placeholder="Be kind and friendly…")
|
|
|
|
| 188 |
if st.session_state.generate_clicked and not st.session_state.generated:
|
| 189 |
|
| 190 |
# ─ simple validation
|
| 191 |
+
if not st.session_state.CTA.strip() or not st.session_state.segment_info.strip() or not st.session_state.brand.strip():
|
| 192 |
+
st.error("CTA, Segment info, and brand are mandatory 🚫")
|
| 193 |
st.stop()
|
| 194 |
|
| 195 |
# ─ build Snowflake session
|
|
|
|
| 261 |
prog.empty(); status.empty()
|
| 262 |
st.balloons()
|
| 263 |
|
| 264 |
+
# =============================================================
|
|
|
|
| 265 |
if st.session_state.generated:
|
| 266 |
df = st.session_state.users_message
|
| 267 |
+
id_col = st.session_state.identifier_column or ""
|
| 268 |
+
id_col_lower = id_col.lower()
|
| 269 |
|
| 270 |
# expandable per-user cards
|
| 271 |
+
for i, (_, row) in enumerate(df.iterrows(), start=1):
|
| 272 |
+
user_id = row.get(id_col_lower, "(no ID)")
|
| 273 |
+
with st.expander(f"{i}. User ID: {user_id}", expanded=(i == 1)):
|
| 274 |
+
# --- Features
|
| 275 |
st.write("##### 👤 Features")
|
| 276 |
+
feats = st.session_state.selected_source_features or []
|
| 277 |
cols = st.columns(3)
|
| 278 |
+
for idx, feature in enumerate(feats):
|
| 279 |
+
val = row.get(feature, "—")
|
| 280 |
+
cols[idx % 3].markdown(f"**{feature}**: {val}")
|
| 281 |
|
| 282 |
st.markdown("---")
|
| 283 |
+
|
| 284 |
+
# --- Messages
|
| 285 |
st.write("##### ✉️ Messages")
|
| 286 |
+
raw = row.get("message", "")
|
| 287 |
+
# try to parse JSON if it's a str
|
| 288 |
+
if isinstance(raw, str):
|
| 289 |
+
try:
|
| 290 |
+
blob = json.loads(raw)
|
| 291 |
+
except json.JSONDecodeError:
|
| 292 |
+
st.error(f"Could not parse JSON for user {user_id}")
|
| 293 |
+
continue
|
| 294 |
+
elif isinstance(raw, dict) or isinstance(raw, list):
|
| 295 |
+
blob = raw
|
| 296 |
+
else:
|
| 297 |
+
blob = {}
|
| 298 |
+
|
| 299 |
+
# extract sequence
|
| 300 |
+
if isinstance(blob, dict):
|
| 301 |
+
seq = blob.get("messages_sequence", [])
|
| 302 |
+
elif isinstance(blob, list):
|
| 303 |
+
seq = blob
|
| 304 |
+
else:
|
| 305 |
+
seq = []
|
| 306 |
+
|
| 307 |
+
# make sure it's a list
|
| 308 |
+
if not isinstance(seq, list):
|
| 309 |
+
seq = [seq]
|
| 310 |
+
|
| 311 |
+
# render each message
|
| 312 |
+
for j, msg in enumerate(seq, start=1):
|
| 313 |
+
if not isinstance(msg, dict):
|
| 314 |
+
# if it's just a string or number, render it plainly
|
| 315 |
+
st.markdown(f"**{j}. (no header)**")
|
| 316 |
+
st.markdown(str(msg))
|
| 317 |
st.markdown("---")
|
| 318 |
+
continue
|
| 319 |
+
|
| 320 |
+
header = msg.get("header", "(no header)")
|
| 321 |
+
st.markdown(f"**{j}. {header}**")
|
| 322 |
+
|
| 323 |
+
# optional title
|
| 324 |
+
title = msg.get("title")
|
| 325 |
+
if title:
|
| 326 |
+
st.markdown(f"**Title:** {title}")
|
| 327 |
+
|
| 328 |
+
# thumbnail (per-message or fallback per-user)
|
| 329 |
+
thumb = msg.get("thumbnail_url") or row.get("thumbnail_url")
|
| 330 |
+
if thumb:
|
| 331 |
+
st.image(thumb, width=150)
|
| 332 |
+
|
| 333 |
+
# the main message body
|
| 334 |
+
body = msg.get("message", "")
|
| 335 |
+
st.markdown(body)
|
| 336 |
+
|
| 337 |
+
# optional "read more" link
|
| 338 |
+
url = msg.get("web_url_path")
|
| 339 |
+
if url:
|
| 340 |
+
st.markdown(f"[Read more]({url})")
|
| 341 |
|
| 342 |
+
st.markdown("---")
|
|
|
|
| 343 |
|
app_V1.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import html
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from io import StringIO
|
| 5 |
+
import streamlit as st
|
| 6 |
+
import pandas as pd
|
| 7 |
+
from bs4 import BeautifulSoup
|
| 8 |
+
from snowflake.snowpark import Session
|
| 9 |
+
|
| 10 |
+
from Messaging_system.Permes import Permes
|
| 11 |
+
from Messaging_system.context_validator import Validator
|
| 12 |
+
from dotenv import load_dotenv
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
# -----------------------------------------------------------------------
|
| 16 |
+
# Load CSV file
|
| 17 |
+
@st.cache_data
|
| 18 |
+
def load_data(file_path):
|
| 19 |
+
return pd.read_csv(file_path)
|
| 20 |
+
|
| 21 |
+
# -----------------------------------------------------------------------
|
| 22 |
+
|
| 23 |
+
def load_config_(file_path):
|
| 24 |
+
"""
|
| 25 |
+
Loads configuration JSON files from the local space. (mostly for loading the Snowflake connection parameters)
|
| 26 |
+
:param file_path: local path to the JSON file
|
| 27 |
+
:return: JSON file
|
| 28 |
+
"""
|
| 29 |
+
with open(file_path, 'r') as file:
|
| 30 |
+
return json.load(file)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# -----------------------------------------------------------------------
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# Set page configuration and apply custom CSS for black and gold theme
|
| 37 |
+
st.set_page_config(page_title="Personalized Message Generator", page_icon=":mailbox_with_mail:", layout="wide")
|
| 38 |
+
|
| 39 |
+
st.markdown(
|
| 40 |
+
"""
|
| 41 |
+
<style>
|
| 42 |
+
body {
|
| 43 |
+
background-color: #000000;
|
| 44 |
+
color: #FFD700;
|
| 45 |
+
}
|
| 46 |
+
.stButton > button {
|
| 47 |
+
background-color: #FFD700;
|
| 48 |
+
color: #000000;
|
| 49 |
+
}
|
| 50 |
+
h1, h2, h3, h4, h5, h6 {
|
| 51 |
+
color: #FFD700;
|
| 52 |
+
}
|
| 53 |
+
.section {
|
| 54 |
+
margin-bottom: 30px;
|
| 55 |
+
}
|
| 56 |
+
.input-label {
|
| 57 |
+
font-size: 18px;
|
| 58 |
+
font-weight: bold;
|
| 59 |
+
margin-top: 10px;
|
| 60 |
+
}
|
| 61 |
+
</style>
|
| 62 |
+
""",
|
| 63 |
+
unsafe_allow_html=True
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# -----------------------------------------------------------------------
|
| 68 |
+
|
| 69 |
+
def filter_validated_users(users):
|
| 70 |
+
"""
|
| 71 |
+
Filters the input DataFrame by removing rows where the 'valid' column has the value 'False'.
|
| 72 |
+
|
| 73 |
+
Parameters:
|
| 74 |
+
users (DataFrame): A pandas DataFrame with a 'valid' column containing strings 'True' or 'False'.
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
DataFrame: A filtered DataFrame containing only rows where 'valid' is 'True'.
|
| 78 |
+
"""
|
| 79 |
+
# Convert the 'valid' column to boolean for easier filtering
|
| 80 |
+
users['valid'] = users['valid'].map({'True': True, 'False': False})
|
| 81 |
+
|
| 82 |
+
# Filter the DataFrame to include only rows where 'valid' is True
|
| 83 |
+
filtered_users = users[users['valid']]
|
| 84 |
+
|
| 85 |
+
# Optional: Reset the index of the filtered DataFrame
|
| 86 |
+
filtered_users = filtered_users.reset_index(drop=True)
|
| 87 |
+
|
| 88 |
+
return filtered_users
|
| 89 |
+
|
| 90 |
+
# -----------------------------------------------------------------------
|
| 91 |
+
|
| 92 |
+
# --------------------------------------------------------------
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# --------------------------------------------------------------
|
| 97 |
+
def clean_html_tags(users_df):
|
| 98 |
+
"""
|
| 99 |
+
accept the data as a Pandas Dataframe and return the preprocessed dataframe.
|
| 100 |
+
This function has access to the columns that contain HTML tags and codes, Therefore it will apply cleaning
|
| 101 |
+
procedures to those columns.
|
| 102 |
+
functions to preprocess the data
|
| 103 |
+
:return: updates users_df
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
for col in users_df.columns:
|
| 107 |
+
# Apply the cleaning function to each cell in the column
|
| 108 |
+
users_df[col] = users_df[col].apply(clean_text)
|
| 109 |
+
|
| 110 |
+
return users_df
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# --------------------------------------------------------------
|
| 114 |
+
|
| 115 |
+
def clean_text(text):
|
| 116 |
+
if isinstance(text, str):
|
| 117 |
+
# Unescape HTML entities
|
| 118 |
+
text = html.unescape(text)
|
| 119 |
+
# Parse HTML and get text
|
| 120 |
+
soup = BeautifulSoup(text, "html.parser")
|
| 121 |
+
return soup.get_text()
|
| 122 |
+
else:
|
| 123 |
+
return text
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# ----------------------------------------------------------------------------
|
| 127 |
+
# Load OpenAI API key from Streamlit secrets
|
| 128 |
+
openai_api_key = os.environ.get('OPENAI_API')
|
| 129 |
+
st.session_state["openai_api_key"] = openai_api_key
|
| 130 |
+
# ----------------------------------------------------------------------------
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# Main function
|
| 134 |
+
|
| 135 |
+
def initialize_session_state():
|
| 136 |
+
# Initialize session state variables if not already set
|
| 137 |
+
st.session_state["involve_recsys_result"] = False
|
| 138 |
+
st.session_state["involve_last_interaction"] = False
|
| 139 |
+
st.session_state.valid_instructions = ""
|
| 140 |
+
st.session_state.invalid_instructions = ""
|
| 141 |
+
|
| 142 |
+
# Initialize session state variables if not already set
|
| 143 |
+
for key in [
|
| 144 |
+
"data", "brand","recsys_contents", "generated", "csv_output", "users_message", "messaging_mode",
|
| 145 |
+
"messaging_type", "target_column", "ugc_column", "identifier_column", "input_validator", "selected_input_features"
|
| 146 |
+
"selected_features", "additional_instructions", "segment_info", "message_style", "sample_example",
|
| 147 |
+
"CTA", "all_features", "number_of_messages", "instructionset", "segment_name", "number_of_samples",
|
| 148 |
+
"selected_source_features", "platform"
|
| 149 |
+
]:
|
| 150 |
+
if key not in st.session_state:
|
| 151 |
+
st.session_state[key] = None
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def upload_csv_file():
|
| 155 |
+
st.header("Upload CSV File")
|
| 156 |
+
uploaded_file = st.file_uploader("Choose a CSV file", type="csv")
|
| 157 |
+
|
| 158 |
+
if uploaded_file is not None:
|
| 159 |
+
users = load_data(uploaded_file)
|
| 160 |
+
st.write(f"Data loaded from {uploaded_file.name}")
|
| 161 |
+
st.session_state.data = users
|
| 162 |
+
|
| 163 |
+
columns = users.columns.tolist()
|
| 164 |
+
st.subheader("Available Columns in Uploaded CSV")
|
| 165 |
+
st.write(columns)
|
| 166 |
+
return users
|
| 167 |
+
else:
|
| 168 |
+
return None
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def select_identifier_column(users):
|
| 172 |
+
st.header("Select Identifier Column")
|
| 173 |
+
columns = users.columns.tolist()
|
| 174 |
+
identifier_column = st.selectbox("Select the identifier column", columns)
|
| 175 |
+
st.session_state.identifier_column = identifier_column
|
| 176 |
+
st.markdown("---")
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def select_target_audience():
|
| 180 |
+
st.header("Select Target Audience")
|
| 181 |
+
options = ["drumeo", "pianote", "guitareo", "singeo"]
|
| 182 |
+
brand = st.selectbox("Choose the brand for the users", options)
|
| 183 |
+
st.session_state.brand = brand
|
| 184 |
+
st.markdown("---")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def select_target_messaging_type():
|
| 188 |
+
st.header("Select Target Messaging Type")
|
| 189 |
+
messaging_type = st.selectbox("Choose the target messaging type", ["Push Notification", "In-App Notification"])
|
| 190 |
+
|
| 191 |
+
st.session_state.messaging_type = "push" if messaging_type == "Push Notification" else "app"
|
| 192 |
+
st.markdown("---")
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def input_personalization_parameters():
|
| 196 |
+
st.header("Personalization Parameters")
|
| 197 |
+
st.session_state.segment_info = st.text_area("Segment Info", "", placeholder="Tell us more about the users...")
|
| 198 |
+
st.session_state.CTA = st.text_area("CTA", "", placeholder="e.g., check out 'Inspired by your activity' that we have crafted just for you!")
|
| 199 |
+
st.session_state.message_style = st.text_area("Message Style", "", placeholder="(optional) e.g., be kind and friendly (it's better to be as specific as possible)")
|
| 200 |
+
st.session_state.sample_example = st.text_area("Sample Example", "", placeholder="(optional) e.g., Hello! We have crafted a perfect set of courses just for you!")
|
| 201 |
+
number_of_samples = st.text_input("Number of samples to generate messages", "20", placeholder="(optional) default is 20")
|
| 202 |
+
st.session_state.number_of_samples = int(number_of_samples) if number_of_samples else 20
|
| 203 |
+
st.markdown("---")
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def input_message_sequence_parameters():
|
| 207 |
+
"""Collect settings for sequential message generation (new feature)."""
|
| 208 |
+
st.header("Sequential Messaging Parameters")
|
| 209 |
+
|
| 210 |
+
# Number of sequential messages
|
| 211 |
+
number_of_messages = st.number_input(
|
| 212 |
+
"Number of sequential messages to generate (per user)",
|
| 213 |
+
min_value=1, max_value=10, value=1, step=1, key="num_seq_msgs"
|
| 214 |
+
)
|
| 215 |
+
st.session_state.number_of_messages = number_of_messages
|
| 216 |
+
|
| 217 |
+
# Segment name for storage / tracking
|
| 218 |
+
segment_name = st.text_input(
|
| 219 |
+
"Segment Name", value="", placeholder="e.g., no_recent_activity", key="segment_name_input"
|
| 220 |
+
)
|
| 221 |
+
st.session_state.segment_name = segment_name
|
| 222 |
+
|
| 223 |
+
# Instruction set for each message
|
| 224 |
+
st.subheader("Instructions per Message")
|
| 225 |
+
st.caption("Provide additional tone or style instructions for each sequential message. Leave blank to inherit the main instructions.")
|
| 226 |
+
|
| 227 |
+
instructionset = {}
|
| 228 |
+
cols = st.columns(number_of_messages)
|
| 229 |
+
for i in range(1, number_of_messages + 1):
|
| 230 |
+
with cols[(i - 1) % number_of_messages]:
|
| 231 |
+
instr = st.text_input(
|
| 232 |
+
f"Message {i} instructions", value="", placeholder="e.g., Be Cheerful & Motivational", key=f"instr_{i}"
|
| 233 |
+
)
|
| 234 |
+
if instr.strip():
|
| 235 |
+
instructionset[i] = instr.strip()
|
| 236 |
+
|
| 237 |
+
# Save to session state
|
| 238 |
+
st.session_state.instructionset = instructionset
|
| 239 |
+
st.markdown("---")
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def select_features_from_source_info():
|
| 243 |
+
st.header("Select Features from Available Source Information")
|
| 244 |
+
available_features = ["first_name", "biography", "birthday_reminder", "goals", "Minutes_practiced", "Last_completed_content"]
|
| 245 |
+
selected_source_features = st.multiselect("Select features to use from available source information", available_features)
|
| 246 |
+
selected_source_features.append("instrument")
|
| 247 |
+
st.session_state.selected_source_features = selected_source_features
|
| 248 |
+
st.markdown("---")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def select_features_from_input_file(users):
|
| 252 |
+
st.header("Select Features from your Input file")
|
| 253 |
+
columns = users.columns.tolist()
|
| 254 |
+
selected_features = st.multiselect("Select features to use in generated messages from the input file", columns)
|
| 255 |
+
st.session_state.selected_features = selected_features
|
| 256 |
+
st.markdown("---")
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def provide_additional_instructions():
|
| 260 |
+
st.header("Additional Instructions")
|
| 261 |
+
additional_instructions = st.text_area("Provide additional instructions on how to use selected features in the generated message", "")
|
| 262 |
+
st.session_state.additional_instructions = additional_instructions
|
| 263 |
+
st.markdown("---")
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def parse_user_generated_context(users):
|
| 267 |
+
st.header("Parsing User-Generated Context")
|
| 268 |
+
user_generated_context = st.checkbox("Do we have a user-generated context provided in the input that you wish to filter?")
|
| 269 |
+
st.session_state.user_generated_context = user_generated_context
|
| 270 |
+
|
| 271 |
+
if user_generated_context:
|
| 272 |
+
columns = users.columns.tolist()
|
| 273 |
+
ugc_column = st.selectbox("Select the column that contains User-Generated Context", columns)
|
| 274 |
+
st.session_state.ugc_column = ugc_column
|
| 275 |
+
|
| 276 |
+
st.subheader("Provide Additional Instructions for Validation (Optional)")
|
| 277 |
+
valid_instructions = st.text_area("Instructions for valid context", placeholder="Provide instructions for what constitutes valid context...")
|
| 278 |
+
invalid_instructions = st.text_area("Instructions for invalid context", placeholder="Provide instructions for what constitutes invalid context...")
|
| 279 |
+
st.session_state.valid_instructions = valid_instructions
|
| 280 |
+
st.session_state.invalid_instructions = invalid_instructions
|
| 281 |
+
|
| 282 |
+
input_validator = Validator(api_key=st.session_state.openai_api_key)
|
| 283 |
+
st.session_state.input_validator = input_validator
|
| 284 |
+
st.markdown("---")
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def include_content_recommendations():
|
| 288 |
+
st.header("Include Content Recommendations")
|
| 289 |
+
include_recommendation = st.checkbox("Would you like to include content in the message to recommend to the students?")
|
| 290 |
+
st.session_state.include_recommendation = include_recommendation
|
| 291 |
+
|
| 292 |
+
if include_recommendation:
|
| 293 |
+
recommendation_source = st.radio("Select recommendation source", ["Input File", "Musora Recommender System"])
|
| 294 |
+
st.session_state.recommendation_source = recommendation_source
|
| 295 |
+
|
| 296 |
+
if recommendation_source == "Musora Recommender System":
|
| 297 |
+
st.session_state.involve_recsys_result = True
|
| 298 |
+
st.session_state.messaging_mode = "recsys_result"
|
| 299 |
+
|
| 300 |
+
list_of_content_types = ["song", "workout", "quick_tips", "course"]
|
| 301 |
+
selected_content_types = st.multiselect("Select content_types that you would like to recommend", list_of_content_types)
|
| 302 |
+
st.session_state.recsys_contents = selected_content_types
|
| 303 |
+
else:
|
| 304 |
+
st.session_state.involve_recsys_result = False
|
| 305 |
+
st.session_state.messaging_mode = "message"
|
| 306 |
+
columns = st.session_state.data.columns.tolist()
|
| 307 |
+
target_column = st.selectbox("Select the target column for recommendations", columns)
|
| 308 |
+
st.session_state.target_column = target_column
|
| 309 |
+
else:
|
| 310 |
+
st.session_state.messaging_mode = "message"
|
| 311 |
+
st.session_state.target_column = None
|
| 312 |
+
st.markdown("---")
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def generate_personalized_messages(users):
|
| 316 |
+
st.header("Generate Personalized Messages")
|
| 317 |
+
if st.button("Generate Personalized Messages"):
|
| 318 |
+
if st.session_state.CTA.strip() == "" or st.session_state.segment_info.strip() == "":
|
| 319 |
+
st.error("CTA and Segment Info are mandatory fields and cannot be left empty.")
|
| 320 |
+
else:
|
| 321 |
+
conn = {
|
| 322 |
+
"user": os.environ.get("snowflake_user"),
|
| 323 |
+
"password": os.environ.get("snowflake_password"),
|
| 324 |
+
"account": os.environ.get("snowflake_account"),
|
| 325 |
+
"role": os.environ.get("snowflake_role"),
|
| 326 |
+
"database": os.environ.get("snowflake_database"),
|
| 327 |
+
"warehouse": os.environ.get("snowflake_warehouse"),
|
| 328 |
+
"schema": os.environ.get("snowflake_schema")
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
config_file_path = 'Config_files/message_system_config.json'
|
| 332 |
+
config_file = load_config_(config_file_path)
|
| 333 |
+
session = Session.builder.configs(conn).create()
|
| 334 |
+
|
| 335 |
+
if st.session_state.user_generated_context:
|
| 336 |
+
if st.session_state.valid_instructions.strip() or st.session_state.invalid_instructions.strip():
|
| 337 |
+
st.session_state.input_validator.set_validator_instructions(
|
| 338 |
+
valid_instructions=st.session_state.valid_instructions,
|
| 339 |
+
invalid_instructions=st.session_state.invalid_instructions
|
| 340 |
+
)
|
| 341 |
+
else:
|
| 342 |
+
st.session_state.input_validator.set_validator_instructions()
|
| 343 |
+
|
| 344 |
+
# Create a progress bar
|
| 345 |
+
progress_bar = st.progress(0)
|
| 346 |
+
status_text = st.empty()
|
| 347 |
+
|
| 348 |
+
# Define a callback function to update the progress bar
|
| 349 |
+
def progress_callback(progress, total):
|
| 350 |
+
percent_complete = int(progress / total * 100)
|
| 351 |
+
progress_bar.progress(percent_complete)
|
| 352 |
+
status_text.text(f"Validating user_generated_context: {percent_complete}%")
|
| 353 |
+
|
| 354 |
+
st.info("Validating user-generated content. This may take a few moments...")
|
| 355 |
+
users = st.session_state.input_validator.validate_dataframe(
|
| 356 |
+
dataframe=users, target_column=st.session_state.ugc_column, progress_callback=progress_callback)
|
| 357 |
+
users = filter_validated_users(users)
|
| 358 |
+
st.success("User-generated content has been validated and filtered.")
|
| 359 |
+
|
| 360 |
+
st.session_state.all_features = st.session_state.selected_source_features + st.session_state.selected_features
|
| 361 |
+
|
| 362 |
+
if "Last_completed_content" in st.session_state.selected_source_features:
|
| 363 |
+
st.session_state.involve_last_interaction = True
|
| 364 |
+
else:
|
| 365 |
+
st.session_state.involve_last_interaction = False
|
| 366 |
+
|
| 367 |
+
# Create a progress bar
|
| 368 |
+
progress_bar = st.progress(0)
|
| 369 |
+
status_text = st.empty()
|
| 370 |
+
|
| 371 |
+
# Define a callback function to update the progress bar
|
| 372 |
+
def progress_callback(progress, total):
|
| 373 |
+
percent_complete = int(progress / total * 100)
|
| 374 |
+
progress_bar.progress(percent_complete)
|
| 375 |
+
status_text.text(f"Processing: {percent_complete}%")
|
| 376 |
+
|
| 377 |
+
permes = Permes()
|
| 378 |
+
users_message = permes.create_personalize_messages(
|
| 379 |
+
session=session,
|
| 380 |
+
users=users,
|
| 381 |
+
brand=st.session_state.brand,
|
| 382 |
+
config_file=config_file,
|
| 383 |
+
openai_api_key=os.environ.get('OPENAI_API'),
|
| 384 |
+
CTA=st.session_state.CTA,
|
| 385 |
+
segment_info=st.session_state.segment_info,
|
| 386 |
+
number_of_samples=st.session_state.number_of_samples,
|
| 387 |
+
message_style=st.session_state.message_style,
|
| 388 |
+
sample_example=st.session_state.sample_example,
|
| 389 |
+
selected_input_features=st.session_state.selected_features,
|
| 390 |
+
selected_source_features=st.session_state.selected_source_features,
|
| 391 |
+
additional_instructions=st.session_state.additional_instructions,
|
| 392 |
+
platform=st.session_state.messaging_type,
|
| 393 |
+
involve_last_interaction=st.session_state.involve_last_interaction,
|
| 394 |
+
involve_recsys_result=st.session_state.involve_recsys_result,
|
| 395 |
+
messaging_mode=st.session_state.messaging_mode,
|
| 396 |
+
identifier_column=st.session_state.identifier_column,
|
| 397 |
+
target_column=st.session_state.target_column,
|
| 398 |
+
recsys_contents=st.session_state.recsys_contents,
|
| 399 |
+
progress_callback=progress_callback,
|
| 400 |
+
# NEW PARAMETERS
|
| 401 |
+
number_of_messages=st.session_state.number_of_messages,
|
| 402 |
+
instructionset=st.session_state.instructionset,
|
| 403 |
+
segment_name=st.session_state.segment_name
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
# Clear the progress bar and status text after completion
|
| 407 |
+
progress_bar.empty()
|
| 408 |
+
status_text.empty()
|
| 409 |
+
|
| 410 |
+
csv_output = users_message.to_csv(encoding='utf-8-sig', index=False)
|
| 411 |
+
st.session_state.csv_output = csv_output
|
| 412 |
+
st.session_state.users_message = users_message
|
| 413 |
+
st.session_state.generated = True
|
| 414 |
+
st.success("Personalized messages have been generated.")
|
| 415 |
+
st.markdown("---")
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def download_generated_messages():
|
| 419 |
+
if st.session_state.get('generated', False):
|
| 420 |
+
st.header("Download Generated Messages")
|
| 421 |
+
|
| 422 |
+
# Suppose `df` is your final DataFrame
|
| 423 |
+
df = st.session_state.users_message # or wherever your DataFrame is
|
| 424 |
+
|
| 425 |
+
# Write CSV to an in-memory buffer, with utf-8-sig encoding
|
| 426 |
+
csv_buffer = StringIO()
|
| 427 |
+
df.to_csv(csv_buffer, index=False, encoding='utf-8-sig')
|
| 428 |
+
csv_buffer.seek(0)
|
| 429 |
+
|
| 430 |
+
# Convert to bytes (this will include the UTF-8 BOM)
|
| 431 |
+
csv_bytes = csv_buffer.getvalue().encode('utf-8-sig')
|
| 432 |
+
|
| 433 |
+
# Provide the bytes to download_button
|
| 434 |
+
st.download_button(
|
| 435 |
+
label="Download output messages as a CSV file",
|
| 436 |
+
data=csv_bytes,
|
| 437 |
+
file_name='personalized_messages.csv',
|
| 438 |
+
mime='text/csv'
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
def view_generated_messages():
|
| 443 |
+
# Only run if messages have been generated
|
| 444 |
+
if not st.session_state.get('generated', False):
|
| 445 |
+
return
|
| 446 |
+
|
| 447 |
+
st.title("🔔 Generated Push Notifications Review")
|
| 448 |
+
df = st.session_state.users_message
|
| 449 |
+
identifier = st.session_state.identifier_column.lower()
|
| 450 |
+
features = st.session_state.all_features
|
| 451 |
+
|
| 452 |
+
for idx, (_, user_row) in enumerate(df.iterrows(), start=1):
|
| 453 |
+
user_id = user_row.get(identifier, "N/A")
|
| 454 |
+
# Collapsible container per user
|
| 455 |
+
with st.expander(f"{idx}. User ID: {user_id}", expanded=(idx == 1)):
|
| 456 |
+
st.markdown("##### 👤 User Features")
|
| 457 |
+
# 3-column layout for user metadata
|
| 458 |
+
feature_cols = st.columns(3)
|
| 459 |
+
for i, feat in enumerate(features):
|
| 460 |
+
val = user_row.get(feat, "N/A")
|
| 461 |
+
feature_cols[i % 3].write(f"**{feat}**: {val}")
|
| 462 |
+
|
| 463 |
+
st.markdown("---")
|
| 464 |
+
st.markdown("##### 📝 Generated Messages")
|
| 465 |
+
raw = user_row.get('message', '[]')
|
| 466 |
+
|
| 467 |
+
try:
|
| 468 |
+
parsed = json.loads(raw)
|
| 469 |
+
# If it's the nested form {"messages_sequence": [ … ]}, grab the list inside.
|
| 470 |
+
if isinstance(parsed, dict) and 'messages_sequence' in parsed:
|
| 471 |
+
messages = parsed['messages_sequence']
|
| 472 |
+
# If somehow it's already a list, leave it alone.
|
| 473 |
+
elif isinstance(parsed, list):
|
| 474 |
+
messages = parsed
|
| 475 |
+
else:
|
| 476 |
+
st.warning(
|
| 477 |
+
"Unexpected JSON structure for messages; expected a list or {'messages_sequence': [...]}")
|
| 478 |
+
messages = []
|
| 479 |
+
except json.JSONDecodeError:
|
| 480 |
+
st.error("Could not parse message JSON")
|
| 481 |
+
messages = []
|
| 482 |
+
|
| 483 |
+
# Display each push notification
|
| 484 |
+
for m_idx, msg in enumerate(messages, start=1):
|
| 485 |
+
c_img, c_text = st.columns([1, 3])
|
| 486 |
+
with c_img:
|
| 487 |
+
thumb = msg.get('thumbnail_url')
|
| 488 |
+
if thumb:
|
| 489 |
+
st.image(thumb, width=80)
|
| 490 |
+
else:
|
| 491 |
+
st.write("No image")
|
| 492 |
+
|
| 493 |
+
with c_text:
|
| 494 |
+
header = msg.get('header', '')
|
| 495 |
+
body = msg.get('message', '')
|
| 496 |
+
link = msg.get('web_url_path', '#')
|
| 497 |
+
st.markdown(f"**{m_idx}. {header}**")
|
| 498 |
+
st.markdown(body)
|
| 499 |
+
st.markdown(f"[Read more →]({link})")
|
| 500 |
+
|
| 501 |
+
st.markdown("---")
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
if __name__ == "__main__":
|
| 505 |
+
st.title("Personalized Message Generator")
|
| 506 |
+
|
| 507 |
+
# Initialize session state variables
|
| 508 |
+
initialize_session_state()
|
| 509 |
+
|
| 510 |
+
# Upload CSV File
|
| 511 |
+
users = upload_csv_file()
|
| 512 |
+
|
| 513 |
+
if users is not None:
|
| 514 |
+
# Proceed with the rest of the application
|
| 515 |
+
select_identifier_column(users)
|
| 516 |
+
select_target_audience()
|
| 517 |
+
select_target_messaging_type()
|
| 518 |
+
input_personalization_parameters()
|
| 519 |
+
input_message_sequence_parameters()
|
| 520 |
+
select_features_from_source_info()
|
| 521 |
+
select_features_from_input_file(users)
|
| 522 |
+
provide_additional_instructions()
|
| 523 |
+
parse_user_generated_context(users)
|
| 524 |
+
include_content_recommendations()
|
| 525 |
+
generate_personalized_messages(users)
|
| 526 |
+
download_generated_messages()
|
| 527 |
+
view_generated_messages()
|
messaging_main_test.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import html
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from snowflake.snowpark import Session
|
| 7 |
+
from bs4 import BeautifulSoup
|
| 8 |
+
from Messaging_system.Permes import Permes
|
| 9 |
+
import streamlit as st
|
| 10 |
+
from Messaging_system.context_validator import Validator
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# --------------------------------------------------------------
|
| 14 |
+
def load_config_(file_path):
|
| 15 |
+
"""
|
| 16 |
+
Loads configuration JSON files from the local space. (mostly for loading the Snowflake connection parameters)
|
| 17 |
+
:param file_path: local path to the JSON file
|
| 18 |
+
:return: JSON file
|
| 19 |
+
"""
|
| 20 |
+
with open(file_path, 'r') as file:
|
| 21 |
+
return json.load(file)
|
| 22 |
+
|
| 23 |
+
# --------------------------------------------------------------
|
| 24 |
+
def clean_html_tags(users_df):
|
| 25 |
+
"""
|
| 26 |
+
accept the data as a Pandas Dataframe and return the preprocessed dataframe.
|
| 27 |
+
This function has access to the columns that contain HTML tags and codes, Therefore it will apply cleaning
|
| 28 |
+
procedures to those columns.
|
| 29 |
+
functions to preprocess the data
|
| 30 |
+
:return: updates users_df
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
for col in users_df.columns:
|
| 34 |
+
# Apply the cleaning function to each cell in the column
|
| 35 |
+
users_df[col] = users_df[col].apply(clean_text)
|
| 36 |
+
|
| 37 |
+
return users_df
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# --------------------------------------------------------------
|
| 41 |
+
def clean_text(text):
|
| 42 |
+
if isinstance(text, str):
|
| 43 |
+
# Unescape HTML entities
|
| 44 |
+
text = html.unescape(text)
|
| 45 |
+
# Parse HTML and get text
|
| 46 |
+
soup = BeautifulSoup(text, "html.parser")
|
| 47 |
+
return soup.get_text()
|
| 48 |
+
else:
|
| 49 |
+
return text
|
| 50 |
+
|
| 51 |
+
# =============================================================
|
| 52 |
+
def get_credential(key):
|
| 53 |
+
return st.secrets.get(key) or os.getenv(key)
|
| 54 |
+
|
| 55 |
+
# --------------------------------------------------------------
|
| 56 |
+
def filter_validated_users(users):
|
| 57 |
+
"""
|
| 58 |
+
Filters the input DataFrame by removing rows where the 'valid' column has the value 'False'.
|
| 59 |
+
|
| 60 |
+
Parameters:
|
| 61 |
+
users (DataFrame): A pandas DataFrame with a 'valid' column containing strings 'True' or 'False'.
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
DataFrame: A filtered DataFrame containing only rows where 'valid' is 'True'.
|
| 65 |
+
"""
|
| 66 |
+
# Convert the 'valid' column to boolean for easier filtering
|
| 67 |
+
users['valid'] = users['valid'].map({'True': True, 'False': False})
|
| 68 |
+
|
| 69 |
+
# Filter the DataFrame to include only rows where 'valid' is True
|
| 70 |
+
filtered_users = users[users['valid']]
|
| 71 |
+
|
| 72 |
+
# Optional: Reset the index of the filtered DataFrame
|
| 73 |
+
filtered_users = filtered_users.reset_index(drop=True)
|
| 74 |
+
|
| 75 |
+
return filtered_users
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# --------------------------------------------------------------
|
| 79 |
+
if __name__ == "__main__":
|
| 80 |
+
# path to sample data
|
| 81 |
+
path = "Data/not_active_drumeo_camp.csv"
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# loading sample data
|
| 85 |
+
users = pd.read_csv(path)
|
| 86 |
+
users = clean_html_tags(users)
|
| 87 |
+
|
| 88 |
+
config_file_path = 'Config_files/message_system_config.json'
|
| 89 |
+
config_file = load_config_(config_file_path)
|
| 90 |
+
|
| 91 |
+
openai_api_key = get_credential("OPENAI_API")
|
| 92 |
+
|
| 93 |
+
conn = dict(
|
| 94 |
+
user=get_credential("snowflake_user"),
|
| 95 |
+
password=get_credential("snowflake_password"),
|
| 96 |
+
account=get_credential("snowflake_account"),
|
| 97 |
+
role=get_credential("snowflake_role"),
|
| 98 |
+
database=get_credential("snowflake_database"),
|
| 99 |
+
warehouse=get_credential("snowflake_warehouse"),
|
| 100 |
+
schema=get_credential("snowflake_schema")
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
# --------------------
|
| 104 |
+
# #Do we need to validate user-generated context?
|
| 105 |
+
# user_generated_context = True
|
| 106 |
+
# input_validator = Validator(api_key=openai_api_key)
|
| 107 |
+
# input_validator.set_validator_instructions()
|
| 108 |
+
# users = input_validator.validate_dataframe(dataframe=users, target_column="forum_content")
|
| 109 |
+
# users = filter_validated_users(users)
|
| 110 |
+
# --------------------
|
| 111 |
+
|
| 112 |
+
session = Session.builder.configs(conn).create()
|
| 113 |
+
|
| 114 |
+
brand = "drumeo"
|
| 115 |
+
identifier_column = "user_id"
|
| 116 |
+
|
| 117 |
+
segment_info = """This is a Drumeo user who have been inactive for 30 days or more. This means
|
| 118 |
+
they have not taken any actions on the platform (e.g. lesson, songs, etc.) that would cause them to advance their
|
| 119 |
+
musical skills. We want to re-motivate the user so they can get back on track to reach their drumming goals"""
|
| 120 |
+
|
| 121 |
+
# sample inputs
|
| 122 |
+
|
| 123 |
+
CTA = """Re-engage the user by reminding them of their goals, and by recommending content that will get them back on track. """
|
| 124 |
+
|
| 125 |
+
# sample_example = """we have crafted a perfect set of courses just for you! come and check it out!"""
|
| 126 |
+
|
| 127 |
+
additional_instructions = """Include weeks_since _last_interaction in the message if you can create a better message to re-engage the user."""
|
| 128 |
+
|
| 129 |
+
recsys_contents = ["workout", "course", "quick_tips"]
|
| 130 |
+
|
| 131 |
+
# number_of_samples = users.shape[0]
|
| 132 |
+
number_of_samples = 5
|
| 133 |
+
|
| 134 |
+
# number of messages to generate
|
| 135 |
+
number_of_messages = 3
|
| 136 |
+
instructionset = {
|
| 137 |
+
1: "Be Cheerful & Motivational",
|
| 138 |
+
2: "Be Passive aggressive, sarcastic and mean",
|
| 139 |
+
3: "Be sad and disappointed"
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
involve_recsys_result = True
|
| 143 |
+
involve_last_interaction = False
|
| 144 |
+
|
| 145 |
+
# messaging_mode = "recommend_playlist"
|
| 146 |
+
|
| 147 |
+
sample_example = """
|
| 148 |
+
Below are sample messages from us. make the generated message close to our sound in terms of style, tune, and the way we write messages.
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
Example 1
|
| 152 |
+
header: The Drums Miss You, [first_name]
|
| 153 |
+
message: 🥁 It’s been a while. Jump back in with this quick lesson!
|
| 154 |
+
|
| 155 |
+
Example 2
|
| 156 |
+
Header: Time To Practice 🥁
|
| 157 |
+
message: A quick workout will help you reach your musical goals.
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
Example 3
|
| 161 |
+
header: 🥁 Don’t Stop The Beat
|
| 162 |
+
message: A few minutes of practice makes a huge difference. You’ll love this lesson!
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
Example 4
|
| 166 |
+
header: It’s Been A While
|
| 167 |
+
message: 🥁We haven’t seen you in 7 weeks. Get back on track with a quick lesson!
|
| 168 |
+
|
| 169 |
+
Example 5:
|
| 170 |
+
header: Miss Playing, Michael? 🥁
|
| 171 |
+
message: It's been 5 weeks! Jump back in with a cool workout to learn new beats!
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
sample_example = None
|
| 175 |
+
|
| 176 |
+
platform = "push"
|
| 177 |
+
|
| 178 |
+
selected_source_features = ["first_name", "weeks_since_last_interaction"]
|
| 179 |
+
selected_input_features = None
|
| 180 |
+
|
| 181 |
+
segment_name = "no_recent_activity"
|
| 182 |
+
permes = Permes()
|
| 183 |
+
|
| 184 |
+
users_message = permes.create_personalize_messages(session=session,
|
| 185 |
+
users=users,
|
| 186 |
+
brand=brand,
|
| 187 |
+
config_file=config_file,
|
| 188 |
+
openai_api_key=openai_api_key,
|
| 189 |
+
CTA=CTA,
|
| 190 |
+
segment_info=segment_info,
|
| 191 |
+
number_of_samples=number_of_samples,
|
| 192 |
+
number_of_messages=number_of_messages,
|
| 193 |
+
instructionset=instructionset,
|
| 194 |
+
selected_source_features=selected_source_features,
|
| 195 |
+
selected_input_features=selected_input_features,
|
| 196 |
+
additional_instructions=additional_instructions,
|
| 197 |
+
platform=platform,
|
| 198 |
+
involve_recsys_result=involve_recsys_result,
|
| 199 |
+
identifier_column=identifier_column,
|
| 200 |
+
recsys_contents=recsys_contents,
|
| 201 |
+
sample_example=sample_example,
|
| 202 |
+
segment_name=segment_name)
|
| 203 |
+
|
| 204 |
+
users_message.to_csv(f"drumeo_not_active_complete.csv", encoding='utf-8-sig', index=False)
|
| 205 |
+
|