Web Search and Multi-Modality

#16
by BMCVRN - opened
.github/workflows/deployment.yml CHANGED
@@ -55,7 +55,7 @@ jobs:
55
  env:
56
  IMAGE_TAG: ${{ github.sha }}
57
  run: |
58
- docker build --build-arg FASTAPI_KEY=${{secrets.FASTAPI_KEY}} --build-arg OPENAI_API_KEY=${{secrets.OPENAI_API_KEY}} -t ${{inputs.ecr_url}}:$IMAGE_TAG .
59
  docker push ${{inputs.ecr_url}}:$IMAGE_TAG
60
  echo "image=${{inputs.ecr_url}}:$IMAGE_TAG" >> $GITHUB_OUTPUT
61
 
 
55
  env:
56
  IMAGE_TAG: ${{ github.sha }}
57
  run: |
58
+ docker build --build-arg FASTAPI_KEY=${{secrets.FASTAPI_KEY}} --build-arg BING_API_KEY=${{secrets.BING_API_KEY}} --build-arg OPENAI_API_KEY=${{secrets.OPENAI_API_KEY}} -t ${{inputs.ecr_url}}:$IMAGE_TAG .
59
  docker push ${{inputs.ecr_url}}:$IMAGE_TAG
60
  echo "image=${{inputs.ecr_url}}:$IMAGE_TAG" >> $GITHUB_OUTPUT
61
 
Dockerfile CHANGED
@@ -29,12 +29,14 @@ ARG FASTAPI_KEY
29
  ARG OPENAI_API_KEY
30
  ARG OPENAI_GENERAL_ASSISTANT
31
  ARG SENTRY_DSN
 
32
 
33
  ENV FASTAPI_KEY=$FASTAPI_KEY
34
  ENV OPENAI_API_KEY=$OPENAI_API_KEY
35
  ENV OPENAI_GENERAL_ASSISTANT=$OPENAI_GENERAL_ASSISTANT
36
  ENV PYTHONUNBUFFERED=1
37
  ENV SENTRY_DSN=$SENTRY_DSN
 
38
 
39
  # Set the working directory
40
  # WORKDIR /code
 
29
  ARG OPENAI_API_KEY
30
  ARG OPENAI_GENERAL_ASSISTANT
31
  ARG SENTRY_DSN
32
+ ARG BING_API_KEY
33
 
34
  ENV FASTAPI_KEY=$FASTAPI_KEY
35
  ENV OPENAI_API_KEY=$OPENAI_API_KEY
36
  ENV OPENAI_GENERAL_ASSISTANT=$OPENAI_GENERAL_ASSISTANT
37
  ENV PYTHONUNBUFFERED=1
38
  ENV SENTRY_DSN=$SENTRY_DSN
39
+ ENV BING_API_KEY=$BING_API_KEY
40
 
41
  # Set the working directory
42
  # WORKDIR /code
app/assistants.py CHANGED
@@ -16,173 +16,14 @@ import pytz
16
  from app.exceptions import AssistantError, BaseOurcoachException, OpenAIRequestError, UtilsError
17
  from app.utils import get_booked_gg_sessions, get_growth_guide, get_growth_guide_summary, get_user_subscriptions, get_users_mementos, print_log
18
  from app.flows import FOLLOW_UP_STATE, GENERAL_COACHING_STATE, MICRO_ACTION_STATE, REFLECTION_STATE
 
19
 
20
  load_dotenv()
21
 
22
  logger = logging.getLogger(__name__)
23
  OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
24
 
25
- import requests
26
 
27
- class SearchEngine:
28
- BING_API_KEY = os.getenv("BING_API_KEY")
29
- BING_ENDPOINT = 'https://api.bing.microsoft.com/v7.0'
30
-
31
- @staticmethod
32
- def search(feedback_type_name, search_term):
33
- """
34
- Public method to perform a search based on the feedback type.
35
- """
36
- search_methods = {
37
- "Resource Links": SearchEngine._search_relevant_links,
38
- "Book/Podcast Recommendations": SearchEngine._search_books_or_podcasts,
39
- # "Success Stories/Testimonials": SearchEngine._search_success_stories,
40
- "Inspirational Stories or Case Studies": SearchEngine._search_inspirational_stories,
41
- "Fun Facts": SearchEngine._search_fun_facts,
42
- # "Visual Content (Images, Infographics)": SearchEngine._search_visual_content,
43
- "Personalised Recommendations": SearchEngine._search_personalized_recommendations
44
- }
45
-
46
- search_method = search_methods.get(feedback_type_name)
47
-
48
- if search_method:
49
- return search_method(search_term)
50
- else:
51
- return (feedback_type_name, search_term)
52
-
53
- @staticmethod
54
- def _search_relevant_links(search_term):
55
- """
56
- Uses Bing Web Search API to search for relevant links.
57
- """
58
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
59
- params = {'q': search_term, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
60
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
61
- if response.status_code == 200:
62
- data = response.json()
63
- links = []
64
- if 'webPages' in data and 'value' in data['webPages']:
65
- for result in data['webPages']['value']:
66
- links.append(result)
67
- return links
68
- return ["No relevant links found."]
69
-
70
- @staticmethod
71
- def _search_books_or_podcasts(search_term):
72
- """
73
- Uses Bing Web Search API to search for books or podcasts.
74
- """
75
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
76
- query = f"{search_term} book OR podcast"
77
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
78
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
79
- if response.status_code == 200:
80
- data = response.json()
81
- recommendations = []
82
- if 'webPages' in data and 'value' in data['webPages']:
83
- for result in data['webPages']['value']:
84
- title = result.get('name', 'Unknown Title')
85
- url = result.get('url', '')
86
- recommendations.append(f"{title}: {url}")
87
- return recommendations
88
- return ["No book or podcast recommendations found."]
89
-
90
- @staticmethod
91
- def _search_success_stories(search_term):
92
- """
93
- Uses Bing Web Search API to search for success stories.
94
- """
95
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
96
- query = f"{search_term} success stories"
97
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
98
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
99
- if response.status_code == 200:
100
- data = response.json()
101
- stories = []
102
- if 'webPages' in data and 'value' in data['webPages']:
103
- for result in data['webPages']['value']:
104
- title = result.get('name', 'Unknown Title')
105
- url = result.get('url', '')
106
- stories.append(f"{title}: {url}")
107
- return stories
108
- return ["No success stories found."]
109
-
110
- @staticmethod
111
- def _search_inspirational_stories(search_term):
112
- """
113
- Uses Bing Web Search API to search for inspirational stories or case studies.
114
- """
115
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
116
- query = f"{search_term} inspirational stories OR case studies"
117
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
118
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
119
- if response.status_code == 200:
120
- data = response.json()
121
- stories = []
122
- if 'webPages' in data and 'value' in data['webPages']:
123
- for result in data['webPages']['value']:
124
- title = result.get('name', 'Unknown Title')
125
- url = result.get('url', '')
126
- stories.append(f"{title}: {url}")
127
- return stories
128
- return ["No inspirational stories found."]
129
-
130
- @staticmethod
131
- def _search_fun_facts(search_term):
132
- """
133
- Uses Bing Web Search API to search for fun facts related to personal growth.
134
- """
135
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
136
- query = f"{search_term} fun facts"
137
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
138
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
139
- if response.status_code == 200:
140
- data = response.json()
141
- facts = []
142
- if 'webPages' in data and 'value' in data['webPages']:
143
- for result in data['webPages']['value']:
144
- snippet = result.get('snippet', '')
145
- facts.append(snippet)
146
- return facts
147
- return ["No fun facts found."]
148
-
149
- @staticmethod
150
- def _search_visual_content(search_term):
151
- """
152
- Uses Bing Image Search API to search for images or infographics.
153
- """
154
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
155
- params = {'q': search_term, 'count': 3}
156
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/images/search", headers=headers, params=params)
157
- if response.status_code == 200:
158
- data = response.json()
159
- images = []
160
- if 'value' in data:
161
- for result in data['value']:
162
- image_url = result.get('contentUrl', '')
163
- images.append(image_url)
164
- return images
165
- return ["No visual content found."]
166
-
167
- @staticmethod
168
- def _search_personalized_recommendations(search_term):
169
- """
170
- Uses Bing Web Search API to provide personalized recommendations.
171
- """
172
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
173
- query = f"tips for {search_term}"
174
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
175
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
176
- if response.status_code == 200:
177
- data = response.json()
178
- recommendations = []
179
- if 'webPages' in data and 'value' in data['webPages']:
180
- for result in data['webPages']['value']:
181
- title = result.get('name', 'Unknown Title')
182
- url = result.get('url', '')
183
- recommendations.append(f"{title}: {url}")
184
- return recommendations
185
- return ["No personalized recommendations found."]
186
 
187
  class FeedbackContent:
188
  def __init__(self, content, role):
@@ -569,11 +410,11 @@ class Assistant:
569
  "tool_call_id": tool.id,
570
  "output": "Generate a final coach message (feedback message) using these 3 feedback types (together with the stated emoji at the beginning of each feedback): " + str(sample_feedbacks)
571
  })
572
- elif tool.function.name == "search_resource":
573
  type = json.loads(tool.function.arguments)['resource_type']
574
  query = json.loads(tool.function.arguments)['query']
575
  logger.info(f"Getting microaction theme: {type} - {query}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
576
- relevant_context = SearchEngine.search(type, query)
577
  logger.info(f"Finish Getting microaction theme: {relevant_context}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
578
  tool_outputs.append({
579
  "tool_call_id": tool.id,
@@ -676,7 +517,7 @@ class Assistant:
676
  user_info = '** If the user asks for their progress, also include a link to their Revelation Dashboard: {OURCOACH_DASHBOARD_URL} so that they can find out more **\n\n'
677
  if category == "personal":
678
  user_info += f"** Personal Information **\n\n{self.cm.user.user_info}"
679
- user_info += f"\n\n** User's Mantra This Week**\n\n{self.cm.user.mantra}"
680
  elif category == "challenges":
681
  user_info += f"** User's Challenges (prioritise ONGOING challenges) **\n\n{self.cm.user.challenges}\n\nLet the user know that ongoing challenges from their growth guide will be integrated into their day-to-day interaction."
682
  elif category == "recommended_actions":
 
16
  from app.exceptions import AssistantError, BaseOurcoachException, OpenAIRequestError, UtilsError
17
  from app.utils import get_booked_gg_sessions, get_growth_guide, get_growth_guide_summary, get_user_subscriptions, get_users_mementos, print_log
18
  from app.flows import FOLLOW_UP_STATE, GENERAL_COACHING_STATE, MICRO_ACTION_STATE, REFLECTION_STATE
19
+ from app.web_search import SearchEngine
20
 
21
  load_dotenv()
22
 
23
  logger = logging.getLogger(__name__)
24
  OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
25
 
 
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  class FeedbackContent:
29
  def __init__(self, content, role):
 
410
  "tool_call_id": tool.id,
411
  "output": "Generate a final coach message (feedback message) using these 3 feedback types (together with the stated emoji at the beginning of each feedback): " + str(sample_feedbacks)
412
  })
413
+ elif tool.function.name == "search_web":
414
  type = json.loads(tool.function.arguments)['resource_type']
415
  query = json.loads(tool.function.arguments)['query']
416
  logger.info(f"Getting microaction theme: {type} - {query}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
417
+ relevant_context = SearchEngine.search(type, query, self.cm.user.user_id)
418
  logger.info(f"Finish Getting microaction theme: {relevant_context}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
419
  tool_outputs.append({
420
  "tool_call_id": tool.id,
 
517
  user_info = '** If the user asks for their progress, also include a link to their Revelation Dashboard: {OURCOACH_DASHBOARD_URL} so that they can find out more **\n\n'
518
  if category == "personal":
519
  user_info += f"** Personal Information **\n\n{self.cm.user.user_info}"
520
+ user_info += f"\n\n** User's Mantra This Week:**\n\n{self.cm.user.mantra or 'Mantra not available.'}"
521
  elif category == "challenges":
522
  user_info += f"** User's Challenges (prioritise ONGOING challenges) **\n\n{self.cm.user.challenges}\n\nLet the user know that ongoing challenges from their growth guide will be integrated into their day-to-day interaction."
523
  elif category == "recommended_actions":
app/cache.py CHANGED
@@ -34,7 +34,7 @@ def upload_file_to_s3(filename):
34
  s3_client = session.client('s3')
35
  with open(filename, "rb") as f:
36
  ## Upload to Production Folder
37
- s3_client.upload_fileobj(f, bucket, f'staging/users/{user_id}.pkl')
38
  logger.info(f"File {filename} uploaded successfully to S3", extra={'user_id': user_id, 'endpoint': function_name})
39
 
40
  os.remove(filename)
 
34
  s3_client = session.client('s3')
35
  with open(filename, "rb") as f:
36
  ## Upload to Production Folder
37
+ s3_client.upload_fileobj(f, bucket, f'dev/users/{user_id}.pkl')
38
  logger.info(f"File {filename} uploaded successfully to S3", extra={'user_id': user_id, 'endpoint': function_name})
39
 
40
  os.remove(filename)
app/conversation_manager.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os
2
  import openai
3
  import pandas as pd
@@ -10,7 +11,9 @@ from datetime import datetime
10
 
11
  import dotenv
12
 
13
- from app.utils import id_to_persona
 
 
14
  dotenv.load_dotenv()
15
 
16
  OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
@@ -27,7 +30,13 @@ class ConversationManager:
27
  self.assistants = {'general': Assistant(asst_id, self), 'intro': Assistant('asst_baczEK65KKvPWIUONSzdYH8j', self)}
28
 
29
  self.client = client
30
- self.state = {'date': pd.Timestamp.now(tz='UTC').strftime("%Y-%m-%d %a %H:%M:%S")}
 
 
 
 
 
 
31
 
32
  self.current_thread = self.create_thread()
33
  self.daily_thread = None
@@ -70,6 +79,8 @@ class ConversationManager:
70
  {id_to_persona(self.user.asst_id)}, always adhere to your choosen persona by incorporating it conversationally.
71
  You represent a coach at ourcoach. You may refer to you Knowledgebase (ourcoach FAQ) for all information related to ourcoach.
72
  ** Branding ** Always stylize ourcoach as 'ourcoach' instead of 'OurCoach' or 'Ourcoach', regardless of any grammatical errors.
 
 
73
  -------------------------------------------
74
  You are coaching:
75
  \n\n{user_interaction_guidelines}\n\n\
@@ -103,11 +114,51 @@ class ConversationManager:
103
  return message
104
 
105
  @catch_error
106
- def _run_current_thread(self, text, thread=None, hidden=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  if thread is None:
108
  thread = self.current_thread
109
  logger.warning(f"{self}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
110
- logger.info(f"User Message: {text}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
 
 
111
 
112
  # need to select assistant
113
  if self.intro_done:
@@ -117,7 +168,7 @@ class ConversationManager:
117
  logger.info(f"Running intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
118
  run, just_finished_intro, message = self.assistants['intro'].process(thread, text)
119
 
120
- logger.info(f"Run {run.id} {run.status} just finished intro: {just_finished_intro}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
121
 
122
  if 'message' in run.metadata:
123
  info = run.metadata['message']
@@ -136,21 +187,21 @@ class ConversationManager:
136
 
137
  if hidden:
138
  self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
139
- logger.info(f"Deleted hidden message: {message}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
140
 
141
  return self._get_current_thread_history(remove_system_message=False)[-1], run
142
 
143
  @catch_error
144
  def _send_and_replace_message(self, text, replacement_msg=None):
145
- logger.info(f"Sending hidden message: {text}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
146
  response, _ = self._run_current_thread(text, hidden=True)
147
 
148
  # check if there is a replacement message
149
  if replacement_msg:
150
- logger.info(f"Adding replacement message: {replacement_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
151
  # get the last message
152
  last_msg = list(self.client.beta.threads.messages.list(self.current_thread.id, order="asc"))[-1]
153
- logger.info(f"Last message: {last_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
154
  response = last_msg.content[0].text.value
155
 
156
  # delete the last message
@@ -203,13 +254,13 @@ class ConversationManager:
203
  Be mindful of this information at all times in order to
204
  be as personalised as possible when conversing. Ensure to
205
  follow the conversation guidelines and flow provided.""", "role":"assistant"}] + messages[-29:]
206
- logger.info(f"Current Thread Messages: {messages}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
207
 
208
  temp_thread = self.client.beta.threads.create(messages=messages)
209
- logger.info(f"Created Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
210
 
211
  if add_to_main:
212
- logger.info(f"Adding message to main thread: {text}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
213
  self.add_message_to_thread(self.current_thread.id, "assistant", text)
214
 
215
  self.add_message_to_thread(temp_thread.id, "user", text)
@@ -220,7 +271,7 @@ class ConversationManager:
220
 
221
  # delete temp thread
222
  self.client.beta.threads.delete(temp_thread.id)
223
- logger.info(f"Deleted Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
224
 
225
  return response
226
 
@@ -229,19 +280,18 @@ class ConversationManager:
229
  if old_thread is None:
230
  old_thread = self.current_thread
231
 
232
- # create a new thread
233
- messages = [msg for msg in self._get_current_thread_history(remove_system_message=False) if not msg['content'].startswith("[hidden]")]
234
- if len(messages) >= 29:
235
- messages = messages[-29:]
236
- logger.info(f"Current Thread Messages: {messages}", extra={"user_id": self.user.user_id, "endpoint": "delete_hidden_messages"})
237
-
238
- new_thread = self.client.beta.threads.create(messages=messages)
239
 
240
- # delete old thread
241
- self.client.beta.threads.delete(old_thread.id)
242
-
243
- # set current thread
244
- self.current_thread = new_thread
 
 
 
245
 
246
  @catch_error
247
  def cancel_run(self, run, thread = None):
 
1
+ import base64
2
  import os
3
  import openai
4
  import pandas as pd
 
11
 
12
  import dotenv
13
 
14
+ from app.utils import get_user_local_timezone, id_to_persona
15
+ from PIL import Image
16
+ import io
17
  dotenv.load_dotenv()
18
 
19
  OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
 
30
  self.assistants = {'general': Assistant(asst_id, self), 'intro': Assistant('asst_baczEK65KKvPWIUONSzdYH8j', self)}
31
 
32
  self.client = client
33
+
34
+ # get users local timezone
35
+ timezone = get_user_local_timezone(user.user_id)
36
+
37
+ # convert UTC to user's timezone
38
+ local_time = pd.Timestamp.now(tz='UTC').tz_convert(timezone)
39
+ self.state = {'date': local_time.strftime("%Y-%m-%d %a %H:%M:%S")}
40
 
41
  self.current_thread = self.create_thread()
42
  self.daily_thread = None
 
79
  {id_to_persona(self.user.asst_id)}, always adhere to your choosen persona by incorporating it conversationally.
80
  You represent a coach at ourcoach. You may refer to you Knowledgebase (ourcoach FAQ) for all information related to ourcoach.
81
  ** Branding ** Always stylize ourcoach as 'ourcoach' instead of 'OurCoach' or 'Ourcoach', regardless of any grammatical errors.
82
+ **You truly believe you are your persona**
83
+ **You always try to be engaging and personal with your coachee in order to get them to continue interacting with you daily**
84
  -------------------------------------------
85
  You are coaching:
86
  \n\n{user_interaction_guidelines}\n\n\
 
114
  return message
115
 
116
  @catch_error
117
+ def _run_current_thread(self, text, thread=None, hidden=False, media=None):
118
+ image_context = ""
119
+ if media:
120
+ logger.info(f"Describing Media", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
121
+ # assert media is a base64 encoded string
122
+ try:
123
+ # scale down media to 512x512
124
+ img_data = base64.b64decode(media)
125
+ img = Image.open(io.BytesIO(img_data))
126
+ img = img.resize((512, 512), Image.Resampling.LANCZOS) # Better quality resizing
127
+ buffered = io.BytesIO()
128
+ img.save(buffered, format="JPEG", quality=85) # Specify format and quality
129
+ media = base64.b64encode(buffered.getvalue()).decode()
130
+
131
+ except Exception as e:
132
+ raise ConversationManagerError(user_id=self.user.user_id, message="Invalid base64 image data", e=str(e))
133
+ logger.info(f"Media is valid", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
134
+ response = self.client.chat.completions.create(
135
+ model="gpt-4o",
136
+ messages=[
137
+ {
138
+ "role": "user",
139
+ "content": [
140
+ {
141
+ "type": "text",
142
+ "text": f"""Describe this image to an LLM for it to have rich and complete context.
143
+ {'Lock in on things related to the users message:'+text if text else text}""",
144
+ },
145
+ {
146
+ "type": "image_url",
147
+ "image_url": {"url": f"data:image/jpeg;base64,{media}", "detail": "low"},
148
+ },
149
+ ],
150
+ }
151
+ ])
152
+
153
+ image_context = response.choices[0].message.content
154
+ logger.info(f"Image context: {image_context}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
155
+
156
  if thread is None:
157
  thread = self.current_thread
158
  logger.warning(f"{self}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
159
+
160
+ if image_context:
161
+ text = f"{text}\n\n[MEDIA ATTACHMENT] User attached an image about: {image_context}"
162
 
163
  # need to select assistant
164
  if self.intro_done:
 
168
  logger.info(f"Running intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
169
  run, just_finished_intro, message = self.assistants['intro'].process(thread, text)
170
 
171
+ logger.info(f"Run done, status={run.status} just finished intro: {just_finished_intro}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
172
 
173
  if 'message' in run.metadata:
174
  info = run.metadata['message']
 
187
 
188
  if hidden:
189
  self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
190
+ logger.info(f"Deleted hidden message", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
191
 
192
  return self._get_current_thread_history(remove_system_message=False)[-1], run
193
 
194
  @catch_error
195
  def _send_and_replace_message(self, text, replacement_msg=None):
196
+ logger.info(f"Sending hidden message", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
197
  response, _ = self._run_current_thread(text, hidden=True)
198
 
199
  # check if there is a replacement message
200
  if replacement_msg:
201
+ logger.info(f"Adding replacement message", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
202
  # get the last message
203
  last_msg = list(self.client.beta.threads.messages.list(self.current_thread.id, order="asc"))[-1]
204
+ # logger.info(f"Last message: {last_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
205
  response = last_msg.content[0].text.value
206
 
207
  # delete the last message
 
254
  Be mindful of this information at all times in order to
255
  be as personalised as possible when conversing. Ensure to
256
  follow the conversation guidelines and flow provided.""", "role":"assistant"}] + messages[-29:]
257
+ # logger.info(f"Current Thread Messages: {messages}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
258
 
259
  temp_thread = self.client.beta.threads.create(messages=messages)
260
+ # logger.info(f"Created Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
261
 
262
  if add_to_main:
263
+ logger.info(f"Adding message to main thread", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
264
  self.add_message_to_thread(self.current_thread.id, "assistant", text)
265
 
266
  self.add_message_to_thread(temp_thread.id, "user", text)
 
271
 
272
  # delete temp thread
273
  self.client.beta.threads.delete(temp_thread.id)
274
+ logger.info(f"Deleted Temp Thread", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
275
 
276
  return response
277
 
 
280
  if old_thread is None:
281
  old_thread = self.current_thread
282
 
283
+ # find all hidden messages to delete
284
+ to_delete = [{'id': msg.id,'content': msg.content[0].text.value, 'role': msg.role} for msg in self.client.beta.threads.messages.list(self.current_thread.id, order="desc") if msg.content[0].text.value.startswith("[hidden]")]
285
+ logger.info(f"Marked for deletion= {to_delete}", extra={"user_id": self.user.user_id, "endpoint": "delete_hidden_messaged"})
 
 
 
 
286
 
287
+ for msg in to_delete:
288
+ # delete msg
289
+ deleted_message = self.client.beta.threads.messages.delete(
290
+ message_id=msg['id'],
291
+ thread_id=self.current_thread.id,
292
+ )
293
+ logger.info(f"Deleted Message = {deleted_message}", extra={"user_id": self.user.user_id, "endpoint": "delete_hidden_messaged"})
294
+ return True
295
 
296
  @catch_error
297
  def cancel_run(self, run, thread = None):
app/flows.py CHANGED
@@ -59,7 +59,8 @@ Step 1:
59
  **First Message: Micro-Action Suggestion**
60
 
61
  - Propose one clear, actionable task for the day.
62
- - Ensure it is relevant, easy to begin, and framed positively.
 
63
  - Avoid repeating previous actions.
64
  - Your message must be concise! (use Whatsapp texting length)
65
  - Wait for the user's response
@@ -87,6 +88,7 @@ Step 1:
87
 
88
  - **If the user has decided to try out the micro-action:**
89
  - Encourage them to proceed with the task (Your message must be concise!)
 
90
  - Do **not** ask any questions at this point.
91
  - After the user confirms they've completed the micro-action:
92
  - Acknowledge their effort.
@@ -107,7 +109,7 @@ Step 1:
107
  **Key Rules for Micro-Actions**
108
 
109
  - **Personalized and Achievable:** Align with the user’s progress, keeping early actions (Day 1–5) simple and gradually increasing difficulty (Day 6+).
110
- - **Resource Suggestions:** Avoid recommending resources on Day 1; may do so sparingly on later days if relevant.
111
  - **Guided Coaching:** Provide micro-actions as if you naturally guide the user—never state or imply a function call.
112
 
113
  ---
@@ -117,16 +119,15 @@ Step 1:
117
  - **Warm and Encouraging:** Mirror a friendly, personable texting style.
118
  - **Simple and Succinct:** Use conversational, human-like language in WhatsApp texting length.
119
  - **Action-Oriented:** Focus on immediate, actionable steps rather than abstract suggestions.
120
-
121
  ---
122
 
123
  **Interaction Principles**
124
 
125
  1. **Simplicity:** Actions should feel easy and immediately doable.
126
  2. **Relevance:** Align each action with the user’s goal and progress.
127
- 3. **Encouragement:** Motivate the user without judgment or pressure.
128
- 4. **Feedback:** Reinforce positive habits subtly to build momentum.
129
- 5. **Tone:** Casual, personable, and focused on meaningful progress.
130
 
131
  ---
132
 
 
59
  **First Message: Micro-Action Suggestion**
60
 
61
  - Propose one clear, actionable task for the day.
62
+ - Ensure it is relevant, timely, easy to begin, and framed positively.
63
+ - You can call search_web(...) to get better context or timely recommendations to incorporate into your response.
64
  - Avoid repeating previous actions.
65
  - Your message must be concise! (use Whatsapp texting length)
66
  - Wait for the user's response
 
88
 
89
  - **If the user has decided to try out the micro-action:**
90
  - Encourage them to proceed with the task (Your message must be concise!)
91
+ - Ok coach, if you feel like its appropriate i.e, the quality of the microaction can be enhanced via a picture or the microaction accountability tracking can be enhaced by receiving picture, let the user know that you can track their progress and provide coaching even through picture (but dont always ask for pictures, thats kinda creepy).
92
  - Do **not** ask any questions at this point.
93
  - After the user confirms they've completed the micro-action:
94
  - Acknowledge their effort.
 
109
  **Key Rules for Micro-Actions**
110
 
111
  - **Personalized and Achievable:** Align with the user’s progress, keeping early actions (Day 1–5) simple and gradually increasing difficulty (Day 6+).
112
+ - **Resource Suggestions:** You can call search_web(...) to get better context or timely recommendations.
113
  - **Guided Coaching:** Provide micro-actions as if you naturally guide the user—never state or imply a function call.
114
 
115
  ---
 
119
  - **Warm and Encouraging:** Mirror a friendly, personable texting style.
120
  - **Simple and Succinct:** Use conversational, human-like language in WhatsApp texting length.
121
  - **Action-Oriented:** Focus on immediate, actionable steps rather than abstract suggestions.
122
+ - **Coach Mindset:** You have entered focussed mind state akin to that of elite coaches. While being encouraging and friendly, you should lightly push and challenge the user when appropriate.
123
  ---
124
 
125
  **Interaction Principles**
126
 
127
  1. **Simplicity:** Actions should feel easy and immediately doable.
128
  2. **Relevance:** Align each action with the user’s goal and progress.
129
+ 3. **Feedback:** Reinforce positive habits subtly to build momentum.
130
+ 4. **Tone:** Casual, personable, and focused on meaningful progress.
 
131
 
132
  ---
133
 
app/main.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import FastAPI, HTTPException, Security, Query, status, Request, Depends
2
  from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
3
  from fastapi.security import APIKeyHeader
4
  import openai
@@ -28,6 +28,9 @@ import pickle
28
  from app.exceptions import *
29
  import re
30
  import sentry_sdk
 
 
 
31
 
32
  load_dotenv()
33
  AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY')
@@ -35,18 +38,18 @@ AWS_SECRET_KEY = os.getenv('AWS_SECRET_KEY')
35
  REGION = os.getenv('AWS_REGION')
36
  SENTRY_DSN = os.getenv('SENTRY_DSN')
37
 
38
- sentry_sdk.init(
39
- dsn=SENTRY_DSN,
40
- # Set traces_sample_rate to 1.0 to capture 100%
41
- # of transactions for tracing.
42
- traces_sample_rate=1.0,
43
- _experiments={
44
- # Set continuous_profiling_auto_start to True
45
- # to automatically start the profiler on when
46
- # possible.
47
- "continuous_profiling_auto_start": True,
48
- },
49
- )
50
 
51
 
52
 
@@ -278,6 +281,7 @@ class CreateUserItem(BaseModel):
278
  class ChatItem(BaseModel):
279
  user_id: str
280
  message: str
 
281
 
282
  class PersonaItem(BaseModel):
283
  user_id: str
@@ -668,7 +672,7 @@ async def add_ai_message(
668
  ):
669
  user_id = request.user_id
670
  message = request.message
671
- logger.info(f"Adding AI response: {message}", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
672
  print_log("INFO", "Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
673
 
674
  user = get_user(user_id)
@@ -836,7 +840,9 @@ async def chat(
836
  logger.info("Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
837
  user = get_user(request.user_id)
838
 
839
- response = user.send_message(request.message)
 
 
840
  logger.info(f"Assistant response generated", extra={"user_id": request.user_id, "endpoint": "/chat"})
841
  return {"response": response}
842
 
@@ -1109,3 +1115,42 @@ async def get_recent_booking(
1109
  if 'conn' in locals():
1110
  conn.close()
1111
  return {"booking_id": booking_id}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Security, Query, status, Request, Depends, File, UploadFile, Form
2
  from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
3
  from fastapi.security import APIKeyHeader
4
  import openai
 
28
  from app.exceptions import *
29
  import re
30
  import sentry_sdk
31
+ import base64
32
+ from io import BytesIO
33
+ from PIL import Image
34
 
35
  load_dotenv()
36
  AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY')
 
38
  REGION = os.getenv('AWS_REGION')
39
  SENTRY_DSN = os.getenv('SENTRY_DSN')
40
 
41
+ # sentry_sdk.init(
42
+ # dsn=SENTRY_DSN,
43
+ # # Set traces_sample_rate to 1.0 to capture 100%
44
+ # # of transactions for tracing.
45
+ # traces_sample_rate=1.0,
46
+ # _experiments={
47
+ # # Set continuous_profiling_auto_start to True
48
+ # # to automatically start the profiler on when
49
+ # # possible.
50
+ # "continuous_profiling_auto_start": True,
51
+ # },
52
+ # )
53
 
54
 
55
 
 
281
  class ChatItem(BaseModel):
282
  user_id: str
283
  message: str
284
+ image64: str = None
285
 
286
  class PersonaItem(BaseModel):
287
  user_id: str
 
672
  ):
673
  user_id = request.user_id
674
  message = request.message
675
+ logger.info(f"Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
676
  print_log("INFO", "Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
677
 
678
  user = get_user(user_id)
 
840
  logger.info("Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
841
  user = get_user(request.user_id)
842
 
843
+ if request.image64:
844
+ logger.info(f"Processing image", extra={"user_id": request.user_id, "endpoint": "/chat"})
845
+ response = user.send_message(request.message, request.image64)
846
  logger.info(f"Assistant response generated", extra={"user_id": request.user_id, "endpoint": "/chat"})
847
  return {"response": response}
848
 
 
1115
  if 'conn' in locals():
1116
  conn.close()
1117
  return {"booking_id": booking_id}
1118
+
1119
+ @app.post("/answer_image_question")
1120
+ async def answer_image_question(
1121
+ question: str = Form(...),
1122
+ file: UploadFile = File(None),
1123
+ image_base64: str = Form(None)
1124
+ ):
1125
+ if file:
1126
+ contents = await file.read()
1127
+ # convert to base64 string
1128
+ image_base64 = base64.b64encode(contents).decode('utf-8')
1129
+
1130
+ client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
1131
+ response = client.chat.completions.create(
1132
+ model="gpt-4o",
1133
+ messages=[
1134
+ {
1135
+ "role": "user",
1136
+ "content": [
1137
+ {
1138
+ "type": "text",
1139
+ "text": "What is in this image?",
1140
+ },
1141
+ {
1142
+ "type": "image_url",
1143
+ "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"},
1144
+ },
1145
+ ],
1146
+ }
1147
+ ])
1148
+
1149
+ return {"response": response.choices[0]}
1150
+
1151
+ @app.post("/upload_image")
1152
+ async def upload_image(file: UploadFile = File(...)):
1153
+ # process or save the file
1154
+ contents = await file.read()
1155
+ image = Image.open(BytesIO(contents))
1156
+ return {"filename": file.filename, "info": "Image uploaded successfully"}
app/user.py CHANGED
@@ -6,6 +6,7 @@ import pandas as pd
6
  from datetime import datetime, timezone
7
  import json
8
  from app.assistants import Assistant
 
9
  import glob
10
  import pickle # Replace dill with pickle
11
  import random
@@ -328,9 +329,33 @@ class User:
328
  return False
329
 
330
  @catch_error
331
- def set_mantra(self, mantra):
332
- logger.info(f"Setting mantra: {mantra}", extra={"user_id": self.user_id, "endpoint": "set_mantra"})
333
- self.mantra = mantra
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
  @catch_error
336
  def set_goal(self, goal, goal_area, add=True, completed=False):
@@ -457,8 +482,10 @@ class User:
457
  return self.conversations.current_thread
458
 
459
  @catch_error
460
- def send_message(self, text):
461
- response, run = self.conversations._run_current_thread(text)
 
 
462
  message = run.metadata.get("message", "No message")
463
  logger.info(f"Message: {message}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
464
 
@@ -475,6 +502,8 @@ class User:
475
  # Move to the next action
476
  self.growth_plan.next()
477
 
 
 
478
  elif message == "change_goal":
479
  # send the change goal prompt
480
  logger.info("Sending change goal message...", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
@@ -726,8 +755,8 @@ class User:
726
  return [response]
727
 
728
  @catch_error
729
- def do_theme(self, theme, date, day, last_msg_is_answered = True):
730
- logger.info(f"Doing theme: {theme}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
731
 
732
  # Add 1 day to cumulative_plan_day
733
  self.cumulative_plan_day += 1
@@ -833,20 +862,23 @@ class User:
833
  else:
834
  # Remind the user that they can book a Growth Guide session if they have not done one yet after the FINAL_SUMMARY_STATE
835
  if self.growth_plan.previous()['coachingTheme'] == "FINAL_SUMMARY_STATE":
836
- formatted_message = f"[IMPORTANT] The user has not booked a Growth Guide session yet. Remind them that they can book one through their Revelation Dahsboard: {OURCOACH_DASHBOARD_URL} to get more personalized advice and guidance!\n\n" + formatted_message
 
837
 
838
  prompt = f"""** It is a new day: {date} ({day}) 10:00:00 **
839
 
840
- (If the day is a public holiday (e.g., Christmas, New Year, or other significant occasions), customize your message to reflect the context appropriately, acknowledging the holiday or its significance.)
841
 
842
  **Before we start,**
843
  Has the user answered your last question? : {last_msg_is_answered}
844
  If the answer above is "True", you may proceed to do the instruction below
845
- If the answer above is "False", you must immediately ask something like (but warmer) "Hey <user name>, I've noticed that you haven't answered my latest question. Do you still want to continue the growth plan?". If the user says "no", then ask if they want to set a new goal (therefore later, call the change_goal() function)
846
  But if the user says "yes", then proceed to do the instruction below.
847
 
848
  Today is day {self.cumulative_plan_day} of the user's growth journey (out of {final_day} days). You may (or may not) mention this occasionally in your first message of the day.
849
 
 
 
850
  Today's Theme:
851
  {formatted_message}
852
  """
@@ -869,6 +901,8 @@ class User:
869
  self.conversations.delete_hidden_messages()
870
  # update the date in the state
871
  self.conversations.state['date'] = date
 
 
872
 
873
  action = self.growth_plan.current()
874
 
@@ -918,7 +952,20 @@ class User:
918
  else:
919
  last_msg_is_answered = True
920
 
921
- response, prompt = self.do_theme(theme, date, action['day'], last_msg_is_answered)
 
 
 
 
 
 
 
 
 
 
 
 
 
922
 
923
  # add today's reminders to response to schedule
924
  # response['reminders'] = all reminders which date is today (so all the reminders that BE has to queue today)
 
6
  from datetime import datetime, timezone
7
  import json
8
  from app.assistants import Assistant
9
+ from app.exceptions import DBError
10
  import glob
11
  import pickle # Replace dill with pickle
12
  import random
 
329
  return False
330
 
331
  @catch_error
332
+ def set_mantra(self):
333
+ ### To save mantra in database to user object
334
+ logger.info(f"Getting mantra from user...", extra={"user_id": self.user_id, "endpoint": "get_mantra"})
335
+ user_id = self.user_id
336
+ db_params = {
337
+ 'dbname': 'ourcoach',
338
+ 'user': 'ourcoach',
339
+ 'password': 'hvcTL3kN3pOG5KteT17T',
340
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
341
+ 'port': '5432'
342
+ }
343
+ try:
344
+ with psycopg2.connect(**db_params) as conn:
345
+ with conn.cursor() as cursor:
346
+ query = sql.SQL("SELECT mantra FROM {table} WHERE user_id = %s").format(table=sql.Identifier('public', 'user_growth_status'))
347
+ cursor.execute(query, (user_id,))
348
+ row = cursor.fetchone()
349
+ if (row):
350
+ colnames = [desc[0] for desc in cursor.description]
351
+ user_data = dict(zip(colnames, row))
352
+ ### SAVE MANTRA IN USER OBJECT
353
+ self.mantra = user_data['mantra']
354
+ else:
355
+ logger.warning(f"No user info found for {user_id}", extra={'user_id': user_id, 'endpoint': "get_mantra"})
356
+ except psycopg2.Error as e:
357
+ logger.error(f"Database error while retrieving user info for {user_id}: {e}", extra={'user_id': user_id, 'endpoint': "get_mantra"})
358
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
359
 
360
  @catch_error
361
  def set_goal(self, goal, goal_area, add=True, completed=False):
 
482
  return self.conversations.current_thread
483
 
484
  @catch_error
485
+ def send_message(self, text, media=None):
486
+ if media:
487
+ logger.info(f"Sending message with media", extra={"user_id": self.user_id, "endpoint": "send_message"})
488
+ response, run = self.conversations._run_current_thread(text, media=media)
489
  message = run.metadata.get("message", "No message")
490
  logger.info(f"Message: {message}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
491
 
 
502
  # Move to the next action
503
  self.growth_plan.next()
504
 
505
+ response['add_one'] = True
506
+
507
  elif message == "change_goal":
508
  # send the change goal prompt
509
  logger.info("Sending change goal message...", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
 
755
  return [response]
756
 
757
  @catch_error
758
+ def do_theme(self, theme, date, day, last_msg_is_answered = True, extra=None):
759
+ logger.info(f"Doing theme: {theme}, extra={extra}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
760
 
761
  # Add 1 day to cumulative_plan_day
762
  self.cumulative_plan_day += 1
 
862
  else:
863
  # Remind the user that they can book a Growth Guide session if they have not done one yet after the FINAL_SUMMARY_STATE
864
  if self.growth_plan.previous()['coachingTheme'] == "FINAL_SUMMARY_STATE":
865
+ if day != 1:
866
+ formatted_message = f"[IMPORTANT] The user has not booked a Growth Guide session yet. Remind them that they can book one through their Revelation Dahsboard: {OURCOACH_DASHBOARD_URL} to get more personalized advice and guidance!\n\n" + formatted_message
867
 
868
  prompt = f"""** It is a new day: {date} ({day}) 10:00:00 **
869
 
870
+ (If the day is a public holiday (e.g., Christmas, New Year, the user's Birthday or other significant occasions), customize your message to reflect the context appropriately, acknowledging the holiday or its significance.)
871
 
872
  **Before we start,**
873
  Has the user answered your last question? : {last_msg_is_answered}
874
  If the answer above is "True", you may proceed to do the instruction below
875
+ If the answer above is "False", take a deep breath coach, and utilizing your ability as an elite coach with the users best interest in mind, think whether it would be more appropriate to follow up the unanswered question with the user or continue with todays theme below. However if the user indicates that they want to set a new goal (call the change_goal() function)
876
  But if the user says "yes", then proceed to do the instruction below.
877
 
878
  Today is day {self.cumulative_plan_day} of the user's growth journey (out of {final_day} days). You may (or may not) mention this occasionally in your first message of the day.
879
 
880
+ If today is a "Monday" or "Mon", you must include the user's Mantra of the Week : {self.mantra} to your first message of the day (include with today-theme's first message below)
881
+ {extra if extra else ''}
882
  Today's Theme:
883
  {formatted_message}
884
  """
 
901
  self.conversations.delete_hidden_messages()
902
  # update the date in the state
903
  self.conversations.state['date'] = date
904
+ # update mantra
905
+ self.set_mantra()
906
 
907
  action = self.growth_plan.current()
908
 
 
952
  else:
953
  last_msg_is_answered = True
954
 
955
+ extra="Greet the user with a creative good morning message!"
956
+
957
+ # Check if user didnt respond (last 2 messages in conversation history are from the AI) on FINAL_SUMMARY_STATE
958
+ messages = self.get_messages()
959
+ logger.info(f"Last 2 messages are from: 1) {messages[-1]['role']} and 2) {messages[-2]['role']}")
960
+ if (messages[-1]['role'] == "assistant" and messages[-2]['role'] == "assistant") and self.growth_plan.previous()['coachingTheme'] == "FINAL_SUMMARY_STATE":
961
+ self.extend_growth_plan()
962
+ extra += """\nDang, the user did not indicate what they wanted to do yesterday.
963
+ This is not a good sign as it may indicate that the user is going to dropoff.
964
+ We have automatically extended their growth plan for them, let the user know this and also integrate upselling the ourcoach platform and subscription as much as you can
965
+ in the first message you send to the user.."""
966
+
967
+
968
+ response, prompt = self.do_theme(theme, date, action['day'], last_msg_is_answered, extra=extra)
969
 
970
  # add today's reminders to response to schedule
971
  # response['reminders'] = all reminders which date is today (so all the reminders that BE has to queue today)
app/utils.py CHANGED
@@ -327,7 +327,7 @@ def generate_html(json_data, coach_name='Growth Guide', booking_id = None):
327
  s3_client = session.client('s3')
328
  with open(path_to_upload, "rb") as f:
329
  ## Upload to Production Folder
330
- s3_client.upload_fileobj(f, bucket, f'staging/pre_gg_reports/{filename}.pdf')
331
  logger.info(f"File {filename} uploaded successfully to S3", extra={'booking_id': booking_id, 'endpoint': function_name})
332
 
333
  # Removing files
@@ -984,7 +984,8 @@ def get_user_life_status(user_id):
984
  mantra = json.loads(response.choices[0].message.content)["mantra_of_the_week"]
985
 
986
  # Update the users mantra
987
- user.set_mantra(mantra)
 
988
 
989
  cumulative_life_score = {
990
  "overall": user.personal_growth_score + user.career_growth_score + user.relationship_score + user.mental_well_being_score + user.health_and_wellness_score,
@@ -1304,7 +1305,7 @@ def download_file_from_s3(filename, bucket):
1304
  s3_client = session.client('s3')
1305
  with open(file_path, 'wb') as f:
1306
  ## Upload to Production Folder
1307
- s3_client.download_fileobj(bucket, f"staging/users/{filename}", f)
1308
  logger.info(f"File {filename} downloaded successfully from S3", extra={'user_id': user_id, 'endpoint': function_name})
1309
  return True
1310
  except Exception as e:
 
327
  s3_client = session.client('s3')
328
  with open(path_to_upload, "rb") as f:
329
  ## Upload to Production Folder
330
+ s3_client.upload_fileobj(f, bucket, f'dev/pre_gg_reports/{filename}.pdf')
331
  logger.info(f"File {filename} uploaded successfully to S3", extra={'booking_id': booking_id, 'endpoint': function_name})
332
 
333
  # Removing files
 
984
  mantra = json.loads(response.choices[0].message.content)["mantra_of_the_week"]
985
 
986
  # Update the users mantra
987
+ # user.set_mantra(mantra)
988
+ # We remove because we want the mantra to be updated weekly (by backend), not updated everytime we call this endpoint/func
989
 
990
  cumulative_life_score = {
991
  "overall": user.personal_growth_score + user.career_growth_score + user.relationship_score + user.mental_well_being_score + user.health_and_wellness_score,
 
1305
  s3_client = session.client('s3')
1306
  with open(file_path, 'wb') as f:
1307
  ## Upload to Production Folder
1308
+ s3_client.download_fileobj(bucket, f"dev/users/{filename}", f)
1309
  logger.info(f"File {filename} downloaded successfully from S3", extra={'user_id': user_id, 'endpoint': function_name})
1310
  return True
1311
  except Exception as e:
app/web_search.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from dotenv import load_dotenv
3
+ import os
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ load_dotenv()
9
+
10
+ class SearchEngine:
11
+ BING_API_KEY = os.getenv("BING_API_KEY")
12
+ BING_ENDPOINT = 'https://api.bing.microsoft.com/v7.0'
13
+
14
+ @staticmethod
15
+ def search(feedback_type_name, search_term, user_id):
16
+ logger.info(f"User {user_id}: Initiating search for type '{feedback_type_name}' with term '{search_term}'")
17
+ """
18
+ Public method to perform a search based on the feedback type.
19
+ """
20
+ search_methods = {
21
+ "General": SearchEngine._search_general,
22
+ "Resource Links": SearchEngine._search_relevant_links,
23
+ "Book/Podcast Recommendations": SearchEngine._search_books_or_podcasts,
24
+ "Inspirational Stories or Case Studies": SearchEngine._search_inspirational_stories,
25
+ "Fun Facts": SearchEngine._search_fun_facts,
26
+ "Personalised Recommendations": SearchEngine._search_personalized_recommendations,
27
+ "Videos": SearchEngine._search_videos
28
+ }
29
+
30
+ search_method = search_methods.get(feedback_type_name)
31
+
32
+ if search_method:
33
+ return search_method(search_term)
34
+ else:
35
+ return (feedback_type_name, search_term)
36
+
37
+ @staticmethod
38
+ def _search_relevant_links(search_term):
39
+ logger.debug(f"Searching relevant links for term: {search_term}")
40
+ """
41
+ Uses Bing Web Search API to search for relevant links.
42
+ """
43
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
44
+ params = {'q': search_term, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
45
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
46
+ if response.status_code == 200:
47
+ logger.debug("Received successful response from Bing Web Search API.")
48
+ data = response.json()
49
+ links = []
50
+ if 'webPages' in data and 'value' in data['webPages']:
51
+ for result in data['webPages']['value']:
52
+ links.append(result)
53
+ return links
54
+ else:
55
+ logger.error(f"Bing Web Search API returned status code {response.status_code}")
56
+ return ["No relevant links found."]
57
+
58
+ @staticmethod
59
+ def _search_books_or_podcasts(search_term):
60
+ logger.debug(f"Searching books or podcasts for term: {search_term}")
61
+ """
62
+ Uses Bing Web Search API to search for books or podcasts.
63
+ """
64
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
65
+ query = f"{search_term} book OR podcast"
66
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
67
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
68
+ if response.status_code == 200:
69
+ logger.debug("Received successful response from Bing Web Search API for books/podcasts.")
70
+ data = response.json()
71
+ recommendations = []
72
+ if 'webPages' in data and 'value' in data['webPages']:
73
+ for result in data['webPages']['value']:
74
+ title = result.get('name', 'Unknown Title')
75
+ url = result.get('url', '')
76
+ recommendations.append(f"{title}: {url}")
77
+ return recommendations
78
+ else:
79
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for books/podcasts search.")
80
+ return ["No book or podcast recommendations found."]
81
+
82
+ @staticmethod
83
+ def _search_success_stories(search_term):
84
+ logger.debug(f"Searching success stories for term: {search_term}")
85
+ """
86
+ Uses Bing Web Search API to search for success stories.
87
+ """
88
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
89
+ query = f"{search_term} success stories"
90
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
91
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
92
+ if response.status_code == 200:
93
+ logger.debug("Received successful response from Bing Web Search API for success stories.")
94
+ data = response.json()
95
+ stories = []
96
+ if 'webPages' in data and 'value' in data['webPages']:
97
+ for result in data['webPages']['value']:
98
+ title = result.get('name', 'Unknown Title')
99
+ url = result.get('url', '')
100
+ stories.append(f"{title}: {url}")
101
+ return stories
102
+ else:
103
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for success stories search.")
104
+ return ["No success stories found."]
105
+
106
+ @staticmethod
107
+ def _search_inspirational_stories(search_term):
108
+ logger.debug(f"Searching inspirational stories or case studies for term: {search_term}")
109
+ """
110
+ Uses Bing Web Search API to search for inspirational stories or case studies.
111
+ """
112
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
113
+ query = f"{search_term} inspirational stories OR case studies"
114
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
115
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
116
+ if response.status_code == 200:
117
+ logger.debug("Received successful response from Bing Web Search API for inspirational stories.")
118
+ data = response.json()
119
+ stories = []
120
+ if 'webPages' in data and 'value' in data['webPages']:
121
+ for result in data['webPages']['value']:
122
+ title = result.get('name', 'Unknown Title')
123
+ url = result.get('url', '')
124
+ stories.append(f"{title}: {url}")
125
+ return stories
126
+ else:
127
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for inspirational stories search.")
128
+ return ["No inspirational stories found."]
129
+
130
+ @staticmethod
131
+ def _search_fun_facts(search_term):
132
+ logger.debug(f"Searching fun facts for term: {search_term}")
133
+ """
134
+ Uses Bing Web Search API to search for fun facts related to personal growth.
135
+ """
136
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
137
+ query = f"{search_term} fun facts"
138
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
139
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
140
+ if response.status_code == 200:
141
+ logger.debug("Received successful response from Bing Web Search API for fun facts.")
142
+ data = response.json()
143
+ facts = []
144
+ if 'webPages' in data and 'value' in data['webPages']:
145
+ for result in data['webPages']['value']:
146
+ snippet = result.get('snippet', '')
147
+ facts.append(snippet)
148
+ return facts
149
+ else:
150
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for fun facts search.")
151
+ return ["No fun facts found."]
152
+
153
+ @staticmethod
154
+ def _search_visual_content(search_term):
155
+ logger.debug(f"Searching visual content for term: {search_term}")
156
+ """
157
+ Uses Bing Image Search API to search for images or infographics.
158
+ """
159
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
160
+ params = {'q': search_term, 'count': 3}
161
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/images/search", headers=headers, params=params)
162
+ if response.status_code == 200:
163
+ logger.debug("Received successful response from Bing Image Search API.")
164
+ data = response.json()
165
+ images = []
166
+ if 'value' in data:
167
+ for result in data['value']:
168
+ image_url = result.get('contentUrl', '')
169
+ images.append(image_url)
170
+ return images
171
+ else:
172
+ logger.error(f"Bing Image Search API returned status code {response.status_code} for visual content search.")
173
+ return ["No visual content found."]
174
+
175
+ @staticmethod
176
+ def _search_personalized_recommendations(search_term):
177
+ logger.debug(f"Searching personalized recommendations for term: {search_term}")
178
+ """
179
+ Uses Bing Web Search API to provide personalized recommendations.
180
+ """
181
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
182
+ query = f"tips for {search_term}"
183
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
184
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
185
+ if response.status_code == 200:
186
+ logger.debug("Received successful response from Bing Web Search API for personalized recommendations.")
187
+ data = response.json()
188
+ recommendations = []
189
+ if 'webPages' in data and 'value' in data['webPages']:
190
+ for result in data['webPages']['value']:
191
+ title = result.get('name', 'Unknown Title')
192
+ url = result.get('url', '')
193
+ recommendations.append(f"{title}: {url}")
194
+ return recommendations
195
+ else:
196
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for personalized recommendations search.")
197
+ return ["No personalized recommendations found."]
198
+
199
+ @staticmethod
200
+ def _search_videos(search_term):
201
+ logger.debug(f"Searching videos for term: {search_term}")
202
+ """
203
+ Uses Bing Video Search API to search for videos, prioritizing YouTube results.
204
+ """
205
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
206
+ query = f"site:youtube.com {search_term}"
207
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
208
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/videos/search", headers=headers, params=params)
209
+ if response.status_code == 200:
210
+ logger.debug("Received successful response from Bing Video Search API.")
211
+ data = response.json()
212
+ videos = []
213
+ if 'value' in data:
214
+ for result in data['value']:
215
+ title = result.get('name', 'Unknown Title')
216
+ url = result.get('contentUrl', '')
217
+ # Prioritize YouTube results
218
+ if 'youtube.com' in url.lower():
219
+ videos.append(f"{title}: {url}")
220
+ if len(videos) >= 3:
221
+ break
222
+ # If we don't have enough YouTube results, add other video results
223
+ if len(videos) < 3:
224
+ for result in data['value']:
225
+ title = result.get('name', 'Unknown Title')
226
+ url = result.get('contentUrl', '')
227
+ if url not in [v.split(': ')[1] for v in videos]:
228
+ videos.append(f"{title}: {url}")
229
+ if len(videos) >= 3:
230
+ break
231
+ return videos
232
+ else:
233
+ logger.error(f"Bing Video Search API returned status code {response.status_code} for video search.")
234
+ return ["No video content found."]
235
+
236
+ @staticmethod
237
+ def _search_general(search_term):
238
+ logger.debug(f"Performing a general search for term: {search_term}")
239
+ """
240
+ Uses Bing Web Search API to perform a general search.
241
+ """
242
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
243
+ params = {'q': search_term, 'textDecorations': True, 'textFormat': 'HTML', 'count': 5}
244
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
245
+ if response.status_code == 200:
246
+ logger.debug("Received successful response from Bing Web Search API for general search.")
247
+ data = response.json()
248
+ results = []
249
+ if 'webPages' in data and 'value' in data['webPages']:
250
+ for result in data['webPages']['value']:
251
+ results.append(result)
252
+ return results
253
+ else:
254
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for general search.")
255
+ return ["No results found for general search."]