Danialebrat commited on
Commit
514a1ba
·
1 Parent(s): 29b98c7

- Adding For you section as default

Browse files
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
- continue
 
 
 
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.involve_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,11 +119,11 @@ class MessageGenerator:
119
  }
120
 
121
  else:
122
- # Only "message" is expected when involve_recsys_result is False 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"]}
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 and Segment info are mandatory 🚫")
193
  st.stop()
194
 
195
  # ─ build Snowflake session
@@ -261,40 +261,83 @@ with tab2:
261
  prog.empty(); status.empty()
262
  st.balloons()
263
 
264
- # -------- show results (if any)
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
- with st.expander(f"{i}. User ID: {row[id_col.lower()]}", expanded=(i == 1)):
 
 
273
  st.write("##### 👤 Features")
274
- feats = st.session_state.selected_source_features
275
  cols = st.columns(3)
276
- for idx, f in enumerate(feats):
277
- cols[idx % 3].markdown(f"**{f}**: {row.get(f, '')}")
 
278
 
279
  st.markdown("---")
 
 
280
  st.write("##### ✉️ Messages")
281
- try:
282
- blob = json.loads(row["message"])
283
- seq = (blob.get("messages_sequence", blob)
284
- if isinstance(blob, dict) else blob)
285
-
286
- for j, msg in enumerate(seq, 1):
287
- st.markdown(f"**{j}. {msg.get('header', '(no header)')}**")
288
- thumb = (msg.get("thumbnail_url") # per-message
289
- or row.get("thumbnail_url")) # per-user fallback
290
- if thumb:
291
- st.image(thumb, width=150)
292
- # ---------------------------------------------------------
293
-
294
- st.markdown(msg.get("message", ""))
295
- st.markdown(f"[Read more]({msg.get('web_url_path', '#')})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
- except Exception as e:
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
+