Danialebrat commited on
Commit
093632f
·
1 Parent(s): 690a763

- adding LLM class which contains multiple connectors including ollama models

Browse files

- modifying system prompts
- modifying all class to rely on the new LLM class
- Modifying UI to get LLM model from user

Config_files/message_system_config.json CHANGED
@@ -20,7 +20,8 @@
20
  "AI_Jargon": ["elevate", "enhance", "ignite", "reignite", "rekindle", "rediscover","passion", "boost", "fuel", "thrill", "revive", "spark", "performing", "fresh", "tone", "enthusiasm", "illuminate"],
21
  "AI_phrases_singeo": ["your voice deserves more"],
22
  "header_limit": 30,
23
- "message_limit": 110
 
24
  }
25
 
26
 
 
20
  "AI_Jargon": ["elevate", "enhance", "ignite", "reignite", "rekindle", "rediscover","passion", "boost", "fuel", "thrill", "revive", "spark", "performing", "fresh", "tone", "enthusiasm", "illuminate"],
21
  "AI_phrases_singeo": ["your voice deserves more"],
22
  "header_limit": 30,
23
+ "message_limit": 110,
24
+ "LLM_models": ["4o-mini", "gpt-4o", "deepseek-r1:1.5b", "gemma3:4b"]
25
  }
26
 
27
 
Messaging_system/CoreConfig.py CHANGED
@@ -21,9 +21,8 @@ class CoreConfig:
21
 
22
  # LLM configs
23
  self.api_key = None # will be set by user
24
- self.model = "gpt-4o" # will be set by user
25
  self.temperature = 0.7
26
- # self.model = "gpt-4.1-nano" # will be set by user
27
 
28
  # will be set by user
29
  self.CTA = None
@@ -80,6 +79,12 @@ class CoreConfig:
80
 
81
  self.message_style = message_style
82
 
 
 
 
 
 
 
83
  # --------------------------------------------------------------
84
  # --------------------------------------------------------------
85
  def set_involve_recsys_result(self, involve_recsys_result):
 
21
 
22
  # LLM configs
23
  self.api_key = None # will be set by user
24
+ self.model = "gpt-4o" # default -> will be set by user
25
  self.temperature = 0.7
 
26
 
27
  # will be set by user
28
  self.CTA = None
 
79
 
80
  self.message_style = message_style
81
 
82
+ # --------------------------------------------------------------
83
+ # --------------------------------------------------------------
84
+
85
+ def set_llm_model(self, model):
86
+ self.model = model
87
+
88
  # --------------------------------------------------------------
89
  # --------------------------------------------------------------
90
  def set_involve_recsys_result(self, involve_recsys_result):
Messaging_system/LLM.py CHANGED
@@ -6,31 +6,58 @@ import json
6
  import time
7
  from openai import OpenAI
8
  import openai
9
- from tqdm import tqdm
 
 
10
 
11
 
12
 
13
  class LLM:
14
- def __init__(self, Core, llm):
15
  self.Core = Core
16
- self.llm = llm
17
 
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  def connect_to_llm(self):
21
  """
22
- connect to selected llm
23
  :return:
24
  """
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
 
27
- def get_message(self, prompt, max_retries=4):
28
  """
29
- sending the prompt to the LLM and get back the response
30
  """
31
 
32
  openai.api_key = self.Core.api_key
33
- instructions = self.llm_instructions()
34
  client = OpenAI(api_key=self.Core.api_key)
35
 
36
  for attempt in range(max_retries):
@@ -59,7 +86,6 @@ class LLM:
59
  # Extract JSON code block
60
 
61
  output = json.loads(content)
62
- # output = json.loads(response.choices[0].message.content)
63
 
64
  if 'message' not in output or 'header' not in output:
65
  print(f"'message' or 'header' is missing in response on attempt {attempt + 1}. Retrying...")
@@ -92,4 +118,111 @@ class LLM:
92
  print(e.response)
93
 
94
  print("Max retries exceeded. Returning empty response.")
95
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import time
7
  from openai import OpenAI
8
  import openai
9
+ import ollama
10
+ import torch
11
+ import re
12
 
13
 
14
 
15
  class LLM:
16
+ def __init__(self, Core):
17
  self.Core = Core
 
18
 
19
 
20
+ self.model = None
21
+ self.model_type = "openai" # valid values -> ["openai", "ollama"]
22
+ self.client = None
23
+ self.connect_to_llm()
24
+
25
+
26
+ def get_response(self, prompt, instructions):
27
+ if self.model_type == "openai":
28
+ response = self.get_message_openai(prompt, instructions)
29
+ elif self.model_type == "ollama":
30
+ response = self.get_message_ollama(prompt, instructions)
31
+ else:
32
+ raise f"Invalid model type : {self.model_type}"
33
+
34
+ return response
35
 
36
  def connect_to_llm(self):
37
  """
38
+ connect to selected llm -> ollama or openai connection
39
  :return:
40
  """
41
+ openai_models = ["4o-mini", "gpt-4o", "gpt-4.1-nano"]
42
+ ollama_models = ["deepseek-r1:1.5b", "gemma3:4b", "deepseek-r1:7b"]
43
+
44
+ if self.Core.model in openai_models:
45
+ self.model_type = "openai"
46
+
47
+ if self.Core.model in ollama_models:
48
+ self.model_type = "ollama"
49
+ self.client = ollama.Client()
50
+
51
+ self.model = self.Core.model
52
+
53
 
54
 
55
+ def get_message_openai(self, prompt, instructions, max_retries=4):
56
  """
57
+ sending the prompt to openai LLM and get back the response
58
  """
59
 
60
  openai.api_key = self.Core.api_key
 
61
  client = OpenAI(api_key=self.Core.api_key)
62
 
63
  for attempt in range(max_retries):
 
86
  # Extract JSON code block
87
 
88
  output = json.loads(content)
 
89
 
90
  if 'message' not in output or 'header' not in output:
91
  print(f"'message' or 'header' is missing in response on attempt {attempt + 1}. Retrying...")
 
118
  print(e.response)
119
 
120
  print("Max retries exceeded. Returning empty response.")
121
+ return None
122
+
123
+ # ======================================================================
124
+
125
+ def get_message_ollama(self, prompt, instructions, max_retries=10):
126
+ """
127
+ Send the prompt to the LLM and get back the response.
128
+ Includes handling for GPU memory issues by clearing cache and waiting before retry.
129
+ """
130
+ prompt = instructions + "\n \n" + prompt
131
+ for attempt in range(max_retries):
132
+ try:
133
+ # Try generating the response
134
+ response = self.client.generate(model=self.model, prompt=prompt, format='json')
135
+ except Exception as e:
136
+ # This catches errors like the connection being forcibly closed
137
+ print(f"Error on attempt {attempt + 1}: {e}.")
138
+ try:
139
+ # Clear GPU cache if you're using PyTorch; this may help free up memory
140
+ torch.cuda.empty_cache()
141
+ print("Cleared GPU cache.")
142
+ except Exception as cache_err:
143
+ print("Failed to clear GPU cache:", cache_err)
144
+ # Wait a bit before retrying to allow memory to recover
145
+ time.sleep(2)
146
+ continue
147
+
148
+ try:
149
+ tokens = {
150
+ 'prompt_tokens': 0,
151
+ 'completion_tokens': 0,
152
+ 'total_tokens': 0
153
+ }
154
+
155
+ try:
156
+ output = self.preprocess_and_parse_json(response.response)
157
+ if output is None:
158
+ continue
159
+
160
+ if 'message' not in output or 'header' not in output:
161
+ print(f"'message' or 'header' is missing in response on attempt {attempt + 1}. Retrying...")
162
+ continue # Continue to next attempt
163
+
164
+ else:
165
+ if len(output["header"].strip()) > self.Core.config_file["header_limit"] or len(
166
+ output["message"].strip()) > self.Core.config_file["message_limit"]:
167
+ print(
168
+ f"'header' or 'message' is more than specified characters in response on attempt {attempt + 1}. Retrying...")
169
+ continue
170
+
171
+ except json.JSONDecodeError:
172
+ print(f"Invalid JSON from LLM on attempt {attempt + 1}. Retrying...")
173
+ except Exception as parse_error:
174
+ print("Error processing output:", parse_error)
175
+
176
+ print("Max retries exceeded. Returning empty response.")
177
+ return [], {}
178
+
179
+ # ======================================================================
180
+
181
+ # def preprocess_and_parse_json(self, response):
182
+ # # Remove any leading/trailing whitespace and newlines
183
+ # if response.startswith('```json') and response.endswith('```'):
184
+ # response = response[len('```json'):-len('```')].strip()
185
+ #
186
+ # # Parse the cleaned response into a JSON object
187
+ # try:
188
+ # json_object = json.loads(response)
189
+ # return json_object
190
+ # except json.JSONDecodeError as e:
191
+ # print(f"Failed to parse JSON: {e}")
192
+ # return None
193
+
194
+ # =====================================================================
195
+
196
+ import re
197
+ import json
198
+
199
+ def preprocess_and_parse_json(self, response: str):
200
+ """
201
+ Cleans an LLM response by removing <think> tags and extracting JSON
202
+ from ```json ... ``` fences (or bare text if no fence is found),
203
+ then returns the parsed object or None on failure.
204
+ """
205
+ # 1) Remove all <think>...</think> blocks
206
+ cleaned = re.sub(r'<think>.*?</think>', '', response, flags=re.DOTALL)
207
+
208
+ # 2) Look for a ```json ... ``` fenced block
209
+ fence_pattern = re.compile(r'```json(.*?)```', flags=re.DOTALL)
210
+ fence_match = fence_pattern.search(cleaned)
211
+ if fence_match:
212
+ json_text = fence_match.group(1).strip()
213
+ else:
214
+ # No fence; assume whole cleaned text is JSON
215
+ json_text = cleaned.strip()
216
+
217
+ # 3) Attempt to parse
218
+ try:
219
+ return json.loads(json_text)
220
+ except json.JSONDecodeError as e:
221
+ print(f"Failed to parse JSON: {e}")
222
+ # Optionally, log the offending text for debugging:
223
+ # print("Offending text:", json_text)
224
+ return None
225
+
226
+
227
+
228
+
Messaging_system/Message_generator.py CHANGED
@@ -10,12 +10,14 @@ import streamlit as st
10
  from Messaging_system.MultiMessage import MultiMessage
11
  from Messaging_system.protection_layer import ProtectionLayer
12
  import openai
 
13
 
14
 
15
  class MessageGenerator:
16
 
17
  def __init__(self, CoreConfig):
18
  self.Core = CoreConfig
 
19
 
20
  # --------------------------------------------------------------
21
  # --------------------------------------------------------------
@@ -36,12 +38,11 @@ class MessageGenerator:
36
  progress_callback(progress, total_users)
37
 
38
  if row["prompt"] is not None:
39
- first_message = self.get_llm_response(row["prompt"])
40
 
41
  if first_message is not None:
42
  # adding protection layer
43
- protect = ProtectionLayer(config_file=self.Core.config_file,
44
- messaging_mode=self.Core.messaging_mode)
45
  message, total_tokens = protect.criticize(message=first_message, user=row)
46
 
47
  # updating tokens
@@ -171,80 +172,6 @@ class MessageGenerator:
171
  }
172
  return output_message
173
 
174
- # --------------------------------------------------------------
175
- # --------------------------------------------------------------
176
-
177
- def get_llm_response(self, prompt, max_retries=4):
178
- """
179
- sending the prompt to the LLM and get back the response
180
- """
181
-
182
- openai.api_key = self.Core.api_key
183
- instructions = self.llm_instructions()
184
- client = OpenAI(api_key=self.Core.api_key)
185
-
186
- for attempt in range(max_retries):
187
- try:
188
- response = client.chat.completions.create(
189
- model=self.Core.model,
190
- response_format={"type": "json_object"},
191
- messages=[
192
- {"role": "system", "content": instructions},
193
- {"role": "user", "content": prompt}
194
- ],
195
- max_tokens=500,
196
- n=1,
197
- # temperature=self.Core.temperature,
198
- temperature=0.7,
199
- )
200
-
201
- tokens = {
202
- 'prompt_tokens': response.usage.prompt_tokens,
203
- 'completion_tokens': response.usage.completion_tokens,
204
- 'total_tokens': response.usage.total_tokens
205
- }
206
-
207
- try:
208
- content = response.choices[0].message.content
209
-
210
- # Extract JSON code block
211
-
212
- output = json.loads(content)
213
- # output = json.loads(response.choices[0].message.content)
214
-
215
- if 'message' not in output or 'header' not in output:
216
- print(f"'message' or 'header' is missing in response on attempt {attempt + 1}. Retrying...")
217
- continue # Continue to next attempt
218
-
219
- else:
220
- if len(output["header"].strip()) > self.Core.config_file["header_limit"] or len(
221
- output["message"].strip()) > self.Core.config_file["message_limit"]:
222
- print(
223
- f"'header' or 'message' is more than specified characters in response on attempt {attempt + 1}. Retrying...")
224
- continue
225
-
226
- # validating the JSON
227
- self.Core.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
228
- self.Core.total_tokens['completion_tokens'] += tokens['completion_tokens']
229
- self.Core.temp_token_counter += tokens['total_tokens']
230
- return output
231
-
232
- except json.JSONDecodeError:
233
- print(f"Invalid JSON from LLM on attempt {attempt + 1}. Retrying...")
234
-
235
- except openai.APIConnectionError as e:
236
- print("The server could not be reached")
237
- print(e.__cause__) # an underlying Exception, likely raised within httpx.
238
- except openai.RateLimitError as e:
239
- print("A 429 status code was received; we should back off a bit.")
240
- except openai.APIStatusError as e:
241
- print("Another non-200-range status code was received")
242
- print(e.status_code)
243
- print(e.response)
244
-
245
- print("Max retries exceeded. Returning empty response.")
246
- return None
247
-
248
  # --------------------------------------------------------------
249
  # --------------------------------------------------------------
250
  def llm_instructions(self):
@@ -256,7 +183,7 @@ class MessageGenerator:
256
  jargon_list = "\n".join(f"- {word}" for word in self.Core.config_file["AI_Jargon"])
257
 
258
  instructions = f"""
259
- You are texting a friend and it should feel like a friendly encouraging nudge. Your task is to write a 'header' and a 'message' as a push notification for a {self.Core.get_instrument()} student that sounds like natural everyday speech: friendly, concise, no jargon, and following the instructions.
260
  Write a SUPER CASUAL and NATURAL push notification, as if you are chatting over coffee. Avoid odd phrasings.
261
 
262
  ABSOLUTE RULE – OVERRIDES EVERYTHING ELSE:
 
10
  from Messaging_system.MultiMessage import MultiMessage
11
  from Messaging_system.protection_layer import ProtectionLayer
12
  import openai
13
+ from Messaging_system.LLM import LLM
14
 
15
 
16
  class MessageGenerator:
17
 
18
  def __init__(self, CoreConfig):
19
  self.Core = CoreConfig
20
+ self.llm = LLM(CoreConfig)
21
 
22
  # --------------------------------------------------------------
23
  # --------------------------------------------------------------
 
38
  progress_callback(progress, total_users)
39
 
40
  if row["prompt"] is not None:
41
+ first_message = self.llm.get_response(prompt=row["prompt"], instructions=self.llm_instructions())
42
 
43
  if first_message is not None:
44
  # adding protection layer
45
+ protect = ProtectionLayer(CoreConfig=self.Core)
 
46
  message, total_tokens = protect.criticize(message=first_message, user=row)
47
 
48
  # updating tokens
 
172
  }
173
  return output_message
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  # --------------------------------------------------------------
176
  # --------------------------------------------------------------
177
  def llm_instructions(self):
 
183
  jargon_list = "\n".join(f"- {word}" for word in self.Core.config_file["AI_Jargon"])
184
 
185
  instructions = f"""
186
+ You are a copywriter. Your task is to write a 'header' and a 'message' as a push notification for a {self.Core.get_instrument()} student that sounds like natural everyday speech: friendly, concise, no jargon, and following the instructions.
187
  Write a SUPER CASUAL and NATURAL push notification, as if you are chatting over coffee. Avoid odd phrasings.
188
 
189
  ABSOLUTE RULE – OVERRIDES EVERYTHING ELSE:
Messaging_system/MultiMessage.py CHANGED
@@ -3,6 +3,7 @@ import time
3
  from openai import OpenAI
4
  from Messaging_system.protection_layer import ProtectionLayer
5
  import openai
 
6
 
7
  class MultiMessage:
8
  def __init__(self, CoreConfig):
@@ -11,6 +12,7 @@ class MultiMessage:
11
  for each user, building on previously generated messages.
12
  """
13
  self.Core = CoreConfig
 
14
 
15
  # --------------------------------------------------------------
16
  def generate_multi_messages(self, user):
@@ -41,8 +43,7 @@ class MultiMessage:
41
 
42
  # We'll reuse the same ProtectionLayer
43
  protect = ProtectionLayer(
44
- config_file=self.Core.config_file,
45
- messaging_mode=self.Core.messaging_mode
46
  )
47
 
48
  # If user requested multiple messages, generate the rest
@@ -107,7 +108,8 @@ class MultiMessage:
107
  prompt = self.generate_prompt(context, step)
108
 
109
  # 2) Call our existing LLM routine
110
- response_dict = self.get_llm_response(prompt)
 
111
  return response_dict
112
 
113
  # ===============================================================
@@ -292,78 +294,6 @@ Return only JSON of the form:
292
  return output_message
293
 
294
  # --------------------------------------------------------------
295
- def get_llm_response(self, prompt, max_retries=4):
296
- """
297
- Calls the LLM (similar to MessageGenerator) with the prompt, returning a dict
298
- with keys like 'header' and 'message' if successful, or None otherwise.
299
-
300
- :param prompt: The text prompt for the LLM.
301
- :param max_retries: Number of retries for potential LLM/connection failures.
302
- :return: Dictionary with 'header' and 'message', or None if unsuccessful.
303
- """
304
- openai.api_key = self.Core.api_key
305
- instructions = self.llm_instructions()
306
- client = OpenAI(api_key=self.Core.api_key)
307
-
308
- for attempt in range(max_retries):
309
- try:
310
- response = client.chat.completions.create(
311
- model=self.Core.model,
312
- response_format={"type": "json_object"},
313
- messages=[
314
- {"role": "system", "content": instructions},
315
- {"role": "user", "content": prompt}
316
- ],
317
- max_tokens=500,
318
- n=1,
319
- # temperature=self.Core.temperature
320
- temperature=0.7
321
- )
322
-
323
- tokens = {
324
- 'prompt_tokens': response.usage.prompt_tokens,
325
- 'completion_tokens': response.usage.completion_tokens,
326
- 'total_tokens': response.usage.total_tokens
327
- }
328
-
329
- try:
330
- content = response.choices[0].message.content
331
- output = json.loads(content)
332
-
333
- # Validate output keys
334
- if 'message' not in output or 'header' not in output:
335
- print(f"'message' or 'header' missing in response (attempt {attempt+1}). Retrying...")
336
- continue
337
-
338
- # Check character length constraints
339
- if (len(output["header"].strip()) > self.Core.config_file["header_limit"] or
340
- len(output["message"].strip()) > self.Core.config_file["message_limit"]):
341
- print(f"Header or message exceeded character limits (attempt {attempt+1}). Retrying...")
342
- continue
343
-
344
- # If we're good here, update token usage
345
- self.Core.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
346
- self.Core.total_tokens['completion_tokens'] += tokens['completion_tokens']
347
- self.Core.temp_token_counter += tokens['total_tokens']
348
-
349
- return output
350
-
351
- except json.JSONDecodeError:
352
- print(f"Invalid JSON from LLM (attempt {attempt+1}). Retrying...")
353
-
354
- except openai.APIConnectionError as e:
355
- print("The server could not be reached")
356
- print(e.__cause__)
357
- except openai.RateLimitError as e:
358
- print("Received a 429 status code; backing off might be needed.")
359
- except openai.APIStatusError as e:
360
- print("A non-200 status code was received")
361
- print(e.status_code)
362
- print(e.response)
363
-
364
- print("Max retries exceeded. Returning None.")
365
- return None
366
-
367
  # --------------------------------------------------------------
368
 
369
  def llm_instructions(self):
@@ -375,7 +305,7 @@ Return only JSON of the form:
375
  jargon_list = "\n".join(f"- {word}" for word in self.Core.config_file["AI_Jargon"])
376
 
377
  instructions = f"""
378
- You are texting a friend and it should feel like a friendly encouraging nudge. Your task is to write a 'header' and a 'message' as a push notification for a {self.Core.get_instrument()} student that sounds like natural everyday speech: friendly, concise, no jargon, and following the instructions.
379
  Write a SUPER CASUAL and NATURAL push notification, as if you are chatting over coffee. Avoid odd phrasings.
380
 
381
  ABSOLUTE RULE – OVERRIDES EVERYTHING ELSE:
 
3
  from openai import OpenAI
4
  from Messaging_system.protection_layer import ProtectionLayer
5
  import openai
6
+ from Messaging_system.LLM import LLM
7
 
8
  class MultiMessage:
9
  def __init__(self, CoreConfig):
 
12
  for each user, building on previously generated messages.
13
  """
14
  self.Core = CoreConfig
15
+ self.llm = LLM(CoreConfig)
16
 
17
  # --------------------------------------------------------------
18
  def generate_multi_messages(self, user):
 
43
 
44
  # We'll reuse the same ProtectionLayer
45
  protect = ProtectionLayer(
46
+ CoreConfig=self.Core
 
47
  )
48
 
49
  # If user requested multiple messages, generate the rest
 
108
  prompt = self.generate_prompt(context, step)
109
 
110
  # 2) Call our existing LLM routine
111
+ response_dict = self.llm.get_response(prompt=prompt, instructions=self.llm_instructions())
112
+
113
  return response_dict
114
 
115
  # ===============================================================
 
294
  return output_message
295
 
296
  # --------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  # --------------------------------------------------------------
298
 
299
  def llm_instructions(self):
 
305
  jargon_list = "\n".join(f"- {word}" for word in self.Core.config_file["AI_Jargon"])
306
 
307
  instructions = f"""
308
+ You are a copywriter. Your task is to write a 'header' and a 'message' as a push notification for a {self.Core.get_instrument()} student that sounds like natural everyday speech: friendly, concise, no jargon, and following the instructions.
309
  Write a SUPER CASUAL and NATURAL push notification, as if you are chatting over coffee. Avoid odd phrasings.
310
 
311
  ABSOLUTE RULE – OVERRIDES EVERYTHING ELSE:
Messaging_system/Ollama.py DELETED
@@ -1,166 +0,0 @@
1
- import json
2
- import time
3
-
4
- import torch
5
- import ollama
6
-
7
- class LocalLM:
8
-
9
- def __init__(self, model):
10
- # Initialize the Ollama client
11
- self.client = ollama.Client()
12
- self.model = model
13
-
14
- # def get_llm_response(self, prompt):
15
- #
16
- # # Send the query to the model
17
- # response = self.client.generate(model=self.model, prompt=prompt)
18
- # return response.response
19
-
20
- def preprocess_and_parse_json(self, response):
21
-
22
- # Remove any leading/trailing whitespace and newlines
23
- if response.startswith('```json') and response.endswith('```'):
24
- response = response[len('```json'):-len('```')].strip()
25
-
26
- # Parse the cleaned response into a JSON object
27
- try:
28
- json_object = json.loads(response)
29
- return json_object
30
- except json.JSONDecodeError as e:
31
- print(f"Failed to parse JSON: {e}")
32
- return None
33
-
34
-
35
- def get_llm_response(self, prompt, max_retries=4):
36
- """
37
- sending the prompt to the LLM and get back the response
38
- """
39
-
40
- openai.api_key = self.Core.api_key
41
- instructions = self.llm_instructions()
42
- client = OpenAI(api_key=self.Core.api_key)
43
-
44
- for attempt in range(max_retries):
45
- try:
46
- response = client.chat.completions.create(
47
- model=self.Core.model,
48
- response_format={"type": "json_object"},
49
- messages=[
50
- {"role": "system", "content": instructions},
51
- {"role": "user", "content": prompt}
52
- ],
53
- max_tokens=500,
54
- n=1,
55
- # temperature=self.Core.temperature,
56
- temperature=0.7,
57
- )
58
-
59
- tokens = {
60
- 'prompt_tokens': response.usage.prompt_tokens,
61
- 'completion_tokens': response.usage.completion_tokens,
62
- 'total_tokens': response.usage.total_tokens
63
- }
64
-
65
- try:
66
- content = response.choices[0].message.content
67
-
68
- # Extract JSON code block
69
-
70
- output = json.loads(content)
71
- # output = json.loads(response.choices[0].message.content)
72
-
73
- if 'message' not in output or 'header' not in output:
74
- print(f"'message' or 'header' is missing in response on attempt {attempt + 1}. Retrying...")
75
- continue # Continue to next attempt
76
-
77
- else:
78
- if len(output["header"].strip()) > self.Core.config_file["header_limit"] or len(
79
- output["message"].strip()) > self.Core.config_file["message_limit"]:
80
- print(
81
- f"'header' or 'message' is more than specified characters in response on attempt {attempt + 1}. Retrying...")
82
- continue
83
-
84
- # validating the JSON
85
- self.Core.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
86
- self.Core.total_tokens['completion_tokens'] += tokens['completion_tokens']
87
- self.Core.temp_token_counter += tokens['total_tokens']
88
- return output
89
-
90
- except json.JSONDecodeError:
91
- print(f"Invalid JSON from LLM on attempt {attempt + 1}. Retrying...")
92
-
93
- except openai.APIConnectionError as e:
94
- print("The server could not be reached")
95
- print(e.__cause__) # an underlying Exception, likely raised within httpx.
96
- except openai.RateLimitError as e:
97
- print("A 429 status code was received; we should back off a bit.")
98
- except openai.APIStatusError as e:
99
- print("Another non-200-range status code was received")
100
- print(e.status_code)
101
- print(e.response)
102
-
103
- print("Max retries exceeded. Returning empty response.")
104
- return None
105
-
106
- def get_llm_response(self, prompt, mode, max_retries=10):
107
- """
108
- Send the prompt to the LLM and get back the response.
109
- Includes handling for GPU memory issues by clearing cache and waiting before retry.
110
- """
111
-
112
- for attempt in range(max_retries):
113
- try:
114
- # Try generating the response
115
- response = self.client.generate(model=self.model, prompt=prompt)
116
- except Exception as e:
117
- # This catches errors like the connection being forcibly closed
118
- print(f"Error on attempt {attempt + 1}: {e}.")
119
- try:
120
- # Clear GPU cache if you're using PyTorch; this may help free up memory
121
- torch.cuda.empty_cache()
122
- print("Cleared GPU cache.")
123
- except Exception as cache_err:
124
- print("Failed to clear GPU cache:", cache_err)
125
- # Wait a bit before retrying to allow memory to recover
126
- time.sleep(2)
127
- continue
128
-
129
- try:
130
- tokens = {
131
- 'prompt_tokens': 0,
132
- 'completion_tokens': 0,
133
- 'total_tokens': 0
134
- }
135
-
136
- try:
137
- output = self.preprocess_and_parse_json(response.response)
138
- if output is None:
139
- continue
140
-
141
- if mode == "rating":
142
- # Check if all keys and values are integers (or convertible to integers)
143
- all_int = True
144
- for k, v in output.items():
145
- try:
146
- int(k)
147
- int(v)
148
- except ValueError:
149
- all_int = False
150
- break
151
- if all_int:
152
- return output, tokens
153
- else:
154
- print(f"Keys and values are not integers on attempt {attempt + 1}. Retrying...")
155
- continue # Continue to next attempt
156
- else:
157
- print(f"Invalid mode: {mode}")
158
- return None, tokens
159
-
160
- except json.JSONDecodeError:
161
- print(f"Invalid JSON from LLM on attempt {attempt + 1}. Retrying...")
162
- except Exception as parse_error:
163
- print("Error processing output:", parse_error)
164
-
165
- print("Max retries exceeded. Returning empty response.")
166
- return [], {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Messaging_system/Permes.py CHANGED
@@ -24,7 +24,7 @@ class Permes:
24
  def create_personalize_messages(self, session, users, brand, config_file, openai_api_key, CTA, segment_info,
25
  platform="push", number_of_messages=1, instructionset=None, subsequent_examples=None,
26
  message_style=None, selected_input_features=None, selected_source_features=None
27
- , recsys_contents=None,
28
  additional_instructions=None, identifier_column="user_id",
29
  sample_example=None, number_of_samples=None, involve_recsys_result=False,
30
  messaging_mode="message", target_column=None, ongoing_df=None,
@@ -83,6 +83,9 @@ class Permes:
83
  if selected_source_features is not None:
84
  personalize_message.set_features_to_use(selected_source_features)
85
 
 
 
 
86
  # if involve_recsys_result is not None:
87
  # personalize_message.set_messaging_mode("recsys_result")
88
 
 
24
  def create_personalize_messages(self, session, users, brand, config_file, openai_api_key, CTA, segment_info,
25
  platform="push", number_of_messages=1, instructionset=None, subsequent_examples=None,
26
  message_style=None, selected_input_features=None, selected_source_features=None
27
+ , recsys_contents=None, model=None,
28
  additional_instructions=None, identifier_column="user_id",
29
  sample_example=None, number_of_samples=None, involve_recsys_result=False,
30
  messaging_mode="message", target_column=None, ongoing_df=None,
 
83
  if selected_source_features is not None:
84
  personalize_message.set_features_to_use(selected_source_features)
85
 
86
+ if model is not None:
87
+ personalize_message.set_llm_model(selected_source_features)
88
+
89
  # if involve_recsys_result is not None:
90
  # personalize_message.set_messaging_mode("recsys_result")
91
 
Messaging_system/PromptGenerator.py CHANGED
@@ -109,7 +109,7 @@ class PromptGenerator:
109
  """
110
 
111
  context = f"""
112
- You are texting a friend and it should feel like a friendly encouraging nudge. Your task is to write a 'header' and a 'message' as a push notification for a {self.Core.get_instrument()} student that sounds like everyday natural speech: friendly, short, no jargon, and following the instructions.
113
  """
114
 
115
  return context
 
109
  """
110
 
111
  context = f"""
112
+ Your task is to write a 'header' and a 'message' as a push notification for a {self.Core.get_instrument()} student that sounds like everyday natural speech: friendly, short, no jargon, and following the instructions.
113
  """
114
 
115
  return context
Messaging_system/protection_layer.py CHANGED
@@ -7,7 +7,7 @@ import os
7
  import openai
8
  from openai import OpenAI
9
  from dotenv import load_dotenv
10
- load_dotenv()
11
 
12
 
13
  # -----------------------------------------------------------------------
@@ -17,16 +17,11 @@ class ProtectionLayer:
17
  Protection layer to double check the generated message:
18
  """
19
 
20
- def __init__(self, config_file, messaging_mode):
21
 
22
- self.config_file = config_file
23
- self.messaging_mode = messaging_mode
24
-
25
- # LLM configs
26
- self.api_key = os.environ.get("OPENAI_API") # will be set by user
27
- self.model = "gpt-4o-mini" # will be set by user
28
- self.temperature = 0
29
 
 
30
  # to trace the number of tokens and estimate the cost if needed
31
  self.total_tokens = {
32
  'prompt_tokens': 0,
@@ -39,11 +34,11 @@ class ProtectionLayer:
39
  Setting instructions for the LLM for the second pass.
40
  """
41
 
42
- jargon_list = "\n".join(f"- {word}" for word in self.config_file["AI_Jargon"])
43
 
44
  instructions = f"""
45
 
46
- You are texting a friend that should feels like a friendly nudge. Your job is to *either* approve the candidate message or return a corrected version that obeys the style guide.
47
  the 'header' and a 'message' as a push notification should sounds like everyday speech—friendly, short, no jargon, following the instructions.
48
 
49
  ABSOLUTE RULE – OVERRIDES EVERYTHING ELSE:
@@ -108,72 +103,6 @@ class ProtectionLayer:
108
  return instructions
109
 
110
  # --------------------------------------------------------------
111
- def get_llm_response(self, prompt, max_retries=3):
112
- """
113
- sending the prompt to the LLM and get back the response
114
- """
115
-
116
- openai.api_key = self.api_key
117
- instructions = self.llm_instructions()
118
- client = OpenAI(api_key=self.api_key)
119
-
120
- for attempt in range(max_retries):
121
- try:
122
- response = client.chat.completions.create(
123
- model=self.model,
124
- response_format={"type": "json_object"},
125
- messages=[
126
- {"role": "system", "content": instructions},
127
- {"role": "user", "content": prompt}
128
- ],
129
- max_tokens=500,
130
- n=1,
131
- temperature=self.temperature
132
- )
133
-
134
- tokens = {
135
- 'prompt_tokens': response.usage.prompt_tokens,
136
- 'completion_tokens': response.usage.completion_tokens,
137
- 'total_tokens': response.usage.total_tokens
138
- }
139
-
140
- try:
141
- content = response.choices[0].message.content
142
- # Extract JSON code block
143
-
144
- output = json.loads(content)
145
- # output = json.loads(response.choices[0].message.content)
146
-
147
- if 'message' not in output or 'header' not in output:
148
- print(f"'message' or 'header' is missing in response on attempt {attempt + 1}. Retrying...")
149
- continue # Continue to next attempt
150
-
151
- else:
152
- if len(output["header"].strip()) > self.config_file["header_limit"] or len(output["message"].strip()) > self.config_file["message_limit"]:
153
- print(f"'header' or 'message' is more than specified characters in response on attempt {attempt + 1}. Retrying...")
154
- continue
155
-
156
- # validating the JSON
157
- self.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
158
- self.total_tokens['completion_tokens'] += tokens['completion_tokens']
159
- return output
160
-
161
- except json.JSONDecodeError:
162
- print(f"Invalid JSON from LLM on attempt {attempt + 1}. Retrying...")
163
-
164
- except openai.APIConnectionError as e:
165
- print("The server could not be reached")
166
- print(e.__cause__) # an underlying Exception, likely raised within httpx.
167
- except openai.RateLimitError as e:
168
- print("A 429 status code was received; we should back off a bit.")
169
- except openai.APIStatusError as e:
170
- print("Another non-200-range status code was received")
171
- print(e.status_code)
172
- print(e.response)
173
-
174
- print("Max retries exceeded. Returning empty response.")
175
- return [], {}
176
-
177
  # --------------------------------------------------------------
178
  def get_context(self):
179
  """
@@ -196,7 +125,7 @@ class ProtectionLayer:
196
  :return: new prompt
197
  """
198
  # recommended_content = ""
199
- # if self.messaging_mode == "recsys_result":
200
  # recommended_content = f"""
201
  # ### ** Recommended Content **
202
  # {user['recommendation_info']}
@@ -229,7 +158,7 @@ class ProtectionLayer:
229
  """
230
 
231
  prompt = self.generate_prompt(message, user)
232
- response = self.get_llm_response(prompt)
233
 
234
  return response, self.total_tokens
235
 
 
7
  import openai
8
  from openai import OpenAI
9
  from dotenv import load_dotenv
10
+ from Messaging_system.LLM import LLM
11
 
12
 
13
  # -----------------------------------------------------------------------
 
17
  Protection layer to double check the generated message:
18
  """
19
 
20
+ def __init__(self, CoreConfig):
21
 
22
+ self.Core = CoreConfig
 
 
 
 
 
 
23
 
24
+ self.llm = LLM(CoreConfig)
25
  # to trace the number of tokens and estimate the cost if needed
26
  self.total_tokens = {
27
  'prompt_tokens': 0,
 
34
  Setting instructions for the LLM for the second pass.
35
  """
36
 
37
+ jargon_list = "\n".join(f"- {word}" for word in self.Core.config_file["AI_Jargon"])
38
 
39
  instructions = f"""
40
 
41
+ You are friendly copywriter. Your job is to *either* approve the candidate message or return a corrected version that obeys the style guide.
42
  the 'header' and a 'message' as a push notification should sounds like everyday speech—friendly, short, no jargon, following the instructions.
43
 
44
  ABSOLUTE RULE – OVERRIDES EVERYTHING ELSE:
 
103
  return instructions
104
 
105
  # --------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  # --------------------------------------------------------------
107
  def get_context(self):
108
  """
 
125
  :return: new prompt
126
  """
127
  # recommended_content = ""
128
+ # if self.Core.messaging_mode == "recsys_result":
129
  # recommended_content = f"""
130
  # ### ** Recommended Content **
131
  # {user['recommendation_info']}
 
158
  """
159
 
160
  prompt = self.generate_prompt(message, user)
161
+ response = self.llm.get_response(prompt=prompt, instructions=self.llm_instructions())
162
 
163
  return response, self.total_tokens
164
 
app.py CHANGED
@@ -81,7 +81,7 @@ def init_state() -> None:
81
  valid_instructions="",
82
  invalid_instructions="",
83
  messaging_type="push",
84
- generated=False,
85
  include_recommendation=False,
86
  data=None, brand=None, recsys_contents=[], csv_output=None,
87
  users_message=None, messaging_mode=None, target_column=None,
@@ -90,7 +90,7 @@ def init_state() -> None:
90
  additional_instructions=None, segment_info="", message_style="",
91
  sample_example="", CTA="", all_features=None, number_of_messages=1,
92
  instructionset={}, subsequent_examples={}, segment_name="", number_of_samples=10,
93
- selected_source_features=[], platform=None, generate_clicked=False,
94
  )
95
  for k, v in defaults.items():
96
  st.session_state.setdefault(k, v)
@@ -163,6 +163,13 @@ with st.sidebar:
163
  key="brand",
164
  )
165
 
 
 
 
 
 
 
 
166
  # ─ Personalisation
167
  st.text_area("Segment info *", key="segment_info")
168
  st.text_area("CTA (Call to Action) *", key="CTA")
@@ -258,7 +265,6 @@ with tab2:
258
  warehouse=get_credential("snowflake_warehouse"),
259
  schema=get_credential("snowflake_schema")
260
  )
261
- config = load_config_("Config_files/message_system_config.json")
262
  session = Session.builder.configs(conn).create()
263
 
264
  # ─ prepare parameters
@@ -295,8 +301,9 @@ with tab2:
295
  session=session,
296
  users=st.session_state.data,
297
  brand=st.session_state.brand,
298
- config_file=config,
299
  openai_api_key=get_credential("OPENAI_API"),
 
300
  CTA=st.session_state.CTA,
301
  segment_info=st.session_state.segment_info,
302
  number_of_samples=st.session_state.number_of_samples,
 
81
  valid_instructions="",
82
  invalid_instructions="",
83
  messaging_type="push",
84
+ generated=False, model=None,
85
  include_recommendation=False,
86
  data=None, brand=None, recsys_contents=[], csv_output=None,
87
  users_message=None, messaging_mode=None, target_column=None,
 
90
  additional_instructions=None, segment_info="", message_style="",
91
  sample_example="", CTA="", all_features=None, number_of_messages=1,
92
  instructionset={}, subsequent_examples={}, segment_name="", number_of_samples=10,
93
+ selected_source_features=[], platform=None, generate_clicked=False, config=None,
94
  )
95
  for k, v in defaults.items():
96
  st.session_state.setdefault(k, v)
 
163
  key="brand",
164
  )
165
 
166
+ # ─ Brand
167
+ st.selectbox(
168
+ "LLM model *",
169
+ st.session_state.config["LLM_models"],
170
+ key="model",
171
+ )
172
+
173
  # ─ Personalisation
174
  st.text_area("Segment info *", key="segment_info")
175
  st.text_area("CTA (Call to Action) *", key="CTA")
 
265
  warehouse=get_credential("snowflake_warehouse"),
266
  schema=get_credential("snowflake_schema")
267
  )
 
268
  session = Session.builder.configs(conn).create()
269
 
270
  # ─ prepare parameters
 
301
  session=session,
302
  users=st.session_state.data,
303
  brand=st.session_state.brand,
304
+ config_file=st.session_state.config,
305
  openai_api_key=get_credential("OPENAI_API"),
306
+ model=st.session_state.model,
307
  CTA=st.session_state.CTA,
308
  segment_info=st.session_state.segment_info,
309
  number_of_samples=st.session_state.number_of_samples,
messaging_main_test.py CHANGED
@@ -114,13 +114,16 @@ if __name__ == "__main__":
114
  brand = "singeo"
115
  identifier_column = "user_id"
116
 
117
- segment_info = """This is a singeo user who didn't practiced for a while."""
118
 
119
  # sample inputs
120
 
121
- CTA = """Re-engage the user by reminding them of their goals, and by recommending content that will get them back on track. """
122
 
123
- # sample_example = """we have crafted a perfect set of courses just for you! come and check it out!"""
 
 
 
124
 
125
  # additional_instructions = """Include weeks_since _last_interaction in the message if you can create a better message to re-engage the user."""
126
  additional_instructions = None
@@ -133,14 +136,14 @@ if __name__ == "__main__":
133
  # number of messages to generate
134
  number_of_messages = 3
135
  instructionset = {
136
- 1: "Be highly motivational positive and kind",
137
- 2: "Be highly motivational positive and kind",
138
- 3: "Be highly motivational positive and kind",
139
  }
140
 
141
  subsequent_examples = {
142
- 1: "header: Don’t Stop Believin’"
143
- "message: You're closer to your goals than you think! Start singing now.",
144
  2: "header: Here Comes The Sun"
145
  "message: A quick practice session will light up your day. Let’s get right back at it. ",
146
  3: "header: Ain’t No Mountain High Enough"
@@ -152,21 +155,10 @@ if __name__ == "__main__":
152
 
153
  # messaging_mode = "recommend_playlist"
154
 
155
- sample_example = """
156
- 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.
157
-
158
-
159
- Example 1
160
- header: Your voice Misses You, [first_name]
161
- message: It’s been a while. Jump back in with this quick lesson!
162
-
163
- """
164
-
165
- # sample_example = None
166
 
167
  platform = "push"
168
 
169
- selected_source_features = ["first_name", "weeks_since_last_interaction"]
170
  selected_input_features = None
171
 
172
  segment_name = "no_recent_activity"
 
114
  brand = "singeo"
115
  identifier_column = "user_id"
116
 
117
+ segment_info = """Student who haven't practiced for a few days"""
118
 
119
  # sample inputs
120
 
121
+ CTA = """The goal is to tell them to practice singing"""
122
 
123
+ sample_example = """
124
+ Header: Sing Your Heart Out
125
+ Message: It’s been a few days. Take a lesson today and start practicing!
126
+ """
127
 
128
  # additional_instructions = """Include weeks_since _last_interaction in the message if you can create a better message to re-engage the user."""
129
  additional_instructions = None
 
136
  # number of messages to generate
137
  number_of_messages = 3
138
  instructionset = {
139
+ 1: "Talk like a singing coach motivating your student. Don't say things a singer wouldn't say. Make the message quick, concise, and in casual language. Tell them to practice, take a lesson, or warm up today. ",
140
+ 2: "Talk like a singing coach motivating your student. Don't say things a singer wouldn't say. Make the message quick, concise, and in casual language. Tell them to practice, take a lesson, or warm up today. ",
141
+ 3: "Talk like a singing coach motivating your student. Don't say things a singer wouldn't say. Make the message quick, concise, and in casual language. Tell them to practice, take a lesson, or warm up today. ",
142
  }
143
 
144
  subsequent_examples = {
145
+ 1: "Header: Sing Your Heart Out!"
146
+ "Message: It’s been a few days. Take a lesson today and start practicing!",
147
  2: "header: Here Comes The Sun"
148
  "message: A quick practice session will light up your day. Let’s get right back at it. ",
149
  3: "header: Ain’t No Mountain High Enough"
 
155
 
156
  # messaging_mode = "recommend_playlist"
157
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  platform = "push"
160
 
161
+ selected_source_features = None
162
  selected_input_features = None
163
 
164
  segment_name = "no_recent_activity"