Sentry + New User Context

#9
by BMCVRN - opened
Dockerfile CHANGED
@@ -28,11 +28,13 @@ EXPOSE 7860
28
  ARG FASTAPI_KEY
29
  ARG OPENAI_API_KEY
30
  ARG OPENAI_GENERAL_ASSISTANT
 
31
 
32
  ENV FASTAPI_KEY=$FASTAPI_KEY
33
  ENV OPENAI_API_KEY=$OPENAI_API_KEY
34
  ENV OPENAI_GENERAL_ASSISTANT=$OPENAI_GENERAL_ASSISTANT
35
  ENV PYTHONUNBUFFERED=1
 
36
 
37
  # Set the working directory
38
  # WORKDIR /code
 
28
  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
app/__pycache__/assistants.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/assistants.cpython-312.pyc and b/app/__pycache__/assistants.cpython-312.pyc differ
 
app/__pycache__/flows.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/flows.cpython-312.pyc and b/app/__pycache__/flows.cpython-312.pyc differ
 
app/__pycache__/main.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ
 
app/__pycache__/user.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/user.cpython-312.pyc and b/app/__pycache__/user.cpython-312.pyc differ
 
app/__pycache__/utils.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/utils.cpython-312.pyc and b/app/__pycache__/utils.cpython-312.pyc differ
 
app/assistants.py CHANGED
@@ -13,7 +13,8 @@ import psycopg2
13
  from psycopg2 import sql
14
  import pytz
15
 
16
- from app.utils import get_growth_guide_summary, get_users_mementos, print_log
 
17
  from app.flows import FOLLOW_UP_STATE, GENERAL_COACHING_STATE, MICRO_ACTION_STATE, REFLECTION_STATE
18
 
19
  load_dotenv()
@@ -304,13 +305,42 @@ def get_current_datetime(user_id):
304
  return datetime.now().astimezone(pytz.timezone(user_timezone))
305
 
306
  class Assistant:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  def __init__(self, id, cm):
308
  self.id = id
309
  self.cm = cm
310
  self.recent_run = None
311
 
 
312
  def cancel_run(self, run, thread):
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  try:
 
314
  if run.status != 'completed':
315
  cancel = self.cm.client.beta.threads.runs.cancel(thread_id=thread.id, run_id=run.id)
316
  while cancel.status != 'cancelled':
@@ -319,7 +349,7 @@ class Assistant:
319
  thread_id=thread.id,
320
  run_id=cancel.id
321
  )
322
- logger.info(f"Cancelled run: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
323
  return True
324
  else:
325
  logger.info(f"Run already completed: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
@@ -328,14 +358,13 @@ class Assistant:
328
  # check if run has expired. run has a field 'expires_at' like run.expires_at = 1735008568
329
  # if expired, return True and log run already expired
330
  if run.expires_at < get_current_datetime().timestamp():
331
- logger.error(f"Run already expired: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
332
  return True
333
  else:
334
- logger.error(f"Error cancelling run: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
335
  return False
336
 
337
-
338
-
339
  def process(self, thread, text):
340
  # template_search = self.cm.add_message_to_thread(thread.id, "assistant", f"Pay attention to the current state you are in and the flow template to respond to the users query:")
341
  message = self.cm.add_message_to_thread(thread.id, "user", text)
@@ -348,24 +377,39 @@ class Assistant:
348
  just_finished_intro = False
349
  try:
350
  if run.status == 'completed':
351
- logger.info(f"Run Completed: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
 
352
  return run, just_finished_intro, message
353
 
354
- reccursion = 0
355
- logger.info(f"[Run Pending] Status: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
356
- while run.status == 'requires_action':
357
- logger.info(f"Run Calling tool [{reccursion}]: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
358
- run, just_finished_intro = self.call_tool(run, thread)
359
- reccursion += 1
360
- if run == "cancelled" or run == "change_goal":
361
- break
362
-
363
- if run in ['cancelled', 'change_goal'] or run.status != 'completed':
364
- logger.error(f"RUN NOT COMPLETED: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
365
- finally:
366
- self.recent_run = run
367
- return run, just_finished_intro, message
368
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  def call_tool(self, run, thread):
370
  tool_outputs = []
371
  logger.info(f"Required actions: {list(map(lambda x: f'{x.function.name}({x.function.arguments})', run.required_action.submit_tool_outputs.tool_calls))}",
@@ -566,9 +610,8 @@ class Assistant:
566
 
567
  # cancel current run
568
  run = self.cancel_run(run, thread)
569
- logger.info(f"Successfully cancelled run",
570
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_start_now"})
571
- return "cancelled", just_finish_intro
572
  elif tool.function.name == "change_goal":
573
  logger.info(f"Changing user goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_change_goal"})
574
 
@@ -577,9 +620,8 @@ class Assistant:
577
 
578
  # cancel current run
579
  run = self.cancel_run(run, thread)
580
- logger.info(f"Successfully cancelled run",
581
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_change_goal"})
582
- return "change_goal", just_finish_intro
583
  elif tool.function.name == "complete_goal":
584
  logger.info(f"Completing user goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_complete_goal"})
585
  goal = self.cm.user.update_goal(None, 'COMPLETED')
@@ -618,6 +660,7 @@ class Assistant:
618
  # "growth_guide_session",
619
  # "life_score",
620
  # "recent_wins",
 
621
  # ]
622
  logger.info(f"Getting user information: {category}",
623
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
@@ -625,27 +668,29 @@ class Assistant:
625
  if category == "personal":
626
  user_info += f"** Personal Information **\n\n{self.cm.user.user_info}"
627
  elif category == "challenges":
628
- user_info += f"** 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."
629
  elif category == "recommended_actions":
630
- user_info += f"** Recommended Actions (upcoming microactions, recommended by growth guide) **\n\n{self.cm.user.recommended_micro_actions}\n\nLet the user know that these microactions from their growth guide will be integrated into their day-to-day interaction."
631
  elif category == "micro_actions":
632
- user_info += f"** Micro Actions (already introduced microactions) **\n\n{self.cm.user.micro_actions}"
633
  elif category == "other_focusses":
634
- user_info += f"** Other Focusses (other areas of focus) **\n\n{self.cm.user.other_focusses}\n\nLet the user know that other areas of focus from their growth guide will be integrated into their day-to-day interaction."
635
  elif category == "reminders":
636
- user_info += f"** Reminders **\n\n{self.cm.user.reminders}"
637
  elif category == "goal":
638
- user_info += f"** Goal (prioritise the latest [last item in the array] goal) **\n\n{self.cm.user.goal}"
639
  elif category == "growth_guide_session":
640
  if self.cm.user.last_gg_session is not None:
641
  user_info += f"** GG Session **\n\n{get_growth_guide_summary(self.cm.user.user_id, self.cm.user.last_gg_session)}"
642
  else:
643
- user_info += f"** GG Session **\n\nNo GG yet. Let the user know they can book one now through their Revelation Dashboard: {OURCOACH_DASHBOARD_URL}!"
 
644
  elif category == "life_score":
645
- user_info += f"** Life scores for each area **\n\n Personal Growth: {self.cm.user.personal_growth_score} || Career: {self.cm.user.career_growth_score} || Health/Wellness: {self.cm.user.health_and_wellness_score} || Relationships: {self.cm.user.relationship_score} || Mental Health: {self.cm.user.mental_well_being_score}"
646
  elif category == "recent_wins":
647
- user_info += f"** Recent Wins / Achievements **\n\n {self.cm.user.recent_wins}"
648
-
 
649
  logger.info(f"Finish Getting user information: {user_info}",
650
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
651
  tool_outputs.append({
@@ -670,17 +715,20 @@ class Assistant:
670
  )
671
  logger.info("Tool outputs submitted successfully",
672
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
673
- except Exception as e:
674
- logger.error(f"Failed to submit tool outputs: {str(e)}",
675
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
676
- finally:
677
  return run, just_finish_intro
 
 
678
  else:
679
  logger.warning("No tool outputs to submit",
680
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
681
- return {"status": "No tool outputs to submit"}
682
-
683
-
 
 
 
 
 
684
 
685
 
686
  class GeneralAssistant(Assistant):
 
13
  from psycopg2 import sql
14
  import pytz
15
 
16
+ from app.exceptions import AssistantError, BaseOurcoachException, OpenAIRequestError
17
+ from app.utils import 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()
 
305
  return datetime.now().astimezone(pytz.timezone(user_timezone))
306
 
307
  class Assistant:
308
+ def catch_error(func):
309
+ def wrapper(self, *args, **kwargs):
310
+ try:
311
+ return func(self, *args, **kwargs)
312
+ except (BaseOurcoachException) as e:
313
+ raise e
314
+ except openai.BadRequestError as e:
315
+ raise OpenAIRequestError(user_id=self.cm.user.user_id, message="Error getting response from OpenAI", e=str(e), run_id=self.recent_run.id)
316
+ except Exception as e:
317
+ # Handle other exceptions
318
+ logger.error(f"An unexpected error occurred in Assistant: {e}")
319
+ raise AssistantError(user_id=self.cm.user.user_id, message="Unexpected error in Assistant", e=str(e))
320
+ return wrapper
321
+
322
  def __init__(self, id, cm):
323
  self.id = id
324
  self.cm = cm
325
  self.recent_run = None
326
 
327
+ @catch_error
328
  def cancel_run(self, run, thread):
329
+ logger.info(f"(asst) Attempting to cancel run: {run} for thread: {thread}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
330
+ if isinstance(run, str):
331
+ try:
332
+ run = self.cm.client.beta.threads.runs.retrieve(
333
+ thread_id=thread,
334
+ run_id=run
335
+ )
336
+ thread = self.cm.client.beta.threads.retrieve(thread_id=thread)
337
+ except openai.NotFoundError:
338
+ logger.warning(f"Thread {thread} already deleted: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
339
+ return True
340
+ if isinstance(run, PseudoRun):
341
+ return True
342
  try:
343
+ logger.info(f"Attempting to cancel run: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
344
  if run.status != 'completed':
345
  cancel = self.cm.client.beta.threads.runs.cancel(thread_id=thread.id, run_id=run.id)
346
  while cancel.status != 'cancelled':
 
349
  thread_id=thread.id,
350
  run_id=cancel.id
351
  )
352
+ logger.info(f"Succesfully cancelled run: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
353
  return True
354
  else:
355
  logger.info(f"Run already completed: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
 
358
  # check if run has expired. run has a field 'expires_at' like run.expires_at = 1735008568
359
  # if expired, return True and log run already expired
360
  if run.expires_at < get_current_datetime().timestamp():
361
+ logger.warning(f"Run already expired: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
362
  return True
363
  else:
364
+ logger.warning(f"Error cancelling run: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
365
  return False
366
 
367
+ @catch_error
 
368
  def process(self, thread, text):
369
  # template_search = self.cm.add_message_to_thread(thread.id, "assistant", f"Pay attention to the current state you are in and the flow template to respond to the users query:")
370
  message = self.cm.add_message_to_thread(thread.id, "user", text)
 
377
  just_finished_intro = False
378
  try:
379
  if run.status == 'completed':
380
+ logger.info(f"Run Completed: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
381
+ self.recent_run = run
382
  return run, just_finished_intro, message
383
 
384
+ elif run.status == 'failed':
385
+ raise OpenAIRequestError(user_id=self.cm.user.id, message="Run failed", run_id=run.id)
386
+
387
+ elif run.status == 'requires_action':
388
+ reccursion = 0
389
+ logger.info(f"[Run Pending] Status: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
390
+ while run.status == 'requires_action':
391
+ logger.info(f"Run Calling tool [{reccursion}]: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
392
+ run, just_finished_intro = self.call_tool(run, thread)
393
+ reccursion += 1
394
+ if reccursion > 10:
395
+ logger.warning(f"Run has exceeded maximum recussrion depth({10}) for function_call: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
396
+ self.cancel_run(run, thread)
397
+ raise OpenAIRequestError(user_id=self.cm.id, message="Tool Call Reccursion Depth Reached")
398
+ if run.status == 'cancelled':
399
+ logger.warning(f"RUN NOT COMPLETED: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
400
+ self.cancel_run(run, thread)
401
+ break
402
+ elif run.status == 'completed':
403
+ logger.info(f"Run Completed: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
404
+ self.recent_run = run
405
+ return run, just_finished_intro, message
406
+ elif run.status == 'failed':
407
+ raise OpenAIRequestError(user_id=self.cm.id, message="Run failed")
408
+ return run, just_finished_intro, message
409
+ except openai.BadRequestError as e:
410
+ raise OpenAIRequestError(user_id=self.cm.user, message="Error getting response from OpenAI", e=str(e))
411
+
412
+ @catch_error
413
  def call_tool(self, run, thread):
414
  tool_outputs = []
415
  logger.info(f"Required actions: {list(map(lambda x: f'{x.function.name}({x.function.arguments})', run.required_action.submit_tool_outputs.tool_calls))}",
 
610
 
611
  # cancel current run
612
  run = self.cancel_run(run, thread)
613
+ run = PseudoRun(status="cancelled", metadata={"message": "start_now"})
614
+ return run, just_finish_intro
 
615
  elif tool.function.name == "change_goal":
616
  logger.info(f"Changing user goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_change_goal"})
617
 
 
620
 
621
  # cancel current run
622
  run = self.cancel_run(run, thread)
623
+ run = PseudoRun(status="cancelled", metadata={"message": "change_goal"})
624
+ return run, just_finish_intro
 
625
  elif tool.function.name == "complete_goal":
626
  logger.info(f"Completing user goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_complete_goal"})
627
  goal = self.cm.user.update_goal(None, 'COMPLETED')
 
660
  # "growth_guide_session",
661
  # "life_score",
662
  # "recent_wins",
663
+ # "subscription/payments"
664
  # ]
665
  logger.info(f"Getting user information: {category}",
666
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
 
668
  if category == "personal":
669
  user_info += f"** Personal Information **\n\n{self.cm.user.user_info}"
670
  elif category == "challenges":
671
+ 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."
672
  elif category == "recommended_actions":
673
+ user_info += f"** User's Recommended Actions (upcoming microactions, recommended by growth guide) **\n\n{self.cm.user.recommended_micro_actions}\n\nLet the user know that these microactions from their growth guide will be integrated into their day-to-day interaction."
674
  elif category == "micro_actions":
675
+ user_info += f"** User's Micro Actions (already introduced microactions) **\n\n{self.cm.user.micro_actions}"
676
  elif category == "other_focusses":
677
+ user_info += f"** User's Other Focusses (other areas of focus) **\n\n{self.cm.user.other_focusses}\n\nLet the user know that other areas of focus from their growth guide will be integrated into their day-to-day interaction."
678
  elif category == "reminders":
679
+ user_info += f"** User's Reminders **\n\n{self.cm.user.reminders}"
680
  elif category == "goal":
681
+ user_info += f"** User's Goal (prioritise the latest [last item in the array] goal) **\n\n{self.cm.user.goal}"
682
  elif category == "growth_guide_session":
683
  if self.cm.user.last_gg_session is not None:
684
  user_info += f"** GG Session **\n\n{get_growth_guide_summary(self.cm.user.user_id, self.cm.user.last_gg_session)}"
685
  else:
686
+ user_info += f"** GG Session **\n\nNo GG yet. Let the user know they can book one now through their Revelation Dashboard: {OURCOACH_DASHBOARD_URL}! "
687
+ user_info += f"** Suggested Growth Guide Topics **\n\n{self.cm.user.recommended_gg_topics}\nOnly suggest 1-2 topics and let the user know they can can find more suggestions on their dashboard"
688
  elif category == "life_score":
689
+ user_info += f"** User's Life scores for each area **\n\n Personal Growth: {self.cm.user.personal_growth_score} || Career: {self.cm.user.career_growth_score} || Health/Wellness: {self.cm.user.health_and_wellness_score} || Relationships: {self.cm.user.relationship_score} || Mental Health: {self.cm.user.mental_well_being_score}"
690
  elif category == "recent_wins":
691
+ user_info += f"** User's Recent Wins / Achievements **\n\n {self.cm.user.recent_wins}"
692
+ elif category == "subscription_info":
693
+ user_info += f"** User's Subscription Information **\n\n This is a sorted list (most recent first) of the users subscription history:\n{get_user_subscriptions(self.cm.user.user_id)}\nNote that the stripe_status is one of ['trialing', 'cancelled', 'active'] where trialing and active indicate that the user is a 'Premium' user. Remind them of their benefits. Alternatively, if the user is not Premium (status='cancelled'), persuade and motivate them to subscribe to unlock more features."
694
  logger.info(f"Finish Getting user information: {user_info}",
695
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
696
  tool_outputs.append({
 
715
  )
716
  logger.info("Tool outputs submitted successfully",
717
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
 
 
 
 
718
  return run, just_finish_intro
719
+ except Exception as e:
720
+ raise OpenAIRequestError(user_id=self.cm.user.id, message="Error submitting tool outputs", e=str(e), run_id=run.id)
721
  else:
722
  logger.warning("No tool outputs to submit",
723
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
724
+ run = PseudoRun(status="completed", metadata={"message": "No tool outputs to submit"})
725
+ return run, just_finish_intro
726
+
727
+ class PseudoRun:
728
+ def __init__(self, status, metadata=None):
729
+ self.id = "pseudo_run"
730
+ self.status = status
731
+ self.metadata = metadata or {}
732
 
733
 
734
  class GeneralAssistant(Assistant):
app/cache.py CHANGED
@@ -8,6 +8,8 @@ import re
8
 
9
  from dotenv import load_dotenv
10
 
 
 
11
  logger = logging.getLogger(__name__)
12
 
13
  load_dotenv()
@@ -41,7 +43,7 @@ def upload_file_to_s3(filename):
41
  return True
42
  except (FileNotFoundError, NoCredentialsError, PartialCredentialsError) as e:
43
  logger.error(f"S3 upload failed for {filename}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
44
- return False
45
 
46
  class CustomTTLCache:
47
  def __init__(self, ttl=60, cleanup_interval=10):
 
8
 
9
  from dotenv import load_dotenv
10
 
11
+ from app.exceptions import DBError
12
+
13
  logger = logging.getLogger(__name__)
14
 
15
  load_dotenv()
 
43
  return True
44
  except (FileNotFoundError, NoCredentialsError, PartialCredentialsError) as e:
45
  logger.error(f"S3 upload failed for {filename}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
46
+ raise DBError(user_id, "S3Error", f"Failed to upload file {filename} to S3", e)
47
 
48
  class CustomTTLCache:
49
  def __init__(self, ttl=60, cleanup_interval=10):
app/conversation_manager.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import openai
3
+ import pandas as pd
4
+ from datetime import datetime, timezone
5
+ from app.assistants import Assistant
6
+ import random
7
+ import logging
8
+ from app.exceptions import BaseOurcoachException, ConversationManagerError, OpenAIRequestError
9
+ from datetime import datetime
10
+
11
+ import dotenv
12
+ dotenv.load_dotenv()
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ def get_current_datetime():
17
+ return datetime.now(timezone.utc)
18
+
19
+ class ConversationManager:
20
+ def __init__(self, client, user, asst_id, intro_done=False):
21
+ self.user = user
22
+ self.intro_done = intro_done
23
+ self.assistants = {'general': Assistant('asst_vnucWWELJlCWadfAARwyKkCW', self), 'intro': Assistant('asst_baczEK65KKvPWIUONSzdYH8j', self)}
24
+
25
+ self.client = client
26
+ self.state = {'date': pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S")}
27
+
28
+ self.current_thread = self.create_thread()
29
+ self.daily_thread = None
30
+
31
+ logger.info("Initializing conversation state", extra={"user_id": self.user.user_id, "endpoint": "conversation_init"})
32
+
33
+ def __getstate__(self):
34
+ state = self.__dict__.copy()
35
+ # Remove unpicklable or unnecessary attributes
36
+ if 'client' in state:
37
+ del state['client']
38
+ return state
39
+
40
+ def __setstate__(self, state):
41
+ self.__dict__.update(state)
42
+ # Re-initialize attributes that were not pickled
43
+ self.client = None
44
+
45
+ def catch_error(func):
46
+ def wrapper(self, *args, **kwargs):
47
+ try:
48
+ return func(self, *args, **kwargs)
49
+ except BaseOurcoachException as e:
50
+ raise e
51
+ except openai.BadRequestError as e:
52
+ raise OpenAIRequestError(user_id=self.user.user_id, message="OpenAI Request Error", e=str(e))
53
+ except Exception as e:
54
+ # Handle other exceptions
55
+ logger.error(f"An unexpected error occurred: {e}")
56
+ raise ConversationManagerError(user_id=self.user.user_id, message="Unexpected error in ConversationManager", e=str(e))
57
+ return wrapper
58
+
59
+ @catch_error
60
+ def create_thread(self):
61
+ user_interaction_guidelines =self.user.user_interaction_guidelines
62
+ thread = self.client.beta.threads.create()
63
+ self.system_message = self.add_message_to_thread(thread.id, "assistant",
64
+ f"""
65
+ You are coaching:
66
+ \n\n{user_interaction_guidelines}\n\n\
67
+ Be mindful of this information at all times in order to
68
+ be as personalised as possible when conversing. Ensure to
69
+ follow the conversation guidelines and flow templates. Use the
70
+ current state of the conversation to adhere to the flow. Do not let the user know about any transitions.\n\n
71
+ ** Today is {self.state['date']}.\n\n **
72
+ ** You are now in the INTRODUCTION STATE. **
73
+ """)
74
+ return thread
75
+
76
+ @catch_error
77
+ def _get_current_thread_history(self, remove_system_message=True, _msg=None, thread=None):
78
+ if thread is None:
79
+ thread = self.current_thread
80
+ if not remove_system_message:
81
+ return [{"role": msg.role, "content": msg.content[0].text.value} for msg in self.client.beta.threads.messages.list(thread.id, order="asc")]
82
+ if _msg:
83
+ return [{"role": msg.role, "content": msg.content[0].text.value} for msg in self.client.beta.threads.messages.list(thread.id, order="asc", after=_msg.id)][1:]
84
+ return [{"role": msg.role, "content": msg.content[0].text.value} for msg in self.client.beta.threads.messages.list(thread.id, order="asc")][1:] # remove the system message
85
+
86
+ @catch_error
87
+ def add_message_to_thread(self, thread_id, role, content):
88
+ message = self.client.beta.threads.messages.create(
89
+ thread_id=thread_id,
90
+ role=role,
91
+ content=content
92
+ )
93
+ return message
94
+
95
+ @catch_error
96
+ def _run_current_thread(self, text, thread=None, hidden=False):
97
+ if thread is None:
98
+ thread = self.current_thread
99
+ logger.warning(f"{self}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
100
+ logger.info(f"User Message: {text}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
101
+
102
+ # need to select assistant
103
+ if self.intro_done:
104
+ logger.info(f"Running general assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
105
+ run, just_finished_intro, message = self.assistants['general'].process(thread, text)
106
+ else:
107
+ logger.info(f"Running intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
108
+ run, just_finished_intro, message = self.assistants['intro'].process(thread, text)
109
+
110
+ 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"})
111
+
112
+ if 'message' in run.metadata:
113
+ message = run.metadata['message']
114
+
115
+ if message == 'start_now':
116
+ self.intro_done = True
117
+ logger.info(f"Start now", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
118
+ elif message == 'change_goal':
119
+ self.intro_done = False
120
+ logger.info(f"Changing goal, reset to intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
121
+
122
+
123
+ if hidden:
124
+ self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
125
+ logger.info(f"Deleted hidden message: {message}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
126
+
127
+ return self._get_current_thread_history(remove_system_message=False)[-1], run
128
+
129
+ @catch_error
130
+ def _send_and_replace_message(self, text, replacement_msg=None):
131
+ logger.info(f"Sending hidden message: {text}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
132
+ response, _ = self._run_current_thread(text, hidden=True)
133
+
134
+ # check if there is a replacement message
135
+ if replacement_msg:
136
+ logger.info(f"Adding replacement message: {replacement_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
137
+ # get the last message
138
+ last_msg = list(self.client.beta.threads.messages.list(self.current_thread.id, order="asc"))[-1]
139
+ logger.info(f"Last message: {last_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
140
+ response = last_msg.content[0].text.value
141
+
142
+ # delete the last message
143
+ self.client.beta.threads.messages.delete(message_id=last_msg.id, thread_id=self.current_thread.id)
144
+ self.add_message_to_thread(self.current_thread.id, "user", replacement_msg)
145
+ self.add_message_to_thread(self.current_thread.id, "assistant", response)
146
+
147
+ logger.info(f"Hidden message response: {response}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
148
+ # NOTE: this is a hack, should get the response straight from the run
149
+ return {'content': response, 'role': 'assistant'}
150
+
151
+ @catch_error
152
+ def _add_ai_message(self, text):
153
+ return self.add_message_to_thread(self.current_thread.id, "assistant", text)
154
+
155
+ @catch_error
156
+ def get_daily_thread(self):
157
+ if self.daily_thread is None:
158
+ messages = self._get_current_thread_history(remove_system_message=False)
159
+
160
+ self.daily_thread = self.client.beta.threads.create(
161
+ messages=messages[:30]
162
+ )
163
+
164
+ # Add remaining messages one by one if there are more than 30
165
+ for msg in messages[30:]:
166
+ self.add_message_to_thread(
167
+ self.daily_thread.id,
168
+ msg['role'],
169
+ msg['content']
170
+ )
171
+ self.last_daily_message = list(self.client.beta.threads.messages.list(self.daily_thread.id, order="asc"))[-1]
172
+ else:
173
+ messages = self._get_current_thread_history(remove_system_message=False, _msg=self.last_daily_message)
174
+ self.client.beta.threads.delete(self.daily_thread.id)
175
+ self.daily_thread = self.client.beta.threads.create(messages=messages)
176
+ self.last_daily_message = list(self.client.beta.threads.messages.list(self.daily_thread.id, order="asc"))[-1]
177
+ logger.info(f"Daily Thread: {self._get_current_thread_history(thread=self.daily_thread)}", extra={"user_id": self.user.user_id, "endpoint": "send_morning_message"})
178
+ logger.info(f"Last Daily Message: {self.last_daily_message}", extra={"user_id": self.user.user_id, "endpoint": "send_morning_message"})
179
+ return self._get_current_thread_history(thread=self.daily_thread)
180
+ # [{"role":, "content":}, ....]
181
+
182
+ @catch_error
183
+ def _send_morning_message(self, text, add_to_main=False):
184
+ # create a new thread
185
+ # OPENAI LIMITATION: Can only attach a maximum of 32 messages when creating a new thread
186
+ messages = self._get_current_thread_history(remove_system_message=False)
187
+ if len(messages) >= 29:
188
+ messages = [{"content": """ Remember who you are coaching.
189
+ Be mindful of this information at all times in order to
190
+ be as personalised as possible when conversing. Ensure to
191
+ follow the conversation guidelines and flow provided.""", "role":"assistant"}] + messages[-29:]
192
+ logger.info(f"Current Thread Messages: {messages}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
193
+
194
+ temp_thread = self.client.beta.threads.create(messages=messages)
195
+ logger.info(f"Created Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
196
+
197
+ if add_to_main:
198
+ logger.info(f"Adding message to main thread: {text}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
199
+ self.add_message_to_thread(self.current_thread.id, "assistant", text)
200
+
201
+ self.add_message_to_thread(temp_thread.id, "user", text)
202
+
203
+ self._run_current_thread(text, thread=temp_thread)
204
+ response = self._get_current_thread_history(thread=temp_thread)[-1]
205
+ logger.info(f"Hidden Response: {response}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
206
+
207
+ # delete temp thread
208
+ self.client.beta.threads.delete(temp_thread.id)
209
+ logger.info(f"Deleted Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
210
+
211
+ return response
212
+
213
+ @catch_error
214
+ def delete_hidden_messages(self, old_thread=None):
215
+ if old_thread is None:
216
+ old_thread = self.current_thread
217
+
218
+ # create a new thread
219
+ messages = [msg for msg in self._get_current_thread_history(remove_system_message=False) if not msg['content'].startswith("[hidden]")]
220
+ if len(messages) >= 29:
221
+ messages = messages[-29:]
222
+ logger.info(f"Current Thread Messages: {messages}", extra={"user_id": self.user.user_id, "endpoint": "delete_hidden_messages"})
223
+
224
+ new_thread = self.client.beta.threads.create(messages=messages)
225
+
226
+ # delete old thread
227
+ self.client.beta.threads.delete(old_thread.id)
228
+
229
+ # set current thread
230
+ self.current_thread = new_thread
231
+
232
+ @catch_error
233
+ def cancel_run(self, run, thread = None):
234
+ # Cancels and recreates a thread
235
+ logger.info(f"(CM) Cancelling run {run} for thread {thread}", extra={"user_id": self.user.user_id, "endpoint": "cancel_run"})
236
+ if thread is None:
237
+ thread = self.current_thread.id
238
+
239
+ if self.intro_done:
240
+ self.assistants['general'].cancel_run(run, thread)
241
+ else:
242
+ self.assistants['intro'].cancel_run(run, thread)
243
+
244
+ logger.info(f"Run cancelled", extra={"user_id": self.user.user_id, "endpoint": "cancel_run"})
245
+ return True
246
+
247
+ @catch_error
248
+ def clone(self, client):
249
+ """Creates a new ConversationManager with copied thread messages."""
250
+ # Create new instance with same init parameters
251
+ new_cm = ConversationManager(
252
+ client,
253
+ self.user,
254
+ self.assistants['general'].id,
255
+ intro_done=True
256
+ )
257
+
258
+ # Get all messages from current thread
259
+ messages = self._get_current_thread_history(remove_system_message=False)
260
+
261
+ # Delete the automatically created thread from constructor
262
+ new_cm.client.beta.threads.delete(new_cm.current_thread.id)
263
+
264
+ # Create new thread with first 30 messages
265
+ new_cm.current_thread = new_cm.client.beta.threads.create(
266
+ messages=messages[:30]
267
+ )
268
+
269
+ # Add remaining messages one by one if there are more than 30
270
+ for msg in messages[30:]:
271
+ new_cm.add_message_to_thread(
272
+ new_cm.current_thread.id,
273
+ msg['role'],
274
+ msg['content']
275
+ )
276
+
277
+ # Copy other relevant state
278
+ new_cm.state = self.state
279
+
280
+ return new_cm
281
+
282
+ def __str__(self):
283
+ return f"ConversationManager(intro_done={self.intro_done}, assistants={self.assistants}, current_thread={self.current_thread})"
284
+
285
+ def __repr__(self):
286
+ return (f"ConversationManager("
287
+ f"intro_done={self.intro_done}, current_thread={self.current_thread})")
app/exceptions.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+ from typing import Optional, Dict, Any
3
+ import traceback
4
+ import logging
5
+ import json
6
+ import re
7
+ from datetime import datetime
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ import re
12
+
13
+ class BaseOurcoachException(Exception):
14
+ def __init__(self, **kwargs):
15
+ self.user_id = kwargs.get('user_id', 'no-user')
16
+ self.code = kwargs.get('code', 'UnknownError')
17
+ self.message = kwargs.get('message', 'An unknown error occurred')
18
+ self.e = kwargs.get('e', None)
19
+
20
+ # Initialize the parent Exception with the message
21
+ Exception.__init__(self, self.message)
22
+
23
+ # Capture the full traceback
24
+ self.stack_trace = traceback.format_exc()
25
+ self.timestamp = datetime.utcnow()
26
+
27
+ logger.exception(f"Error raised with code={self.code}, message={self.message}, user_id={self.user_id}", exc_info=self.e)
28
+
29
+ def get_formatted_details(self) -> dict:
30
+ def format_traceback(traceback_str: str) -> str:
31
+ # Extract error type and message
32
+ error_pattern = r"(\w+Error): (.+?)(?=\n|$)"
33
+ error_match = re.search(error_pattern, traceback_str)
34
+ error_type = error_match.group(1) if error_match else "Unknown Error"
35
+ error_msg = error_match.group(2) if error_match else ""
36
+
37
+ # Extract file paths and line numbers
38
+ file_pattern = r"File \"(.+?)\", line (\d+), in (\w+)"
39
+ matches = re.findall(file_pattern, traceback_str)
40
+
41
+ # Build formatted output
42
+ formatted_lines = [f"Error: {error_type} - {error_msg}\n"]
43
+
44
+ for filepath, line_num, func_name in matches:
45
+ if func_name == "wrapper":
46
+ continue
47
+ # Convert to relative path
48
+ rel_path = filepath.split('app/')[-1] if 'app/' in filepath else filepath.split('\\')[-1]
49
+ formatted_lines.append(f"at {rel_path}:{func_name} (line {line_num})")
50
+
51
+ return "\n".join(formatted_lines)
52
+ """Returns pinpointed error details."""
53
+ return {
54
+ "type": f"{self.__class__.__name__}{'.' + self.code if self.code != self.__class__.__name__ else ''}",
55
+ "message": self.message,
56
+ "stack_trace": format_traceback(self.stack_trace),
57
+ "user_id": self.user_id,
58
+ "at": self.timestamp.isoformat(),
59
+ }
60
+
61
+ def to_json(self) -> Dict[str, Any]:
62
+ """Convert exception to JSON-serializable dictionary"""
63
+ return self.get_formatted_details()
64
+
65
+ def __str__(self) -> str:
66
+ return json.dumps(self.to_json(), indent=2)
67
+
68
+ class DBError(BaseOurcoachException):
69
+ ALLOWED_CODES = ['S3Error', 'SQLError', 'NoOnboardingError', 'NoPickleError', 'NoBookingError']
70
+ def __init__(self, **kwargs):
71
+ if kwargs.get('code') not in self.ALLOWED_CODES:
72
+ raise ValueError(f"Invalid code for DBError: {kwargs.get('code')}")
73
+ super().__init__(**kwargs)
74
+
75
+ def to_json(self) -> Dict[str, Any]:
76
+ base_json = super().to_json()
77
+ base_json["allowed_codes"] = self.ALLOWED_CODES
78
+ return base_json
79
+
80
+ class OpenAIRequestError(BaseOurcoachException):
81
+ def __init__(self, **kwargs):
82
+ super().__init__(**kwargs)
83
+ self.run_id = kwargs.get('run_id', None)
84
+
85
+ def to_json(self) -> Dict[str, Any]:
86
+ base_json = super().to_json()
87
+ base_json["run_id"] = self.run_id
88
+ return base_json
89
+
90
+ class AssistantError(BaseOurcoachException):
91
+ def __init__(self, **kwargs):
92
+ kwargs['code'] = "AssistantError"
93
+ super().__init__(**kwargs)
94
+
95
+ class UserError(BaseOurcoachException):
96
+ def __init__(self, **kwargs):
97
+ kwargs['code'] = "UserError"
98
+ super().__init__(**kwargs)
99
+
100
+ class ConversationManagerError(BaseOurcoachException):
101
+ def __init__(self, **kwargs):
102
+ kwargs['code'] = "ConversationManagerError"
103
+ super().__init__(**kwargs)
104
+
105
+ class FastAPIError(BaseOurcoachException):
106
+ def __init__(self, **kwargs):
107
+ kwargs['code'] = "FastAPIError"
108
+ super().__init__(**kwargs)
109
+
110
+ class UtilsError(BaseOurcoachException):
111
+ def __init__(self, **kwargs):
112
+ kwargs['code'] = "UtilsError"
113
+ super().__init__(**kwargs)
app/flows.py CHANGED
@@ -40,6 +40,11 @@ The user has just completed their growth guide session. Generate an appropriate
40
  """
41
 
42
  MICRO_ACTION_STATE = f"""
 
 
 
 
 
43
  **Micro-Action** *(PLEASE READ CAREFULLY)*
44
  **Objective:** Build momentum toward the user’s goal with small, actionable tasks that feel immediately achievable. Avoid saying “Today’s micro-action is:”—state the action naturally.
45
 
@@ -50,19 +55,24 @@ MICRO_ACTION_STATE = f"""
50
 
51
  *(If the user has postponed micro-actions from the list above, remind them of the postponed micro-actions and ask if they are ready to do it today. If the user says yes, proceed with the postponed micro-action. If the user says no, propose a new micro-action.)*
52
  ---
53
- **The Order of Your Conversation Flow (Follow these step-by-step instructions):**
 
 
 
 
54
 
55
- **Step 1: Your First Message: Micro-Action Suggestion**
56
 
57
  - Propose one clear, actionable task for the day.
58
  - Ensure it is relevant, easy to begin, and framed positively.
59
  - Avoid repeating previous actions.
60
- - Be concise (use Whatsapp texting length)
 
61
 
62
- **Step 2: User Response Handling**
63
 
64
  - **If the user says they can only do it on a later time:**
65
- - Send an understanding yet encouraging message.
66
  - **Ask** what time to remind them (use this question only if necessary).
67
  - Immediately call the `process_reminder()` function:
68
  ```
@@ -74,20 +84,20 @@ MICRO_ACTION_STATE = f"""
74
  )
75
  ```
76
  - Ensure that you set the `'recurrence'` to `'postponed'`.
77
- - Then, end the conversation:
78
- - Provide two short, specific, technical, and practical pieces of advice in bullet points. (Ignore if you've given the two bullet points already)
79
  - Immediately call the `end_conversation()` function without waiting for user's response
80
  - *(Do **not** explicitly mention the content of the reminder to the user.)*
81
 
82
  - **If the user has decided to try out the micro-action:**
83
- - Encourage them to proceed with the task.
84
  - Do **not** ask any questions at this point.
85
  - After the user confirms they've completed the micro-action:
86
  - Acknowledge their effort.
87
- - (Ignore if you've given the two bullet points already) Provide value with two short, specific, technical, and practical pieces of advice in bullet points, **without** asking further questions. Again, do **NOT** ask any question if you don't have to.
88
  - Be concise with your message! (use Whatsapp texting length)
89
 
90
- **Step 3: End Gracefully**
91
 
92
  - After the user's reply, immediately call the `end_conversation()` function.
93
  - Conclude with:
@@ -95,7 +105,7 @@ MICRO_ACTION_STATE = f"""
95
  - Be concise! (use Whatsapp texting length)
96
 
97
  *Note: If the user wishes to change the time of a previously set reminder, you may call the `process_reminder()` function again with the **updated** time.*
98
-
99
  ---
100
 
101
  **Key Rules for Micro-Actions**
@@ -147,6 +157,11 @@ MICRO_ACTION_STATE = f"""
147
  """
148
 
149
  FOLLUP_ACTION_STATE = f"""
 
 
 
 
 
150
  **Following Up Yesterday's Micro Action**
151
  **Objective:** Follow up on the user's progress with their micro-action from yesterday and share useful knowledge, tools, or practices from your persona. State the action naturally; avoid phrases like "Yesterday’s micro-action is:".
152
 
@@ -163,12 +178,15 @@ FOLLUP_ACTION_STATE = f"""
163
 
164
  **Conversation Flow**
165
 
 
 
 
 
166
  **If the user has no upcoming reminders:**
167
 
168
  1. **Follow Up:**
169
  - **If** the user has completed yesterday's micro-action:
170
- - Ask about their experience while doing it.
171
- - Or, ask one discussion question about the micro-action (if they already shared their experience).
172
  - **If** the user hasn't completed it:
173
  - Ask if they're going to do it today.
174
  - **Unless** they've specified a different day—then proceed to Step 2.
@@ -369,6 +387,11 @@ MOTIVATION_INSPIRATION_STATE = f"""
369
  """
370
 
371
  OPEN_DISCUSSION_STATE = f"""
 
 
 
 
 
372
  ## ** Open Discussion and Personalization ** (PLEASE READ CAREFULLY)
373
  Objective: Build rapport, create space for the user to express themselves, and adapt the interaction to their needs.
374
 
@@ -377,6 +400,7 @@ User’s Context
377
  • Day: {{}}/{{}} of their journey.
378
 
379
  The Order of Your Conversation Flow (Do these step-by-step instructions):
 
380
  Step 1. Start with a **creative** or unexpected open question to discuss together.
381
  Step 2. Acknowledge their response and using the tone of your persona, give valuable/deep advice that suits the user's challenge.
382
  Step 3. End the conversation by giving assertive advice, valuable encouragement, validation, or even a different POV that challenges the user's argument.
@@ -536,6 +560,11 @@ Based on the above, coach and engage the user in a succinct and brief conversati
536
  ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow."""
537
 
538
  FUNFACT_STATE = f"""
 
 
 
 
 
539
  ## ** Giving Value to The User ** (PLEASE READ CAREFULLY)
540
  Objective: Provide a personalized and uplifting message that resonates with the user's unique attributes, recent actions, and surroundings, to motivate them on their journey to achieve their goal. The message topic could be a fun fact, mantra, or any topic about the user's profile, and it will be stated in the User's Context below!
541
 
@@ -545,6 +574,7 @@ User’s Context:
545
  • For today's message, use this user's information as the topic: {{}}
546
 
547
  The Order of Your Conversation Flow (Do these step-by-step instructions):
 
548
  Step 1. Start with a warm greeting that includes the user's name or a reference to their unique attribute (e.g., "Hi, Problem-Solver!").
549
  Step 2. Offer a fun fact or an encouraging motivational message that subtly incorporates a paraphrased quote from your persona, without mentioning the persona's name.
550
  Step 3. Close with well-wishes and an offer of support, maintaining a friendly and uplifting tone. Call the end_conversation() immediately!
 
40
  """
41
 
42
  MICRO_ACTION_STATE = f"""
43
+ Note: If the user has not answered your question yesterday, you must say something like (but warmer) "Hey, you didn't answer my question. Do you still want to continue?"
44
+ If the user says "no", then ask if they want to set a new goal (therefore later, call the change_goal() function)
45
+ But if the user says "yes", then proceed to the theme below.
46
+ Otherwise, you may directly proceed to today's theme below without asking the user.
47
+
48
  **Micro-Action** *(PLEASE READ CAREFULLY)*
49
  **Objective:** Build momentum toward the user’s goal with small, actionable tasks that feel immediately achievable. Avoid saying “Today’s micro-action is:”—state the action naturally.
50
 
 
55
 
56
  *(If the user has postponed micro-actions from the list above, remind them of the postponed micro-actions and ask if they are ready to do it today. If the user says yes, proceed with the postponed micro-action. If the user says no, propose a new micro-action.)*
57
  ---
58
+ **The Order of Your Conversation Flow (Follow these step-by-step instructions! You are sending 3 messages in a day):**
59
+
60
+ Step 0: Check whether the user has answered your yesterday's question (if any)
61
+
62
+ Step 1:
63
 
64
+ **First Message: Micro-Action Suggestion**
65
 
66
  - Propose one clear, actionable task for the day.
67
  - Ensure it is relevant, easy to begin, and framed positively.
68
  - Avoid repeating previous actions.
69
+ - Your message must be concise! (use Whatsapp texting length)
70
+ - Wait for the user's response
71
 
72
+ **Second Message: User Response Handling**
73
 
74
  - **If the user says they can only do it on a later time:**
75
+ - Send an understanding yet encouraging message (Your message must be concise!)
76
  - **Ask** what time to remind them (use this question only if necessary).
77
  - Immediately call the `process_reminder()` function:
78
  ```
 
84
  )
85
  ```
86
  - Ensure that you set the `'recurrence'` to `'postponed'`.
87
+ - Then, end the conversation by:
88
+ - Providing two short, specific, technical, and practical pieces of advice in bullet points
89
  - Immediately call the `end_conversation()` function without waiting for user's response
90
  - *(Do **not** explicitly mention the content of the reminder to the user.)*
91
 
92
  - **If the user has decided to try out the micro-action:**
93
+ - Encourage them to proceed with the task (Your message must be concise!)
94
  - Do **not** ask any questions at this point.
95
  - After the user confirms they've completed the micro-action:
96
  - Acknowledge their effort.
97
+ - (Ignore if you've given the two bullet points before) Provide value with two short, specific, technical, and practical pieces of advice in bullet points, **without** asking further questions. Again, do **NOT** ask any question if you don't have to.
98
  - Be concise with your message! (use Whatsapp texting length)
99
 
100
+ **Third Message: End Gracefully**
101
 
102
  - After the user's reply, immediately call the `end_conversation()` function.
103
  - Conclude with:
 
105
  - Be concise! (use Whatsapp texting length)
106
 
107
  *Note: If the user wishes to change the time of a previously set reminder, you may call the `process_reminder()` function again with the **updated** time.*
108
+ *Note: If the user wishes to continue the conversation after it ends, reply concisely, give a short one sentence advice, and end the conversation*
109
  ---
110
 
111
  **Key Rules for Micro-Actions**
 
157
  """
158
 
159
  FOLLUP_ACTION_STATE = f"""
160
+ Note: If the user has not answered your question yesterday, you must say something like (but warmer) "Hey, you didn't answer my question. Do you still want to continue?"
161
+ If the user says "no", then ask if they want to set a new goal (therefore later, call the change_goal() function)
162
+ But if the user says "yes", then proceed to the theme below.
163
+ Otherwise, you may directly proceed to today's theme below without asking the user.
164
+
165
  **Following Up Yesterday's Micro Action**
166
  **Objective:** Follow up on the user's progress with their micro-action from yesterday and share useful knowledge, tools, or practices from your persona. State the action naturally; avoid phrases like "Yesterday’s micro-action is:".
167
 
 
178
 
179
  **Conversation Flow**
180
 
181
+ Step 0: Check whether the user has answered your yesterday's question (if any)
182
+
183
+ Step 1:
184
+
185
  **If the user has no upcoming reminders:**
186
 
187
  1. **Follow Up:**
188
  - **If** the user has completed yesterday's micro-action:
189
+ - Do **not** ask anything and proceed to step 2
 
190
  - **If** the user hasn't completed it:
191
  - Ask if they're going to do it today.
192
  - **Unless** they've specified a different day—then proceed to Step 2.
 
387
  """
388
 
389
  OPEN_DISCUSSION_STATE = f"""
390
+ Note: If the user has not answered your question yesterday, you must say something like (but warmer) "Hey, you didn't answer my question. Do you still want to continue?"
391
+ If the user says "no", then ask if they want to set a new goal (therefore later, call the change_goal() function)
392
+ But if the user says "yes", then proceed to the theme below.
393
+ Otherwise, you may directly proceed to today's theme below without asking the user.
394
+
395
  ## ** Open Discussion and Personalization ** (PLEASE READ CAREFULLY)
396
  Objective: Build rapport, create space for the user to express themselves, and adapt the interaction to their needs.
397
 
 
400
  • Day: {{}}/{{}} of their journey.
401
 
402
  The Order of Your Conversation Flow (Do these step-by-step instructions):
403
+ Step 0. Check whether the user has answered your yesterday's question (if any)
404
  Step 1. Start with a **creative** or unexpected open question to discuss together.
405
  Step 2. Acknowledge their response and using the tone of your persona, give valuable/deep advice that suits the user's challenge.
406
  Step 3. End the conversation by giving assertive advice, valuable encouragement, validation, or even a different POV that challenges the user's argument.
 
560
  ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow."""
561
 
562
  FUNFACT_STATE = f"""
563
+ Note: If the user has not answered your question yesterday, you must say something like (but warmer) "Hey, you didn't answer my question. Do you still want to continue?"
564
+ If the user says "no", then ask if they want to set a new goal (therefore later, call the change_goal() function)
565
+ But if the user says "yes", then proceed to the theme below.
566
+ Otherwise, you may directly proceed to today's theme below without asking the user.
567
+
568
  ## ** Giving Value to The User ** (PLEASE READ CAREFULLY)
569
  Objective: Provide a personalized and uplifting message that resonates with the user's unique attributes, recent actions, and surroundings, to motivate them on their journey to achieve their goal. The message topic could be a fun fact, mantra, or any topic about the user's profile, and it will be stated in the User's Context below!
570
 
 
574
  • For today's message, use this user's information as the topic: {{}}
575
 
576
  The Order of Your Conversation Flow (Do these step-by-step instructions):
577
+ Step 0. Check whether the user has answered your yesterday's question (if any)
578
  Step 1. Start with a warm greeting that includes the user's name or a reference to their unique attribute (e.g., "Hi, Problem-Solver!").
579
  Step 2. Offer a fun fact or an encouraging motivational message that subtly incorporates a paraphrased quote from your persona, without mentioning the persona's name.
580
  Step 3. Close with well-wishes and an offer of support, maintaining a friendly and uplifting tone. Call the end_conversation() immediately!
app/main.py CHANGED
@@ -1,5 +1,5 @@
1
- from fastapi import FastAPI, HTTPException, Security, Query, status, Request
2
- from fastapi.responses import FileResponse, StreamingResponse
3
  from fastapi.security import APIKeyHeader
4
  import openai
5
  from pydantic import BaseModel
@@ -11,11 +11,13 @@ import regex as re
11
  from datetime import datetime, timezone
12
  from app.user import User
13
  from typing import List, Optional, Callable
 
 
14
  from openai import OpenAI
15
  import psycopg2
16
  from psycopg2 import sql
17
  import os
18
- from app.utils import add_to_cache, get_api_key, get_user_info, get_growth_guide_session, pop_cache, print_log, get_user, upload_mementos_to_db, get_user_summary, get_user_life_status, create_pre_gg_report
19
  from dotenv import load_dotenv
20
  import logging.config
21
  import time
@@ -23,11 +25,30 @@ from starlette.middleware.base import BaseHTTPMiddleware
23
  import sys
24
  import boto3
25
  import pickle
 
 
 
26
 
27
  load_dotenv()
28
  AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY')
29
  AWS_SECRET_KEY = os.getenv('AWS_SECRET_KEY')
30
  REGION = os.getenv('AWS_REGION')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  # Create required folders
33
  os.makedirs('logs', exist_ok=True)
@@ -254,229 +275,280 @@ class ChatItem(BaseModel):
254
  user_id: str
255
  message: str
256
 
257
- class AssistantItem(BaseModel):
258
- user_id: str
259
- assistant_id: str
260
-
261
- class ChangeDateItem(BaseModel):
262
- user_id: str
263
- date: str
264
-
265
  class PersonaItem(BaseModel):
266
  user_id: str
267
  persona: str
268
 
269
  class GGItem(BaseModel):
 
270
  gg_session_id: str
 
 
271
  user_id: str
 
272
 
273
- class ErrorResponse(BaseModel):
274
- status: str = "error"
275
- code: int
276
- message: str
277
- timestamp: datetime = datetime.now(timezone.utc)
278
 
279
  class BookingItem(BaseModel):
280
  booking_id: str
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
- @app.get("/ok")
284
- def ok_endpoint():
285
- print_log("INFO", "health check endpoint")
286
- logger.info("Health check endpoint called", extra={"endpoint": "/ok"})
287
- return {"message": "ok"}
288
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  @app.post("/set_intro_done")
290
- def set_intro_done(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
291
  user = get_user(user_id)
 
292
  user.set_intro_done()
293
  logger.info("Intro done", extra={"user_id": user_id, "endpoint": "/set_intro_done"})
294
  return {"response": "ok"}
295
 
 
296
  @app.post("/set_goal")
297
- def set_goal(user_id: str, goal: str, api_key: str = Security(get_api_key)):
298
- user = get_user(user_id)
 
 
 
 
 
 
299
  user.set_goal(goal)
300
  logger.info(f"Goal set: {goal}", extra={"user_id": user_id, "endpoint": "/set_goal"})
301
  return {"response": "ok"}
302
 
303
- @app.post("/do_micro")
304
- def do_micro(request: ChangeDateItem, day: int, api_key: str = Security(get_api_key)):
305
- print_log("INFO", "do_micro endpoint")
306
- logger.info("do_micro endpoint called", extra={"endpoint": "/do_micro"})
307
-
308
- # get user
309
- user = get_user(request.user_id)
310
-
311
- try:
312
  response = user.do_micro(request.date, day)
313
- except openai.BadRequestError:
314
- # Check if there is an active run for the thread id
315
- recent_run = user.get_recent_run()
316
- print_log("INFO",f"Recent run: {recent_run}", extra={"user_id": request.user_id, "endpoint": "/chat"})
317
- logger.info(f"Recent run: {recent_run}", extra={"user_id": request.user_id, "endpoint": "/chat"})
318
- # If there is an active run, cancel it and resubmit the previous message
319
- if recent_run:
320
- user.cancel_run(recent_run)
321
- response = user.send_message(user.get_recent_message())
322
-
323
- print_log("INFO",f"Assistant: {response['content']}", extra={"user_id": request.user_id, "endpoint": "/chat"})
324
- logger.info(f"Assistant: {response['content']}", extra={"user_id": request.user_id, "endpoint": "/chat"})
325
- return {"response": response}
326
-
327
-
328
  # endpoint to change user assistant using user.change_to_latest_assistant()
329
  @app.post("/change_assistant")
330
- def change_assistant(request: AssistantItem, api_key: str = Security(get_api_key)):
331
- user_id = request.user_id
332
- assistant_id = request.assistant_id
333
- print_log("INFO", "Changing assistant", extra={"user_id": user_id, "endpoint": "/change_assistant"})
334
- logger.info("Changing assistant", extra={"user_id": user_id, "endpoint": "/change_assistant"})
335
- user = get_user(user_id)
336
- user.change_assistant(assistant_id)
337
- logger.info(f"Assistant changed to {assistant_id}", extra={"user_id": user_id, "endpoint": "/change_assistant"})
338
- return {"assistant_id": assistant_id}
339
-
 
 
340
 
341
  @app.post("/clear_cache")
342
- def clear_cache(api_key: str = Security(get_api_key)):
343
- print_log("INFO", "Clearing entire cache", extra={"endpoint": "/clear_cache"})
344
- logger.info("Clearing entire cache", extra={"endpoint": "/clear_cache"})
345
- try:
346
- pop_cache(user_id='all')
347
- print_log("INFO", "Cache cleared successfully", extra={"endpoint": "/clear_cache"})
348
- logger.info("Cache cleared successfully", extra={"endpoint": "/clear_cache"})
349
- return {"response": "Cache cleared successfully"}
350
- except Exception as e:
351
- print_log("ERROR", f"Error clearing cache: {str(e)}", extra={"endpoint": "/clear_cache"}, exc_info=True)
352
- logger.error(f"Error clearing cache: {str(e)}", extra={"endpoint": "/clear_cache"}, exc_info=True)
353
- raise HTTPException(
354
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
355
- detail=str(e)
356
- )
357
 
358
  @app.post("/migrate_user")
359
- def migrate_user(request: CreateUserItem, api_key: str = Security(get_api_key)):
360
- user_id = request.user_id
361
- print_log("INFO", "Migrating user", extra={"user_id": request.user_id, "endpoint": "/migrate_user"})
362
- logger.info("Migrating user", extra={"user_id": request.user_id, "endpoint": "/migrate_user"})
363
- def download_file_from_s3(filename, bucket):
364
- user_id = filename.split('.')[0]
365
- function_name = download_file_from_s3.__name__
366
- logger.info(f"Downloading file {filename} from [staging] S3 bucket {bucket}", extra={'user_id': user_id, 'endpoint': function_name})
367
- file_path = os.path.join('users', 'data', filename)
368
- try:
369
- if (AWS_ACCESS_KEY and AWS_SECRET_KEY):
370
- session = boto3.session.Session(aws_access_key_id=AWS_ACCESS_KEY, aws_secret_access_key=AWS_SECRET_KEY, region_name=REGION)
371
- else:
372
- session = boto3.session.Session()
373
- s3_client = session.client('s3')
374
- with open(file_path, 'wb') as f:
375
- ## Upload to Staging Folder
376
- s3_client.download_fileobj(bucket, f"staging/users/{filename}", f)
377
- logger.info(f"File {filename} downloaded successfully from S3", extra={'user_id': user_id, 'endpoint': function_name})
378
- return True
379
- except Exception as e:
380
- logger.error(f"Error downloading file {filename} from S3: {e}", extra={'user_id': user_id, 'endpoint': function_name})
381
- if (os.path.exists(file_path)):
382
- os.remove(file_path)
383
- return False
384
- try:
385
- client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
386
- user_file = os.path.join('users', 'data', f'{user_id}.pkl')
387
- download = download_file_from_s3(f'{user_id}.pkl', 'core-ai-assets')
388
- logger.info(f"Download success: {download}", extra={'user_id': user_id, 'endpoint': 'migrate_user'})
389
- if (download):
390
- with open(user_file, 'rb') as f:
391
- old_user_object = pickle.load(f)
392
- # recreate user using data from above
393
- user = User(user_id, old_user_object.user_info, client, GENERAL_ASSISTANT)
394
- user.conversations.current_thread = old_user_object.conversations.current_thread
395
- user.conversations.intro_done = True
396
- user.done_first_reflection = old_user_object.done_first_reflection
397
- user.client = client
398
- user.conversations.client = client
399
-
400
- api_response = {"user": user.user_info, "user_messages": user.get_messages(), "general_assistant": user.conversations.assistants['general'].id, "intro_assistant": user.conversations.assistants['intro'].id}
401
-
402
- if user.goal:
403
- api_response["goal"] = user.goal
404
- else:
405
- api_response["goal"] = "No goal is not set yet"
406
-
407
- api_response["current_day"] = user.growth_plan.current()['day']
408
- api_response['micro_actions'] = user.micro_actions
409
- api_response['recommended_actions'] = user.recommended_micro_actions
410
- api_response['challenges'] = user.challenges
411
- api_response['other_focusses'] = user.other_focusses
412
- api_response['scores'] = f"Personal Growth: {user.personal_growth_score} || Career: {user.career_growth_score} || Health/Wellness: {user.health_and_wellness_score} || Relationships: {user.relationship_score} || Mental Health: {user.mental_well_being_score}"
413
- api_response['recent_wins'] = user.recent_wins
414
-
415
- add_to_cache(user)
416
- pop_cache(user.user_id)
417
- return api_response
418
- # user.save_user()
419
- # pop_cache(user.user_id)
420
- # user.client = client
421
- # user.conversations.client = client
422
- os.remove(user_file)
423
- logger.info(f"User {user_id} loaded successfully from S3", extra={'user_id': user_id, 'endpoint': 'migrate_user'})
424
-
425
-
426
-
427
- except Exception as e:
428
- logger.error(f"Error migrating user {user_id}: {e}", extra={'user_id': user_id, 'endpoint': 'migrate_user'})
429
- raise HTTPException(
430
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
431
- detail=str(e)
432
- )
433
 
 
 
 
 
434
  @app.get("/get_user")
435
- def get_user_by_id(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
436
  print_log("INFO", "Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
437
  logger.info("Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
438
- try:
439
- user = get_user(user_id)
440
- print_log("INFO", "Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
441
- logger.info("Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
442
- api_response = {"user": user.user_info, "user_messages": user.get_messages(), "general_assistant": user.conversations.assistants['general'].id, "intro_assistant": user.conversations.assistants['intro'].id}
443
-
444
- if user.goal:
445
- api_response["goal"] = user.goal
446
- else:
447
- api_response["goal"] = "No goal is not set yet"
448
-
449
- api_response["current_day"] = user.growth_plan.current()['day']
450
- api_response['micro_actions'] = user.micro_actions
451
- api_response['recommended_actions'] = user.recommended_micro_actions
452
- api_response['challenges'] = user.challenges
453
- api_response['other_focusses'] = user.other_focusses
454
- api_response['reminders'] = user.reminders
455
- api_response['scores'] = f"Personal Growth: {user.personal_growth_score} || Career: {user.career_growth_score} || Health/Wellness: {user.health_and_wellness_score} || Relationships: {user.relationship_score} || Mental Health: {user.mental_well_being_score}"
456
- api_response['recent_wins'] = user.recent_wins
457
-
458
- return api_response
459
- except LookupError:
460
- print_log("ERROR", "User not found", extra={"user_id": user_id, "endpoint": "/get_user"})
461
- logger.error("User not found", extra={"user_id": user_id, "endpoint": "/get_user"})
462
- raise HTTPException(
463
- status_code=status.HTTP_404_NOT_FOUND,
464
- detail=f"User with ID {user_id} not found"
465
- )
466
- except Exception as e:
467
- print_log("ERROR",f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user"}, exc_info=True)
468
- logger.error(f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user"}, exc_info=True)
469
- raise HTTPException(
470
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
471
- detail=str(e)
472
- )
473
 
 
 
 
 
 
 
 
 
 
474
 
 
475
 
476
  @app.post("/update_user_persona")
 
477
  async def update_user_persona(
478
  request: PersonaItem,
479
- api_key: str = Security(get_api_key)
480
  ):
481
  """Update user's legendary persona in the database"""
482
  user_id = request.user_id
@@ -486,78 +558,71 @@ async def update_user_persona(
486
  user.update_user_info(f"User's new Legendary Persona is: {persona}")
487
  logger.info(f"Updated persona to {persona}", extra={"user_id": user_id, "endpoint": "/update_user_persona"})
488
 
489
- try:
490
- # Connect to database
491
- db_params = {
492
- 'dbname': 'ourcoach',
493
- 'user': 'ourcoach',
494
- 'password': 'hvcTL3kN3pOG5KteT17T',
495
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
496
- 'port': '5432'
497
- }
498
- conn = psycopg2.connect(**db_params)
499
- cur = conn.cursor()
500
-
501
- # Get current onboarding data
502
- cur.execute("SELECT onboarding FROM users WHERE id = %s", (user_id,))
503
- result = cur.fetchone()
504
- if not result:
505
- raise HTTPException(status_code=404, detail="User not found")
506
-
507
- # Update legendPersona in onboarding JSON
508
- onboarding = json.loads(result[0])
509
- onboarding['legendPersona'] = persona
510
-
511
- # Update database
512
- cur.execute(
513
- "UPDATE users SET onboarding = %s WHERE id = %s",
514
- (json.dumps(onboarding), user_id)
515
  )
516
- conn.commit()
517
 
518
- return {"status": "success", "message": f"Updated persona to {persona}"}
519
-
520
- except Exception as e:
521
- raise HTTPException(status_code=500, detail=str(e))
522
 
523
- finally:
524
- if 'cur' in locals():
525
- cur.close()
526
- if 'conn' in locals():
527
- conn.close()
 
 
 
 
 
 
 
528
 
529
  @app.post("/add_ai_message")
530
- def add_ai_message(request: ChatItem, api_key: str = Security(get_api_key)):
 
 
 
 
531
  user_id = request.user_id
532
  message = request.message
533
  logger.info("Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
534
  print_log("INFO", "Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
535
- try:
536
- user = get_user(user_id)
537
- user.add_ai_message(message)
538
 
539
- add_to_cache(user)
540
- update = pop_cache(user.user_id)
541
-
542
- print_log("INFO", "AI response added", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
543
- return {"response": "ok"}
544
- except LookupError:
545
- print_log("ERROR", "User not found", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
546
- logger.error("User not found", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
547
- raise HTTPException(
548
- status_code=status.HTTP_404_NOT_FOUND,
549
- detail=f"User with ID {user_id} not found"
550
- )
551
- except Exception as e:
552
- print_log("ERROR",f"Error adding AI response: {str(e)}", extra={"user_id": user_id, "endpoint": "/add_ai_message"}, exc_info=True)
553
- logger.error(f"Error adding AI response: {str(e)}", extra={"user_id": user_id, "endpoint": "/add_ai_message"}, exc_info=True)
554
- raise HTTPException(
555
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
556
- detail=str(e)
557
- )
558
 
559
  @app.post("/schedule_gg_reminder")
560
- def schedule_gg_reminder(request: ChangeDateItem, api_key: str = Security(get_api_key)):
 
 
 
 
561
  # session_id = request.gg_session_id
562
  user_id = request.user_id
563
  logger.info(f"Scheduling GG session reminder for {request.date}", extra={"user_id": user_id, "endpoint": "/schedule_gg_reminder"})
@@ -573,53 +638,59 @@ def schedule_gg_reminder(request: ChangeDateItem, api_key: str = Security(get_ap
573
  return {"response": response}
574
 
575
  @app.post("/process_gg_session")
576
- def process_gg_session(request: GGItem, api_key: str = Security(get_api_key)):
577
- session_id = request.gg_session_id
578
- user_id = request.user_id
579
- logger.info(f"Processing GG session: {session_id}", extra={"user_id": user_id, "endpoint": "/process_gg_session"})
580
- print_log("INFO", f"Processing GG session: {session_id}", extra={"user_id": user_id, "endpoint": "/process_gg_session"})
581
-
582
- # get user
583
- user = get_user(user_id)
584
-
585
- # get the session_data
586
- session_data = get_growth_guide_session(user_id, session_id)
587
 
588
- # update user
589
- response = user.process_growth_guide_session(session_data, session_id)
590
- logger.info(f"GG session processed: {session_id}, response: {response}", extra={"user_id": user_id, "endpoint": "/process_gg_session"})
 
 
591
  return {"response": response}
592
 
 
593
  @app.get("/user_daily_messages")
594
- def get_daily_message(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
595
  logger.info("Getting daily messages", extra={"user_id": user_id, "endpoint": "/user_daily_messages"})
596
  user = get_user(user_id)
597
  daily_messages = user.get_daily_messages()
598
  return {"response": daily_messages}
599
 
600
  @app.post("/batch_refresh_users")
601
- def refresh_multiple_users(user_ids: List[str], api_key: str = Security(get_api_key)):
 
 
 
 
602
  logger.info("Refreshing multiple users", extra={"endpoint": "/batch_refresh_users"})
603
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
604
  failed_users = []
605
 
606
  for i,user_id in enumerate(user_ids):
607
- try:
608
- old_user = get_user(user_id)
609
- user = old_user.refresh(client)
610
- add_to_cache(user)
611
- update = pop_cache(user.user_id)
612
- logger.info(f"Successfully refreshed user {i+1}/{len(user_ids)}", extra={"user_id": user_id, "endpoint": "/batch_refresh_users"})
613
- except Exception as e:
614
- logger.error(f"Failed to refresh user: {str(e)}", extra={"user_id": user_id, "endpoint": "/batch_refresh_users"})
615
- failed_users.append(user_id)
616
 
617
  if failed_users:
618
  return {"status": "partial", "failed_users": failed_users}
619
  return {"status": "success", "failed_users": []}
620
 
621
  @app.post("/refresh_user")
622
- def refresh_user(request: CreateUserItem, api_key: str = Security(get_api_key)):
 
 
 
 
623
  print_log("INFO","Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
624
  logger.info("Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
625
 
@@ -633,267 +704,131 @@ def refresh_user(request: CreateUserItem, api_key: str = Security(get_api_key)):
633
  return {"response": "ok"}
634
 
635
  @app.post("/create_user")
636
- def create_user(request: CreateUserItem, api_key: str = Security(get_api_key)):
637
-
638
- # # delete and recreate user log file
639
- # user_id = request.user_id
640
- # user_log_file = os.path.join('logs', 'users', f'{user_id}.log')
641
- # if os.path.exists(user_log_file):
642
- # os.remove(user_log_file)
643
- # # create new user log file
644
- # open(user_log_file, 'w').close()
645
-
646
- print_log("INFO","Creating new user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
647
  logger.info("Creating new user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
648
- try:
649
- client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
650
 
651
- # check if user exists by looking for pickle file in users/data
652
- # if os.path.exists(f'users/data/{request.user_id}.pkl'):
653
- # print_log("INFO",f"User already exists: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
654
- # logger.info(f"User already exists: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
655
- # return {"message": f"[OK] User already exists: {request.user_id}"}
656
-
657
- user_info, _ = get_user_info(request.user_id)
658
- if not user_info:
659
- print_log("ERROR",f"Could not fetch user information from DB {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
660
- logger.error(f"Could not fetch user information from DB {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
661
- raise HTTPException(
662
- status_code=status.HTTP_400_BAD_REQUEST,
663
- detail="Could not fetch user information from DB"
664
- )
665
-
666
- user = User(request.user_id, user_info, client, GENERAL_ASSISTANT)
667
 
668
- # create memento folder for user
669
- folder_path = os.path.join("mementos", "to_upload", request.user_id)
 
 
 
 
 
670
 
671
- # create folder if not exists
672
- os.makedirs(folder_path, exist_ok=True)
673
- print_log("INFO",f"Created temp memento folder for user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
674
- logger.info(f"Created temp memento folder for user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
675
 
676
-
677
- # upload user pickle file to s3 bucket
678
- try:
679
- add_to_cache(user)
680
- pop_cache(request.user_id)
681
- upload = True
682
- except:
683
- upload = False
684
-
685
- if upload == True:
686
- print_log("INFO",f"Successfully created user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
687
- logger.info(f"Successfully created user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
688
- return {"message": {"info": f"[OK] User created: {user}", "messages": user.get_messages()}}
689
- else:
690
- print_log("ERROR",f"Failed to upload user pickle to S3", extra={"user_id": request.user_id, "endpoint": "/create_user"})
691
- logger.error(f"Failed to upload user pickle to S3", extra={"user_id": request.user_id, "endpoint": "/create_user"})
692
- raise HTTPException(
693
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
694
- detail="Failed to upload user pickle to S3"
695
- )
696
-
697
- except Exception as e:
698
- print_log("ERROR",f"Failed to create user: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/create_user"}, exc_info=True)
699
- logger.error(f"Failed to create user: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/create_user"}, exc_info=True)
700
- raise HTTPException(
701
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
702
- detail=str(e)
703
- )
704
 
705
  @app.post("/chat")
706
- def chat(request: ChatItem, api_key: str = Security(get_api_key)):
707
- print_log("INFO","Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
 
 
 
708
  logger.info("Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
709
-
710
- try:
711
- # get user
712
- user = get_user(request.user_id)
713
-
714
- try:
715
- response = user.send_message(request.message)
716
- except openai.BadRequestError as e:
717
- print(e)
718
- # Check if there is an active run for the thread id
719
- recent_run = user.get_recent_run()
720
- print_log("INFO",f"Recent run: {recent_run}", extra={"user_id": request.user_id, "endpoint": "/chat"})
721
- logger.info(f"Recent run: {recent_run}", extra={"user_id": request.user_id, "endpoint": "/chat"})
722
- # If there is an active run, cancel it and resubmit the previous message
723
- if recent_run:
724
- user.cancel_run(recent_run)
725
- response = user.send_message(user.get_recent_message())
726
- finally:
727
- print_log("INFO",f"Assistant: {response['content']}", extra={"user_id": request.user_id, "endpoint": "/chat"})
728
- logger.info(f"Assistant: {response['content']}", extra={"user_id": request.user_id, "endpoint": "/chat"})
729
-
730
- return {"response": response}
731
- except LookupError:
732
- print_log("ERROR",f"User not found for chat: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/chat"})
733
- logger.error(f"User not found for chat: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/chat"})
734
- raise HTTPException(
735
- status_code=status.HTTP_404_NOT_FOUND,
736
- detail=f"User with ID {request.user_id} not found"
737
- )
738
- except ReferenceError:
739
- logger.warning(f"User pickle creation still ongoing for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/chat"})
740
- print_log("WARNING",f"User pickle creation still ongoing for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/chat"})
741
- raise HTTPException(
742
- status_code=status.HTTP_400_BAD_REQUEST,
743
- detail="User pickle creation still ongoing"
744
- )
745
- except Exception as e:
746
- print_log("ERROR",f"Chat error for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/chat"}, exc_info=True)
747
- logger.error(f"Chat error for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/chat"}, exc_info=True)
748
- raise HTTPException(
749
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
750
- detail=str(e)
751
- )
752
 
 
 
 
 
753
  @app.get("/reminders")
754
- def get_reminders(user_id: str, date:str, api_key: str = Security(get_api_key)):
 
 
 
 
 
755
  print_log("INFO","Getting reminders", extra={"user_id": user_id, "endpoint": "/reminders"})
756
  logger.info("Getting reminders", extra={"user_id": user_id, "endpoint": "/reminders"})
757
- try:
758
- user = get_user(user_id)
759
- reminders = user.get_reminders(date)
760
- if len(reminders) == 0:
761
- print_log("INFO",f"No reminders for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
762
- logger.info(f"No reminders for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
763
- reminders = None
764
-
765
- print_log("INFO",f"Successfully retrieved reminders: {reminders}", extra={"user_id": user_id, "endpoint": "/reminders"})
766
- logger.info(f"Successfully retrieved reminders: {reminders} for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
767
- return {"reminders": reminders}
768
- except LookupError:
769
- print_log("ERROR","User not found", extra={"user_id": user_id, "endpoint": "/reminders"})
770
- logger.error("User not found", extra={"user_id": user_id, "endpoint": "/reminders"})
771
- raise HTTPException(
772
- status_code=status.HTTP_404_NOT_FOUND,
773
- detail=f"User with ID {user_id} not found"
774
- )
775
- except Exception as e:
776
- print_log("ERROR",f"Error getting reminders: {str(e)}", extra={"user_id": user_id, "endpoint": "/reminders"}, exc_info=True)
777
- logger.error(f"Error getting reminders: {str(e)}", extra={"user_id": user_id, "endpoint": "/reminders"}, exc_info=True)
778
- raise HTTPException(
779
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
780
- detail=str(e)
781
- )
782
 
783
  @app.post("/change_date")
784
- def change_date(request: ChangeDateItem, api_key: str = Security(get_api_key)):
785
- print_log("INFO",f"Processing date change request, new date: {request.date}",
786
- extra={"user_id": request.user_id, "endpoint": "/change_date"})
787
- logger.info(f"Processing date change request, new date: {request.date}",
788
- extra={"user_id": request.user_id, "endpoint": "/change_date"})
 
 
 
 
 
789
  try:
790
- user_id = request.user_id
 
 
 
 
 
 
 
 
 
 
 
791
 
792
- user = get_user(user_id)
793
- logger.info(f"User: {user}", extra={"user_id": user_id, "endpoint": "/change_date"})
794
 
795
- # infer follow_up dates
796
- user.infer_memento_follow_ups()
 
 
 
 
 
 
 
797
 
798
- # Push users mementos to DB
799
- try:
800
- upload = upload_mementos_to_db(user_id)
801
- if upload:
802
- print_log("INFO",f"Uploaded mementos to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
803
- logger.info(f"Uploaded mementos to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
804
- else:
805
- print_log("ERROR",f"Failed to upload mementos to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
806
- logger.error(f"Failed to upload mementos to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
807
- raise HTTPException(
808
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
809
- detail=f"Failed to upload mementos to DB for user: {user_id}"
810
- )
811
- except ConnectionError as e:
812
- print_log("ERROR",f"Failed to connect to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
813
- logger.error(f"Failed to connect to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
814
- raise HTTPException(
815
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
816
- detail=f"Failed to connect to DB for user: {user_id}"
817
- )
818
-
819
- response = user.change_date(request.date)
820
- response['user_id'] = user_id
821
-
822
- print_log("INFO",f"Date changed successfully for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
823
- logger.info(f"Date changed successfully for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
824
- print_log("DEBUG",f"Change date response: {response}", extra={"user_id": user_id, "endpoint": "/change_date"})
825
- logger.debug(f"Change date response: {response}", extra={"user_id": user_id, "endpoint": "/change_date"})
826
-
827
- # Update user
828
- add_to_cache(user)
829
- update = pop_cache(user.user_id)
830
- if not update:
831
- print_log("ERROR",f"Failed to update user pickle in S3: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
832
- logger.error(f"Failed to update user pickle in S3: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
833
- raise HTTPException(
834
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
835
- detail=f"Failed to update user pickle in S3 {user_id}"
836
- )
837
-
838
- return response
839
- # return {"response": response}
840
-
841
- except ValueError as e:
842
- print_log("ERROR",f"Invalid date format for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/change_date"})
843
- logger.error(f"Invalid date format for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/change_date"})
844
- raise HTTPException(
845
- status_code=status.HTTP_400_BAD_REQUEST,
846
- detail=str(e)
847
- )
848
- except LookupError:
849
- print_log("ERROR",f"User not found for date change: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/change_date"})
850
- logger.error(f"User not found for date change: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/change_date"})
851
- raise HTTPException(
852
- status_code=status.HTTP_404_NOT_FOUND,
853
- detail=f"User with ID {request.user_id} not found"
854
- )
855
- except Exception as e:
856
- print_log("ERROR",f"Error changing date for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/change_date"}, exc_info=True)
857
- logger.error(f"Error changing date for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/change_date"}, exc_info=True)
858
- raise HTTPException(
859
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
860
- detail=str(e)
861
- )
862
-
863
  @app.post("/reset_user_messages")
864
- def reset_user_messages(request: CreateUserItem, api_key: str = Security(get_api_key)):
 
 
 
 
865
  print_log("INFO","Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
866
  logger.info("Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
867
- try:
868
- user = get_user(request.user_id)
869
- user.reset_conversations()
870
- print_log("INFO",f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
871
- logger.info(f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
872
-
873
- add_to_cache(user)
874
- update = pop_cache(user.user_id)
875
-
876
- print_log("INFO",f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
877
- logger.info(f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
878
-
879
- return {"response": "ok"}
880
- except LookupError:
881
- print_log("ERROR",f"User not found for reset: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
882
- logger.error(f"User not found for reset: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
883
- raise HTTPException(
884
- status_code=status.HTTP_404_NOT_FOUND,
885
- detail=f"User with ID {request.user_id} not found"
886
- )
887
- except Exception as e:
888
- print_log("ERROR",f"Error resetting user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/reset_user"}, exc_info=True)
889
- logger.error(f"Error resetting user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/reset_user"}, exc_info=True)
890
- raise HTTPException(
891
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
892
- detail=str(e)
893
- )
894
 
895
  @app.get("/get_logs")
896
- def get_logs(user_id: str = Query(default="", description="User ID to fetch logs for")):
 
 
 
897
  if (user_id):
898
  log_file_path = os.path.join('logs', 'users', f'{user_id}.log')
899
  if not os.path.exists(log_file_path):
@@ -918,201 +853,164 @@ def get_logs(user_id: str = Query(default="", description="User ID to fetch logs
918
  )
919
 
920
  @app.get("/is_user_responsive")
921
- def is_user_responsive(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
922
  logger.info("Checking if user is responsive", extra={"user_id": user_id, "endpoint": "/is_user_responsive"})
923
- try:
924
- user = get_user(user_id)
925
- messages = user.get_messages()
926
- if len(messages) >= 3 and messages[-1]['role'] == 'assistant' and messages[-2]['role'] == 'assistant':
927
- return {"response": False}
928
- else:
929
- return {"response": True}
930
- except LookupError:
931
- logger.error(f"User not found: {user_id}", extra={"user_id": user_id, "endpoint": "/is_user_responsive"})
932
- raise HTTPException(
933
- status_code=status.HTTP_404_NOT_FOUND,
934
- detail=f"User with ID {user_id} not found"
935
- )
936
- except Exception as e:
937
- logger.error(f"Error checking user responsiveness: {str(e)}", extra={"user_id": user_id, "endpoint": "/is_user_responsive"}, exc_info=True)
938
- raise HTTPException(
939
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
940
- detail=str(e)
941
- )
942
 
943
  @app.get("/get_user_summary")
944
- def get_summary_by_id(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
945
  print_log("INFO", "Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
946
  logger.info("Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
947
- try:
948
- user_summary = get_user_summary(user_id)
949
- print_log("INFO", "Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
950
- logger.info("Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
951
- return user_summary
952
- except LookupError:
953
- print_log("ERROR", "User not found", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
954
- logger.error("User not found", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
955
- raise HTTPException(
956
- status_code=status.HTTP_404_NOT_FOUND,
957
- detail=f"User with ID {user_id} not found"
958
- )
959
- except Exception as e:
960
- print_log("ERROR",f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user_summary"}, exc_info=True)
961
- logger.error(f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user_summary"}, exc_info=True)
962
- raise HTTPException(
963
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
964
- detail=str(e)
965
- )
966
 
967
  @app.get("/get_life_status")
968
- def get_life_status_by_id(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
969
  print_log("INFO", "Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
970
  logger.info("Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
971
- try:
972
- life_status = get_user_life_status(user_id)
973
- print_log("INFO", "Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
974
- logger.info("Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
975
- return life_status
976
- except LookupError:
977
- print_log("ERROR", "User not found", extra={"user_id": user_id, "endpoint": "/get_life_status"})
978
- logger.error("User not found", extra={"user_id": user_id, "endpoint": "/get_life_status"})
979
- raise HTTPException(
980
- status_code=status.HTTP_404_NOT_FOUND,
981
- detail=f"User with ID {user_id} not found"
982
- )
983
- except Exception as e:
984
- print_log("ERROR",f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_life_status"}, exc_info=True)
985
- logger.error(f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_life_status"}, exc_info=True)
986
- raise HTTPException(
987
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
988
- detail=str(e)
989
- )
990
 
991
  @app.post("/add_booking_point")
992
- def add_booking_point_by_user(user_id: str, api_key: str = Security(get_api_key)):
993
- try:
994
- user = get_user(user_id)
995
- user.add_point_for_booking()
996
- return {"response": "ok"}
997
- except Exception as e:
998
- print_log("ERROR",f"Error: {str(e)}", extra={"user_id": user_id, "endpoint": "/add_booking_point"}, exc_info=True)
999
- logger.error(f"Error: {str(e)}", extra={"user_id": user_id, "endpoint": "/add_booking_point"}, exc_info=True)
1000
- raise HTTPException(
1001
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1002
- detail=str(e)
1003
- )
1004
 
1005
  @app.post("/add_session_completion_point")
1006
- def add_session_completion_point_by_user(user_id: str, api_key: str = Security(get_api_key)):
1007
- try:
1008
- user = get_user(user_id)
1009
- user.add_point_for_completing_session()
1010
- return {"response": "ok"}
1011
- except Exception as e:
1012
- print_log("ERROR",f"Error: {str(e)}", extra={"user_id": user_id, "endpoint": "/add_session_completion_point"}, exc_info=True)
1013
- logger.error(f"Error: {str(e)}", extra={"user_id": user_id, "endpoint": "/add_session_completion_point"}, exc_info=True)
1014
- raise HTTPException(
1015
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1016
- detail=str(e)
1017
- )
1018
 
1019
  @app.post("/create_pre_gg_report")
1020
- def create_pre_gg_by_booking(request: BookingItem, api_key: str = Security(get_api_key)):
1021
- try:
1022
- create_pre_gg_report(request.booking_id)
1023
- return {"response": "ok"}
1024
- except Exception as e:
1025
- print_log("ERROR",f"Error: {str(e)}", extra={"booking_id": request.booking_id, "endpoint": "/create_pre_gg_report"}, exc_info=True)
1026
- logger.error(f"Error: {str(e)}", extra={"booking_id": request.booking_id, "endpoint": "/create_pre_gg_report"}, exc_info=True)
1027
- raise HTTPException(
1028
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1029
- detail=str(e)
1030
- )
1031
 
1032
  @app.get("/get_user_persona")
1033
- def get_user_persona(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
1034
  """Get user's legendary persona from the database"""
1035
  logger.info("Getting user's persona", extra={"user_id": user_id, "endpoint": "/get_user_persona"})
1036
 
1037
- try:
1038
- # Connect to database
1039
- db_params = {
1040
- 'dbname': 'ourcoach',
1041
- 'user': 'ourcoach',
1042
- 'password': 'hvcTL3kN3pOG5KteT17T',
1043
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
1044
- 'port': '5432'
1045
- }
1046
- conn = psycopg2.connect(**db_params)
1047
- cur = conn.cursor()
1048
-
1049
- # Get onboarding data
1050
- cur.execute("SELECT onboarding FROM users WHERE id = %s", (user_id,))
1051
- result = cur.fetchone()
1052
- if not result:
1053
- raise HTTPException(status_code=404, detail="User not found")
1054
-
1055
- # Extract persona from onboarding JSON
1056
- onboarding = json.loads(result[0])
1057
- persona = onboarding.get('legendPersona', '')
 
 
 
 
 
 
 
1058
 
1059
- return {"persona": persona}
1060
 
1061
- except Exception as e:
1062
- logger.error(f"Error getting user persona: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user_persona"})
1063
- raise HTTPException(status_code=500, detail=str(e))
1064
-
1065
- finally:
1066
- if 'cur' in locals():
1067
- cur.close()
1068
- if 'conn' in locals():
1069
- conn.close()
1070
 
1071
  @app.get("/get_recent_booking")
1072
- def get_recent_booking(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
1073
  """Get the most recent booking ID for a user"""
1074
  logger.info("Getting recent booking", extra={"user_id": user_id, "endpoint": "/get_recent_booking"})
1075
 
1076
- try:
1077
- # Connect to database
1078
- db_params = {
1079
- 'dbname': 'ourcoach',
1080
- 'user': 'ourcoach',
1081
- 'password': 'hvcTL3kN3pOG5KteT17T',
1082
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
1083
- 'port': '5432'
1084
- }
1085
- conn = psycopg2.connect(**db_params)
1086
- cur = conn.cursor()
1087
-
1088
- # Get most recent booking where status == 2
1089
- cur.execute("""
1090
- SELECT booking_id
1091
- FROM public.user_notes
1092
- WHERE user_id = %s
1093
- ORDER BY created_at DESC
1094
- LIMIT 1
1095
- """, (user_id,))
1096
- result = cur.fetchone()
1097
-
1098
- if not result:
1099
- raise HTTPException(
1100
- status_code=status.HTTP_404_NOT_FOUND,
1101
- detail="No bookings found for user"
1102
- )
1103
-
1104
- booking_id = result[0]
1105
- logger.info(f"Found recent booking: {booking_id}", extra={"user_id": user_id, "endpoint": "/get_recent_booking"})
1106
- return {"booking_id": booking_id}
1107
-
1108
- except HTTPException:
1109
- raise
1110
- except Exception as e:
1111
- logger.error(f"Error getting recent booking: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_recent_booking"})
1112
- raise HTTPException(status_code=500, detail=str(e))
1113
 
1114
- finally:
1115
- if 'cur' in locals():
1116
- cur.close()
1117
- if 'conn' in locals():
1118
- conn.close()
 
 
 
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
5
  from pydantic import BaseModel
 
11
  from datetime import datetime, timezone
12
  from app.user import User
13
  from typing import List, Optional, Callable
14
+ from functools import wraps
15
+
16
  from openai import OpenAI
17
  import psycopg2
18
  from psycopg2 import sql
19
  import os
20
+ from app.utils import add_to_cache, download_file_from_s3, get_api_key, get_user_info, get_growth_guide_session, pop_cache, print_log, get_user, upload_mementos_to_db, get_user_summary, get_user_life_status, create_pre_gg_report
21
  from dotenv import load_dotenv
22
  import logging.config
23
  import time
 
25
  import sys
26
  import boto3
27
  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')
34
  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
 
53
  # Create required folders
54
  os.makedirs('logs', exist_ok=True)
 
275
  user_id: str
276
  message: str
277
 
 
 
 
 
 
 
 
 
278
  class PersonaItem(BaseModel):
279
  user_id: str
280
  persona: str
281
 
282
  class GGItem(BaseModel):
283
+ user_id: str
284
  gg_session_id: str
285
+
286
+ class AssistantItem(BaseModel):
287
  user_id: str
288
+ assistant_id: str
289
 
290
+ class ChangeDateItem(BaseModel):
291
+ user_id: str
292
+ date: str
 
 
293
 
294
  class BookingItem(BaseModel):
295
  booking_id: str
296
 
297
+ def catch_endpoint_error(func):
298
+ """Decorator to handle errors in FastAPI endpoints"""
299
+ @wraps(func) # Add this to preserve endpoint metadata
300
+ async def wrapper(*args, **kwargs):
301
+ try:
302
+ # Extract api_key from kwargs if present and pass it to the wrapped function
303
+ api_key = kwargs.pop('api_key', None)
304
+ return await func(*args, **kwargs)
305
+ except OpenAIRequestError as e:
306
+ # OpenAI service error
307
+ # Try to cancel the run so we dont get "Cannot add message to thread with active run"
308
+ # if e.run_id:
309
+ # user_id = e.user_id
310
+ # if user_id != 'no-user':
311
+ # user = get_user(user_id)
312
+ # user.cancel_run(e.run_id)
313
+ logger.error(f"OpenAI service error in {func.__name__}(...): {str(e)}",
314
+ extra={
315
+ 'user_id': e.user_id,
316
+ 'endpoint': func.__name__
317
+ })
318
+ # Extract thread_id and run_id from error message
319
+ thread_match = re.search(r'thread_(\w+)', str(e))
320
+ run_match = re.search(r'run_(\w+)', str(e))
321
+ if thread_match and run_match:
322
+ thread_id = f"thread_{thread_match.group(1)}"
323
+ run_id = f"run_{run_match.group(1)}"
324
+ user = get_user(e.user_id)
325
+ logger.info(f"Cancelling run {run_id} for thread {thread_id}", extra={"user_id": e.user_id, "endpoint": func.__name__})
326
+ user.cancel_run(run_id, thread_id)
327
+ logger.info(f"Run {run_id} cancelled for thread {thread_id}", extra={"user_id": e.user_id, "endpoint": func.__name__})
328
 
329
+ raise HTTPException(
330
+ status_code=status.HTTP_502_BAD_GATEWAY,
331
+ detail=e.get_formatted_details()
332
+ )
333
+ except DBError as e:
334
+ # check if code is one of ["NoOnboardingError", "NoBookingError"] if yes then return code 404 otherwise 500
335
+ if e.code == "NoOnboardingError" or e.code == "NoBookingError":
336
+ # no onboarding or booking data (user not found)
337
+ status_code = 404
338
+ else:
339
+ status_code = 505
340
+ logger.error(f"Database error in {func.__name__}: {str(e)}",
341
+ extra={
342
+ 'user_id': e.user_id,
343
+ 'endpoint': func.__name__
344
+ })
345
+ raise HTTPException(
346
+ status_code=status_code,
347
+ detail=e.get_formatted_details()
348
+ )
349
+ except (UserError, AssistantError, ConversationManagerError, UtilsError) as e:
350
+ # Known internal errors
351
+ logger.error(f"Internal error in {func.__name__}: {str(e)}",
352
+ extra={
353
+ 'user_id': e.user_id,
354
+ 'endpoint': func.__name__,
355
+ 'traceback': traceback.extract_stack()
356
+ })
357
+ raise HTTPException(
358
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
359
+ # detail = traceback.extract_stack()
360
+ detail=e.get_formatted_details()
361
+ )
362
+ except openai.BadRequestError as e:
363
+ # OpenAI request error
364
+ user_id = kwargs.get('user_id', 'no-user')
365
+ logger.error(f"OpenAI request error in {func.__name__}: {str(e)}",
366
+ extra={
367
+ 'user_id': user_id,
368
+ 'endpoint': func.__name__
369
+ })
370
+ raise HTTPException(
371
+ status_code=status.HTTP_400_BAD_REQUEST,
372
+ detail={
373
+ "type": "OpenAIError",
374
+ "message": str(e),
375
+ "user_id": user_id,
376
+ "at": datetime.now(timezone.utc).isoformat()
377
+ }
378
+ )
379
+ except Exception as e:
380
+ # Unknown errors
381
+ user_id = kwargs.get('user_id', 'no-user')
382
+ if len(args) and hasattr(args[0], 'user_id'):
383
+ user_id = args[0].user_id
384
+
385
+ logger.error(f"Unexpected error in {func.__name__}: {str(e)}",
386
+ extra={
387
+ 'user_id': user_id,
388
+ 'endpoint': func.__name__
389
+ })
390
+ raise HTTPException(
391
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
392
+ detail={
393
+ "type": "FastAPIError",
394
+ "message": str(e),
395
+ "user_id": user_id,
396
+ "at": datetime.now(timezone.utc).isoformat()
397
+ }
398
+ )
399
+ # raise FastAPIError(
400
+ # user_id=user_id,
401
+ # message=f"Unexpected error in {func.__name__}",
402
+ # e=str(e)
403
+ # )
404
+ return wrapper
405
+
406
+ # Apply decorator to all endpoints
407
  @app.post("/set_intro_done")
408
+ @catch_endpoint_error
409
+ async def set_intro_done(
410
+ user_id: str,
411
+ api_key: str = Depends(get_api_key) # Change Security to Depends
412
+ ):
413
  user = get_user(user_id)
414
+
415
  user.set_intro_done()
416
  logger.info("Intro done", extra={"user_id": user_id, "endpoint": "/set_intro_done"})
417
  return {"response": "ok"}
418
 
419
+
420
  @app.post("/set_goal")
421
+ @catch_endpoint_error
422
+ async def set_goal(
423
+ user_id: str,
424
+ goal: str,
425
+ api_key: str = Depends(get_api_key) # Change Security to Depends
426
+ ):
427
+ user = get_user(user_id)
428
+
429
  user.set_goal(goal)
430
  logger.info(f"Goal set: {goal}", extra={"user_id": user_id, "endpoint": "/set_goal"})
431
  return {"response": "ok"}
432
 
433
+ @app.post("/do_micro")
434
+ @catch_endpoint_error
435
+ async def do_micro(
436
+ request: ChangeDateItem,
437
+ day: int,
438
+ api_key: str = Depends(get_api_key) # Change Security to Depends
439
+ ):
440
+ user = get_user(request.user_id)
 
441
  response = user.do_micro(request.date, day)
442
+ logger.info(f"Micro action completed", extra={"user_id": request.user_id, "endpoint": "/do_micro"})
443
+ return {"response": response}
444
+
 
 
 
 
 
 
 
 
 
 
 
 
445
  # endpoint to change user assistant using user.change_to_latest_assistant()
446
  @app.post("/change_assistant")
447
+ @catch_endpoint_error
448
+ async def change_assistant(
449
+ request: AssistantItem,
450
+ api_key: str = Depends(get_api_key) # Change Security to Depends
451
+ ):
452
+ user = get_user(request.user_id)
453
+
454
+ user.change_assistant(request.assistant_id)
455
+ logger.info(f"Assistant changed to {request.assistant_id}",
456
+ extra={"user_id": request.user_id, "endpoint": "/change_assistant"})
457
+ return {"assistant_id": request.assistant_id}
458
+
459
 
460
  @app.post("/clear_cache")
461
+ @catch_endpoint_error
462
+ async def clear_cache(
463
+ api_key: str = Depends(get_api_key) # Change Security to Depends
464
+ ):
465
+ pop_cache(user_id='all')
466
+ logger.info("Cache cleared successfully", extra={"endpoint": "/clear_cache"})
467
+ return {"response": "Cache cleared successfully"}
 
 
 
 
 
 
 
 
468
 
469
  @app.post("/migrate_user")
470
+ @catch_endpoint_error
471
+ async def migrate_user(
472
+ request: CreateUserItem,
473
+ api_key: str = Depends(get_api_key) # Change Security to Depends
474
+ ):
475
+ client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
476
+ if not client:
477
+ raise OpenAIRequestError(
478
+ user_id=request.user_id,
479
+ message="Failed to initialize OpenAI client"
480
+ )
481
+
482
+ user_file = os.path.join('users', 'data', f'{request.user_id}.pkl')
483
+
484
+ download_file_from_s3(f'{request.user_id}.pkl', 'core-ai-assets')
485
+
486
+ with open(user_file, 'rb') as f:
487
+ old_user_object = pickle.load(f)
488
+ user = User(request.user_id, old_user_object.user_info, client, GENERAL_ASSISTANT)
489
+ user.conversations.current_thread = old_user_object.conversations.current_thread
490
+ user.conversations.intro_done = True
491
+ user.done_first_reflection = old_user_object.done_first_reflection
492
+ user.client = client
493
+ user.conversations.client = client
494
+
495
+ api_response = {
496
+ "user": user.user_info,
497
+ "user_messages": user.get_messages(),
498
+ "general_assistant": user.conversations.assistants['general'].id,
499
+ "intro_assistant": user.conversations.assistants['intro'].id,
500
+ "goal": user.goal if user.goal else "No goal is not set yet",
501
+ "current_day": user.growth_plan.current()['day'],
502
+ "micro_actions": user.micro_actions,
503
+ "recommended_actions": user.recommended_micro_actions,
504
+ "challenges": user.challenges,
505
+ "other_focusses": user.other_focusses,
506
+ "scores": f"Personal Growth: {user.personal_growth_score} || Career: {user.career_growth_score} || Health/Wellness: {user.health_and_wellness_score} || Relationships: {user.relationship_score} || Mental Health: {user.mental_well_being_score}",
507
+ "recent_wins": user.recent_wins
508
+ }
509
+
510
+ add_to_cache(user)
511
+ pop_cache(user.user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
 
513
+ os.remove(user_file)
514
+ logger.info(f"User {user.user_id} loaded successfully from S3", extra={'user_id': user.user_id, 'endpoint': 'migrate_user'})
515
+ return api_response
516
+
517
  @app.get("/get_user")
518
+ @catch_endpoint_error
519
+ async def get_user_by_id(
520
+ user_id: str,
521
+ api_key: str = Depends(get_api_key) # Change Security to Depends
522
+ ):
523
  print_log("INFO", "Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
524
  logger.info("Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
525
+ user = get_user(user_id)
526
+ print_log("INFO", "Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
527
+ logger.info("Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
528
+ api_response = {"user": user.user_info, "user_messages": user.get_messages(), "general_assistant": user.conversations.assistants['general'].id, "intro_assistant": user.conversations.assistants['intro'].id}
529
+
530
+ if user.goal:
531
+ api_response["goal"] = user.goal
532
+ else:
533
+ api_response["goal"] = "No goal is not set yet"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
+ api_response["current_day"] = user.growth_plan.current()['day']
536
+ api_response['micro_actions'] = user.micro_actions
537
+ api_response['recommended_actions'] = user.recommended_micro_actions
538
+ api_response['challenges'] = user.challenges
539
+ api_response['other_focusses'] = user.other_focusses
540
+ api_response['reminders'] = user.reminders
541
+ api_response['scores'] = f"Personal Growth: {user.personal_growth_score} || Career: {user.career_growth_score} || Health/Wellness: {user.health_and_wellness_score} || Relationships: {user.relationship_score} || Mental Health: {user.mental_well_being_score}"
542
+ api_response['recent_wins'] = user.recent_wins
543
+ api_response['last_gg_session'] = user.last_gg_session
544
 
545
+ return api_response
546
 
547
  @app.post("/update_user_persona")
548
+ @catch_endpoint_error
549
  async def update_user_persona(
550
  request: PersonaItem,
551
+ api_key: str = Depends(get_api_key) # Change Security to Depends
552
  ):
553
  """Update user's legendary persona in the database"""
554
  user_id = request.user_id
 
558
  user.update_user_info(f"User's new Legendary Persona is: {persona}")
559
  logger.info(f"Updated persona to {persona}", extra={"user_id": user_id, "endpoint": "/update_user_persona"})
560
 
561
+ # Connect to database
562
+ db_params = {
563
+ 'dbname': 'ourcoach',
564
+ 'user': 'ourcoach',
565
+ 'password': 'hvcTL3kN3pOG5KteT17T',
566
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
567
+ 'port': '5432'
568
+ }
569
+ conn = psycopg2.connect(**db_params)
570
+ cur = conn.cursor()
571
+
572
+ # Get current onboarding data
573
+ cur.execute("SELECT onboarding FROM users WHERE id = %s", (user_id,))
574
+ result = cur.fetchone()
575
+ if not result:
576
+ raise DBError(
577
+ user_id=user_id,
578
+ code="NoOnboardingError",
579
+ message="User not found in database"
 
 
 
 
 
 
 
580
  )
 
581
 
582
+ # Update legendPersona in onboarding JSON
583
+ onboarding = json.loads(result[0])
584
+ onboarding['legendPersona'] = persona
 
585
 
586
+ # Update database
587
+ cur.execute(
588
+ "UPDATE users SET onboarding = %s WHERE id = %s",
589
+ (json.dumps(onboarding), user_id)
590
+ )
591
+ conn.commit()
592
+ if 'cur' in locals():
593
+ cur.close()
594
+ if 'conn' in locals():
595
+ conn.close()
596
+
597
+ return {"status": "success", "message": f"Updated persona to {persona}"}
598
 
599
  @app.post("/add_ai_message")
600
+ @catch_endpoint_error
601
+ async def add_ai_message(
602
+ request: ChatItem,
603
+ api_key: str = Depends(get_api_key) # Change Security to Depends
604
+ ):
605
  user_id = request.user_id
606
  message = request.message
607
  logger.info("Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
608
  print_log("INFO", "Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
609
+
610
+ user = get_user(user_id)
611
+ user.add_ai_message(message)
612
 
613
+ add_to_cache(user)
614
+ pop_cache(user.user_id)
615
+
616
+ print_log("INFO", "AI response added", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
617
+ return {"response": "ok"}
618
+
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
  @app.post("/schedule_gg_reminder")
621
+ @catch_endpoint_error
622
+ async def schedule_gg_reminder(
623
+ request: ChangeDateItem,
624
+ api_key: str = Depends(get_api_key) # Change Security to Depends
625
+ ):
626
  # session_id = request.gg_session_id
627
  user_id = request.user_id
628
  logger.info(f"Scheduling GG session reminder for {request.date}", extra={"user_id": user_id, "endpoint": "/schedule_gg_reminder"})
 
638
  return {"response": response}
639
 
640
  @app.post("/process_gg_session")
641
+ @catch_endpoint_error
642
+ async def process_gg_session(
643
+ request: GGItem,
644
+ api_key: str = Depends(get_api_key) # Change Security to Depends
645
+ ):
646
+ logger.info("Processing growth guide session", extra={"user_id": request.user_id, "endpoint": "/process_gg_session"})
 
 
 
 
 
647
 
648
+ user = get_user(request.user_id)
649
+ session_data = get_growth_guide_session(request.user_id, request.gg_session_id)
650
+ response = user.process_growth_guide_session(session_data, request.gg_session_id)
651
+ add_to_cache(user)
652
+ pop_cache(user.user_id)
653
  return {"response": response}
654
 
655
+
656
  @app.get("/user_daily_messages")
657
+ @catch_endpoint_error
658
+ async def get_daily_message(
659
+ user_id: str,
660
+ api_key: str = Depends(get_api_key) # Change Security to Depends
661
+ ):
662
  logger.info("Getting daily messages", extra={"user_id": user_id, "endpoint": "/user_daily_messages"})
663
  user = get_user(user_id)
664
  daily_messages = user.get_daily_messages()
665
  return {"response": daily_messages}
666
 
667
  @app.post("/batch_refresh_users")
668
+ @catch_endpoint_error
669
+ async def refresh_multiple_users(
670
+ user_ids: List[str],
671
+ api_key: str = Depends(get_api_key) # Change Security to Depends
672
+ ):
673
  logger.info("Refreshing multiple users", extra={"endpoint": "/batch_refresh_users"})
674
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
675
  failed_users = []
676
 
677
  for i,user_id in enumerate(user_ids):
678
+ old_user = get_user(user_id)
679
+ user = old_user.refresh(client)
680
+ add_to_cache(user)
681
+ pop_cache(user.user_id)
682
+ logger.info(f"Successfully refreshed user {i+1}/{len(user_ids)}", extra={"user_id": user_id, "endpoint": "/batch_refresh_users"})
 
 
 
 
683
 
684
  if failed_users:
685
  return {"status": "partial", "failed_users": failed_users}
686
  return {"status": "success", "failed_users": []}
687
 
688
  @app.post("/refresh_user")
689
+ @catch_endpoint_error
690
+ async def refresh_user(
691
+ request: CreateUserItem,
692
+ api_key: str = Depends(get_api_key) # Change Security to Depends
693
+ ):
694
  print_log("INFO","Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
695
  logger.info("Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
696
 
 
704
  return {"response": "ok"}
705
 
706
  @app.post("/create_user")
707
+ @catch_endpoint_error
708
+ async def create_user(
709
+ request: CreateUserItem,
710
+ api_key: str = Depends(get_api_key) # Change Security to Depends
711
+ ):
 
 
 
 
 
 
712
  logger.info("Creating new user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
 
 
713
 
714
+ client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
715
+ if not client:
716
+ raise OpenAIRequestError("client_init", "Failed to initialize OpenAI client")
 
 
 
 
 
 
 
 
 
 
 
 
 
717
 
718
+ if os.path.exists(f'users/data/{request.user_id}.pkl'):
719
+ return {"message": f"[OK] User already exists: {request.user_id}"}
720
+
721
+ user_info, _ = get_user_info(request.user_id)
722
+ user = User(request.user_id, user_info, client, GENERAL_ASSISTANT)
723
+ folder_path = os.path.join("mementos", "to_upload", request.user_id)
724
+ os.makedirs(folder_path, exist_ok=True)
725
 
726
+ add_to_cache(user)
727
+ pop_cache(request.user_id)
 
 
728
 
729
+ logger.info(f"Successfully created user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
730
+ return {"message": {"info": f"[OK] User created: {user}", "messages": user.get_messages()}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
 
732
  @app.post("/chat")
733
+ @catch_endpoint_error
734
+ async def chat(
735
+ request: ChatItem,
736
+ api_key: str = Depends(get_api_key) # Change Security to Depends
737
+ ):
738
  logger.info("Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
739
+ user = get_user(request.user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
 
741
+ response = user.send_message(request.message)
742
+ logger.info(f"Assistant response generated", extra={"user_id": request.user_id, "endpoint": "/chat"})
743
+ return {"response": response}
744
+
745
  @app.get("/reminders")
746
+ @catch_endpoint_error
747
+ async def get_reminders(
748
+ user_id: str,
749
+ date:str,
750
+ api_key: str = Depends(get_api_key) # Change Security to Depends
751
+ ):
752
  print_log("INFO","Getting reminders", extra={"user_id": user_id, "endpoint": "/reminders"})
753
  logger.info("Getting reminders", extra={"user_id": user_id, "endpoint": "/reminders"})
754
+
755
+ user = get_user(user_id)
756
+ reminders = user.get_reminders(date)
757
+ if len(reminders) == 0:
758
+ print_log("INFO",f"No reminders for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
759
+ logger.info(f"No reminders for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
760
+ reminders = None
761
+
762
+ print_log("INFO",f"Successfully retrieved reminders: {reminders}", extra={"user_id": user_id, "endpoint": "/reminders"})
763
+ logger.info(f"Successfully retrieved reminders: {reminders} for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
764
+ return {"reminders": reminders}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
 
766
  @app.post("/change_date")
767
+ @catch_endpoint_error
768
+ async def change_date(
769
+ request: ChangeDateItem,
770
+ api_key: str = Depends(get_api_key) # Change Security to Depends
771
+ ):
772
+ logger.info(f"Processing date change request", extra={"user_id": request.user_id, "endpoint": "/change_date"})
773
+
774
+ user = get_user(request.user_id)
775
+
776
+ # Validate date format
777
  try:
778
+ datetime.strptime(request.date, "%d-%m-%Y %a %H:%M:%S")
779
+ except ValueError:
780
+ # HF format is YYYY-MM-DD
781
+ try:
782
+ request.date = datetime.strptime(request.date, "%Y-%m-%d")
783
+ # convert to '%d-%m-%Y %a 10:00:00'
784
+ request.date = request.date.strftime("%d-%m-%Y %a 10:00:00")
785
+ except ValueError as e:
786
+ raise FastAPIError(
787
+ message="Invalid date format",
788
+ e=str(e)
789
+ )
790
 
791
+ # Upload mementos to DB
792
+ upload_mementos_to_db(request.user_id)
793
 
794
+ # Change date and get response
795
+ response = user.change_date(request.date)
796
+ response['user_id'] = request.user_id
797
+
798
+ # Update cache
799
+ add_to_cache(user)
800
+ pop_cache(user.user_id)
801
+
802
+ return response
803
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
  @app.post("/reset_user_messages")
805
+ @catch_endpoint_error
806
+ async def reset_user_messages(
807
+ request: CreateUserItem,
808
+ api_key: str = Depends(get_api_key) # Change Security to Depends
809
+ ):
810
  print_log("INFO","Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
811
  logger.info("Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
812
+
813
+ user = get_user(request.user_id)
814
+ user.reset_conversations()
815
+ print_log("INFO",f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
816
+ logger.info(f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
817
+
818
+ add_to_cache(user)
819
+ update = pop_cache(user.user_id)
820
+
821
+ print_log("INFO",f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
822
+ logger.info(f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
823
+
824
+ return {"response": "ok"}
825
+
 
 
 
 
 
 
 
 
 
 
 
 
 
826
 
827
  @app.get("/get_logs")
828
+ @catch_endpoint_error
829
+ async def get_logs(
830
+ user_id: str = Query(default="", description="User ID to fetch logs for")
831
+ ):
832
  if (user_id):
833
  log_file_path = os.path.join('logs', 'users', f'{user_id}.log')
834
  if not os.path.exists(log_file_path):
 
853
  )
854
 
855
  @app.get("/is_user_responsive")
856
+ @catch_endpoint_error
857
+ async def is_user_responsive(
858
+ user_id: str,
859
+ api_key: str = Depends(get_api_key) # Change Security to Depends
860
+ ):
861
  logger.info("Checking if user is responsive", extra={"user_id": user_id, "endpoint": "/is_user_responsive"})
862
+
863
+ user = get_user(user_id)
864
+ messages = user.get_messages()
865
+ if len(messages) >= 3 and messages[-1]['role'] == 'assistant' and messages[-2]['role'] == 'assistant':
866
+ return {"response": False}
867
+ else:
868
+ return {"response": True}
869
+
 
 
 
 
 
 
 
 
 
 
 
870
 
871
  @app.get("/get_user_summary")
872
+ @catch_endpoint_error
873
+ async def get_summary_by_id(
874
+ user_id: str,
875
+ api_key: str = Depends(get_api_key) # Change Security to Depends
876
+ ):
877
  print_log("INFO", "Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
878
  logger.info("Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
879
+ user_summary = get_user_summary(user_id)
880
+ print_log("INFO", "Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
881
+ logger.info("Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
882
+ return user_summary
883
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
 
885
  @app.get("/get_life_status")
886
+ @catch_endpoint_error
887
+ async def get_life_status_by_id(
888
+ user_id: str,
889
+ api_key: str = Depends(get_api_key) # Change Security to Depends
890
+ ):
891
  print_log("INFO", "Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
892
  logger.info("Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
893
+
894
+ life_status = get_user_life_status(user_id)
895
+ print_log("INFO", "Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
896
+ logger.info("Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
897
+ return life_status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
 
899
  @app.post("/add_booking_point")
900
+ @catch_endpoint_error
901
+ async def add_booking_point_by_user(
902
+ user_id: str,
903
+ api_key: str = Depends(get_api_key) # Change Security to Depends
904
+ ):
905
+ user = get_user(user_id)
906
+ user.add_point_for_booking()
907
+ return {"response": "ok"}
908
+
 
 
 
909
 
910
  @app.post("/add_session_completion_point")
911
+ @catch_endpoint_error
912
+ async def add_session_completion_point_by_user(
913
+ user_id: str,
914
+ api_key: str = Depends(get_api_key) # Change Security to Depends
915
+ ):
916
+ user = get_user(user_id)
917
+ user.add_point_for_completing_session()
918
+ return {"response": "ok"}
919
+
 
 
 
920
 
921
  @app.post("/create_pre_gg_report")
922
+ @catch_endpoint_error
923
+ async def create_pre_gg_by_booking(
924
+ request: BookingItem,
925
+ api_key: str = Depends(get_api_key) # Change Security to Depends
926
+ ):
927
+ create_pre_gg_report(request.booking_id)
928
+ return {"response": "ok"}
929
+
 
 
 
930
 
931
  @app.get("/get_user_persona")
932
+ @catch_endpoint_error
933
+ async def get_user_persona(
934
+ user_id: str,
935
+ api_key: str = Depends(get_api_key) # Change Security to Depends
936
+ ):
937
  """Get user's legendary persona from the database"""
938
  logger.info("Getting user's persona", extra={"user_id": user_id, "endpoint": "/get_user_persona"})
939
 
940
+ # Connect to database
941
+ db_params = {
942
+ 'dbname': 'ourcoach',
943
+ 'user': 'ourcoach',
944
+ 'password': 'hvcTL3kN3pOG5KteT17T',
945
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
946
+ 'port': '5432'
947
+ }
948
+ conn = psycopg2.connect(**db_params)
949
+ cur = conn.cursor()
950
+
951
+ # Get onboarding data
952
+ cur.execute("SELECT onboarding FROM users WHERE id = %s", (user_id,))
953
+ result = cur.fetchone()
954
+ if not result:
955
+ raise DBError(
956
+ user_id=user_id,
957
+ code="NoOnboardingError",
958
+ message="User not found in database"
959
+ )
960
+ # Extract persona from onboarding JSON
961
+ onboarding = json.loads(result[0])
962
+ persona = onboarding.get('legendPersona', '')
963
+
964
+ if 'cur' in locals():
965
+ cur.close()
966
+ if 'conn' in locals():
967
+ conn.close()
968
 
969
+ return {"persona": persona}
970
 
971
+
 
 
 
 
 
 
 
 
972
 
973
  @app.get("/get_recent_booking")
974
+ @catch_endpoint_error
975
+ async def get_recent_booking(
976
+ user_id: str,
977
+ api_key: str = Depends(get_api_key) # Change Security to Depends
978
+ ):
979
  """Get the most recent booking ID for a user"""
980
  logger.info("Getting recent booking", extra={"user_id": user_id, "endpoint": "/get_recent_booking"})
981
 
982
+ # Connect to database
983
+ db_params = {
984
+ 'dbname': 'ourcoach',
985
+ 'user': 'ourcoach',
986
+ 'password': 'hvcTL3kN3pOG5KteT17T',
987
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
988
+ 'port': '5432'
989
+ }
990
+ conn = psycopg2.connect(**db_params)
991
+ cur = conn.cursor()
992
+
993
+ # Get most recent booking where status == 2
994
+ cur.execute("""
995
+ SELECT booking_id
996
+ FROM public.user_notes
997
+ WHERE user_id = %s
998
+ ORDER BY created_at DESC
999
+ LIMIT 1
1000
+ """, (user_id,))
1001
+ result = cur.fetchone()
1002
+
1003
+ if not result:
1004
+ raise DBError(
1005
+ user_id=user_id,
1006
+ code="NoBookingError",
1007
+ message="No bookings found for user"
1008
+ )
 
 
 
 
 
 
 
 
 
 
1009
 
1010
+ booking_id = result[0]
1011
+ logger.info(f"Found recent booking: {booking_id}", extra={"user_id": user_id, "endpoint": "/get_recent_booking"})
1012
+ if 'cur' in locals():
1013
+ cur.close()
1014
+ if 'conn' in locals():
1015
+ conn.close()
1016
+ return {"booking_id": booking_id}
app/requirements.txt CHANGED
@@ -14,4 +14,5 @@ regex==2024.9.11
14
  Requests==2.32.3
15
  cachetools>=5.0.0
16
  pdfkit==1.0.0
17
- PyPDF2==3.0.1
 
 
14
  Requests==2.32.3
15
  cachetools>=5.0.0
16
  pdfkit==1.0.0
17
+ PyPDF2==3.0.1
18
+ sentry-sdk==2.19.2
app/user.py CHANGED
@@ -1,6 +1,7 @@
1
  import json
2
  import io
3
  import os
 
4
  import pandas as pd
5
  from datetime import datetime, timezone
6
  import json
@@ -11,7 +12,8 @@ import random
11
  import logging
12
  import psycopg2
13
  from psycopg2 import sql
14
- import random
 
15
 
16
  from app.flows import FINAL_SUMMARY_STATE, FINAL_SUMMARY_STATE, MICRO_ACTION_STATE, MOTIVATION_INSPIRATION_STATE, OPEN_DISCUSSION_STATE, POST_GG_STATE, PROGRESS_REFLECTION_STATE, PROGRESS_SUMMARY_STATE, EDUCATION_STATE, FOLLUP_ACTION_STATE, FUNFACT_STATE
17
  from pydantic import BaseModel
@@ -45,261 +47,21 @@ logger = logging.getLogger(__name__)
45
  def get_current_datetime():
46
  return datetime.now(timezone.utc)
47
 
48
- class ConversationManager:
49
- def __init__(self, client, user, asst_id, intro_done=False):
50
- self.user = user
51
- self.intro_done = intro_done
52
- self.assistants = {'general': Assistant('asst_vnucWWELJlCWadfAARwyKkCW', self), 'intro': Assistant('asst_baczEK65KKvPWIUONSzdYH8j', self)}
53
-
54
- self.client = client
55
- self.state = {'date': pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S")}
56
-
57
- self.current_thread = self.create_thread()
58
- self.daily_thread = None
59
-
60
- logger.info("Initializing conversation state", extra={"user_id": self.user.user_id, "endpoint": "conversation_init"})
61
-
62
- def __getstate__(self):
63
- state = self.__dict__.copy()
64
- # Remove unpicklable or unnecessary attributes
65
- if 'client' in state:
66
- del state['client']
67
- return state
68
-
69
- def __setstate__(self, state):
70
- self.__dict__.update(state)
71
- # Re-initialize attributes that were not pickled
72
- self.client = None
73
-
74
- def create_thread(self):
75
- user_interaction_guidelines =self.user.user_interaction_guidelines
76
- thread = self.client.beta.threads.create()
77
- self.system_message = self.add_message_to_thread(thread.id, "assistant",
78
- f"""
79
- You are coaching:
80
- \n\n{user_interaction_guidelines}\n\n\
81
- Be mindful of this information at all times in order to
82
- be as personalised as possible when conversing. Ensure to
83
- follow the conversation guidelines and flow templates. Use the
84
- current state of the conversation to adhere to the flow. Do not let the user know about any transitions.\n\n
85
- ** Today is {self.state['date']}.\n\n **
86
- ** You are now in the INTRODUCTION STATE. **
87
- """)
88
- return thread
89
-
90
- def _get_current_thread_history(self, remove_system_message=True, _msg=None, thread=None):
91
- if thread is None:
92
- thread = self.current_thread
93
- if not remove_system_message:
94
- return [{"role": msg.role, "content": msg.content[0].text.value} for msg in self.client.beta.threads.messages.list(thread.id, order="asc")]
95
- if _msg:
96
- return [{"role": msg.role, "content": msg.content[0].text.value} for msg in self.client.beta.threads.messages.list(thread.id, order="asc", after=_msg.id)][1:]
97
- return [{"role": msg.role, "content": msg.content[0].text.value} for msg in self.client.beta.threads.messages.list(thread.id, order="asc")][1:] # remove the system message
98
-
99
- def add_message_to_thread(self, thread_id, role, content):
100
- message = self.client.beta.threads.messages.create(
101
- thread_id=thread_id,
102
- role=role,
103
- content=content
104
- )
105
- return message
106
-
107
- def _run_current_thread(self, text, thread=None, hidden=False):
108
- if thread is None:
109
- thread = self.current_thread
110
- logger.warning(f"{self}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
111
- logger.info(f"User Message: {text}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
112
-
113
- # need to select assistant
114
- if self.intro_done:
115
- logger.info(f"Running general assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
116
- run, just_finished_intro, message = self.assistants['general'].process(thread, text)
117
- else:
118
- logger.info(f"Running intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
119
- run, just_finished_intro, message = self.assistants['intro'].process(thread, text)
120
-
121
-
122
- if run == 'cancelled':
123
- self.intro_done = True
124
- logger.info(f"Run was cancelled", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
125
- return None, {"message": "cancelled"}
126
- elif run == 'change_goal':
127
- self.intro_done = False
128
- logger.info(f"Changing goal, reset to intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
129
- return None, {"message": "change_goal"}
130
- else:
131
- status = run.status
132
- logger.info(f"Run {run.id} {status} just finished intro: {just_finished_intro}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
133
-
134
- if hidden:
135
- self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
136
- logger.info(f"Deleted hidden message: {message}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
137
-
138
- if just_finished_intro:
139
- self.intro_done = True
140
- logger.info(f"Intro done", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
141
- return self._get_current_thread_history(remove_system_message=False)[-1], {"message": "intro_done"}
142
-
143
- # NOTE: this is a hack, should get the response straight from the run
144
- return self._get_current_thread_history(remove_system_message=False)[-1], {"message": "coach_response"}
145
-
146
- def _send_and_replace_message(self, text, replacement_msg=None):
147
- logger.info(f"Sending hidden message: {text}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
148
- response, _ = self._run_current_thread(text, hidden=True)
149
-
150
- # check if there is a replacement message
151
- if replacement_msg:
152
- logger.info(f"Adding replacement message: {replacement_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
153
- # get the last message
154
- last_msg = list(self.client.beta.threads.messages.list(self.current_thread.id, order="asc"))[-1]
155
- logger.info(f"Last message: {last_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
156
- response = last_msg.content[0].text.value
157
-
158
- # delete the last message
159
- self.client.beta.threads.messages.delete(message_id=last_msg.id, thread_id=self.current_thread.id)
160
- self.add_message_to_thread(self.current_thread.id, "user", replacement_msg)
161
- self.add_message_to_thread(self.current_thread.id, "assistant", response)
162
-
163
- logger.info(f"Hidden message response: {response}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
164
- # NOTE: this is a hack, should get the response straight from the run
165
- return {'content': response, 'role': 'assistant'}
166
-
167
- def _add_ai_message(self, text):
168
- return self.add_message_to_thread(self.current_thread.id, "assistant", text)
169
-
170
- def get_daily_thread(self):
171
- if self.daily_thread is None:
172
- messages = self._get_current_thread_history(remove_system_message=False)
173
-
174
- self.daily_thread = self.client.beta.threads.create(
175
- messages=messages[:30]
176
- )
177
-
178
- # Add remaining messages one by one if there are more than 30
179
- for msg in messages[30:]:
180
- self.add_message_to_thread(
181
- self.daily_thread.id,
182
- msg['role'],
183
- msg['content']
184
- )
185
- self.last_daily_message = list(self.client.beta.threads.messages.list(self.daily_thread.id, order="asc"))[-1]
186
- else:
187
- messages = self._get_current_thread_history(remove_system_message=False, _msg=self.last_daily_message)
188
- self.client.beta.threads.delete(self.daily_thread.id)
189
- self.daily_thread = self.client.beta.threads.create(messages=messages)
190
- self.last_daily_message = list(self.client.beta.threads.messages.list(self.daily_thread.id, order="asc"))[-1]
191
- logger.info(f"Daily Thread: {self._get_current_thread_history(thread=self.daily_thread)}", extra={"user_id": self.user.user_id, "endpoint": "send_morning_message"})
192
- logger.info(f"Last Daily Message: {self.last_daily_message}", extra={"user_id": self.user.user_id, "endpoint": "send_morning_message"})
193
- return self._get_current_thread_history(thread=self.daily_thread)
194
- # [{"role":, "content":}, ....]
195
-
196
- def _send_morning_message(self, text, add_to_main=False):
197
- # create a new thread
198
- # OPENAI LIMITATION: Can only attach a maximum of 32 messages when creating a new thread
199
- messages = self._get_current_thread_history(remove_system_message=False)
200
- if len(messages) >= 29:
201
- messages = [{"content": """ Remember who you are coaching.
202
- Be mindful of this information at all times in order to
203
- be as personalised as possible when conversing. Ensure to
204
- follow the conversation guidelines and flow provided.""", "role":"assistant"}] + messages[-29:]
205
- logger.info(f"Current Thread Messages: {messages}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
206
-
207
- temp_thread = self.client.beta.threads.create(messages=messages)
208
- logger.info(f"Created Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
209
-
210
- if add_to_main:
211
- logger.info(f"Adding message to main thread: {text}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
212
- self.add_message_to_thread(self.current_thread.id, "assistant", text)
213
-
214
- self.add_message_to_thread(temp_thread.id, "user", text)
215
-
216
- self._run_current_thread(text, thread=temp_thread)
217
- response = self._get_current_thread_history(thread=temp_thread)[-1]
218
- logger.info(f"Hidden Response: {response}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
219
-
220
- # delete temp thread
221
- self.client.beta.threads.delete(temp_thread.id)
222
- logger.info(f"Deleted Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
223
-
224
- return response
225
-
226
- def delete_hidden_messages(self, old_thread=None):
227
- if old_thread is None:
228
- old_thread = self.current_thread
229
-
230
- # create a new thread
231
- messages = [msg for msg in self._get_current_thread_history(remove_system_message=False) if not msg['content'].startswith("[hidden]")]
232
- if len(messages) >= 29:
233
- messages = messages[-29:]
234
- logger.info(f"Current Thread Messages: {messages}", extra={"user_id": self.user.user_id, "endpoint": "delete_hidden_messages"})
235
-
236
- new_thread = self.client.beta.threads.create(messages=messages)
237
-
238
- # delete old thread
239
- self.client.beta.threads.delete(old_thread.id)
240
-
241
- # set current thread
242
- self.current_thread = new_thread
243
-
244
-
245
- def do_first_reflection(self):
246
- question_format = random.choice(['[Option 1] Likert-Scale Objective Question','[Option 2] Multiple-Choice Question','[Option 3] Yes-No Question'])
247
-
248
- tt = f"** Today's reflection topic is the user's most important area. **"
249
- prompt = PROGRESS_REFLECTION_STATE + f"** Start the PROGRESS_REFLECTION_STATE flow **" + tt
250
- logger.info(f"First reflection started", extra={"user_id": self.user.user_id, "endpoint": "do_first_reflection"})
251
- response, _ = self._run_current_thread(prompt)
252
-
253
- return response
254
-
255
- def cancel_run(self, run):
256
- cancel = self.assistants['general'].cancel_run(run, self.current_thread)
257
- if cancel:
258
- logger.info(f"Run cancelled", extra={"user_id": self.user.user_id, "endpoint": "cancel_run"})
259
- return True
260
-
261
- def clone(self, client):
262
- """Creates a new ConversationManager with copied thread messages."""
263
- # Create new instance with same init parameters
264
- new_cm = ConversationManager(
265
- client,
266
- self.user,
267
- self.assistants['general'].id,
268
- intro_done=True
269
- )
270
-
271
- # Get all messages from current thread
272
- messages = self._get_current_thread_history(remove_system_message=False)
273
-
274
- # Delete the automatically created thread from constructor
275
- new_cm.client.beta.threads.delete(new_cm.current_thread.id)
276
-
277
- # Create new thread with first 30 messages
278
- new_cm.current_thread = new_cm.client.beta.threads.create(
279
- messages=messages[:30]
280
- )
281
-
282
- # Add remaining messages one by one if there are more than 30
283
- for msg in messages[30:]:
284
- new_cm.add_message_to_thread(
285
- new_cm.current_thread.id,
286
- msg['role'],
287
- msg['content']
288
- )
289
-
290
- # Copy other relevant state
291
- new_cm.state = self.state
292
-
293
- return new_cm
294
-
295
- def __str__(self):
296
- return f"ConversationManager(intro_done={self.intro_done}, assistants={self.assistants}, current_thread={self.current_thread})"
297
-
298
- def __repr__(self):
299
- return (f"ConversationManager("
300
- f"intro_done={self.intro_done}, current_thread={self.current_thread})")
301
-
302
  class User:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  def __init__(self, user_id, user_info, client, asst_id):
304
  self.user_id = user_id
305
  self.client = client
@@ -319,6 +81,7 @@ class User:
319
  self.health_and_wellness_score = 0
320
  self.reminders = None
321
  self.recent_wins = []
 
322
 
323
  # Read growth_plan.json and store it
324
 
@@ -354,81 +117,79 @@ class User:
354
  self.user_interaction_guidelines = self.generate_user_interaction_guidelines(user_info, client)
355
  self.conversations = ConversationManager(client, self, asst_id)
356
 
 
357
  def extend_growth_plan(self):
358
  # Change current growth plan to 14d growth plan
359
  logger.info(f"Changing plan to 14d...", extra={"user_id": self.user_id, "endpoint": "extend_growth_plan"})
360
- try:
361
- new_growth_plan = {"growthPlan": [
362
- {
363
- "day": 1,
364
- "coachingTheme": "MICRO_ACTION_STATE"
365
- },
366
- {
367
- "day": 2,
368
- "coachingTheme": "FOLLUP_ACTION_STATE"
369
- },
370
- {
371
- "day": 3,
372
- "coachingTheme": "OPEN_DISCUSSION_STATE"
373
- },
374
- {
375
- "day": 4,
376
- "coachingTheme": "MICRO_ACTION_STATE"
377
- },
378
- {
379
- "day": 5,
380
- "coachingTheme": "FOLLUP_ACTION_STATE"
381
- },
382
- {
383
- "day": 6,
384
- "coachingTheme": "FUNFACT_STATE"
385
- },
386
- {
387
- "day": 7,
388
- "coachingTheme": "PROGRESS_REFLECTION_STATE"
389
- },
390
- {
391
- "day": 8,
392
- "coachingTheme": "MICRO_ACTION_STATE"
393
- },
394
- {
395
- "day": 9,
396
- "coachingTheme": "FOLLUP_ACTION_STATE"
397
- },
398
- {
399
- "day": 10,
400
- "coachingTheme": "OPEN_DISCUSSION_STATE"
401
- },
402
- {
403
- "day": 11,
404
- "coachingTheme": "MICRO_ACTION_STATE"
405
- },
406
- {
407
- "day": 12,
408
- "coachingTheme": "FOLLUP_ACTION_STATE"
409
- },
410
- {
411
- "day": 13,
412
- "coachingTheme": "FUNFACT_STATE"
413
- },
414
- {
415
- "day": 14,
416
- "coachingTheme": "FINAL_SUMMARY_STATE"
417
- }
418
- ]
419
- }
420
- self.growth_plan = CircularQueue(array=new_growth_plan['growthPlan'], user_id=self.user_id)
421
- logger.info(f"User Growth Plan: {self.growth_plan} (Day: {self.growth_plan.current()['day']}/{len(self.growth_plan.array)})", extra={"user_id": self.user_id, "endpoint": "user_init"})
422
- logger.info(f"Success.", extra={"user_id": self.user_id, "endpoint": "extend_growth_plan"})
423
- except Exception as e:
424
- logger.error(f"Error occured when changing plan: {e}", extra={"user_id": self.user_id, "endpoint": "extend_growth_plan"})
425
- raise
426
-
427
-
428
  def add_recent_wins(self, wins, context = None):
429
  prompt = f"""
430
  ## Role
431
- You are an expert in writing achievement message and progress notification. Your task is to use the user's achievement and context to formulate a short achievement message/progress notification. The output must be a one sentence short message (less than 15 words) in this JSON output schema:
432
 
433
  ```json
434
  {{
@@ -445,7 +206,7 @@ class User:
445
  Output:
446
  ```
447
  {{
448
- achievement_message: You have completed a 10k run!
449
  }}
450
  ```
451
 
@@ -489,6 +250,7 @@ class User:
489
  self.recent_wins.pop()
490
  self.recent_wins.insert(0,achievement_message)
491
 
 
492
  def add_life_score_point(self, variable, points_added, notes):
493
  if variable == 'Personal Growth':
494
  self.personal_growth_score += points_added
@@ -505,7 +267,8 @@ class User:
505
  elif variable == 'Relationship':
506
  self.relationship_score += points_added
507
  logger.info(f"Added {points_added} points to Relationship for {notes}", extra={"user_id": self.user_id, "endpoint": "add_life_score_point"})
508
-
 
509
  def get_current_goal(self, full=False):
510
  # look for most recent goal with status = ONGOING
511
  for goal in self.goal[::-1]:
@@ -520,6 +283,7 @@ class User:
520
  return self.goal[-1].content
521
  return None
522
 
 
523
  def update_goal(self, goal, status, content=None):
524
  if goal is None:
525
  # complete the current goal
@@ -538,6 +302,7 @@ class User:
538
  return True
539
  return False
540
 
 
541
  def set_goal(self, goal, goal_area, add=True, completed=False):
542
  current_goal = self.get_current_goal()
543
 
@@ -563,6 +328,7 @@ class User:
563
  else:
564
  self.update_goal(current_goal, "ONGOING", content=goal)
565
 
 
566
  def update_recommended_micro_action_status(self, micro_action, status):
567
  for ma in self.recommended_micro_actions:
568
  if ma.content == micro_action:
@@ -571,10 +337,12 @@ class User:
571
  return True
572
  return False
573
 
 
574
  def add_ai_message(self, text):
575
  self.conversations._add_ai_message(text)
576
  return text
577
 
 
578
  def reset_conversations(self):
579
  self.conversations = ConversationManager(self.client, self, self.asst_id)
580
  self.growth_plan.reset()
@@ -591,8 +359,17 @@ class User:
591
  self.health_and_wellness_score = 0
592
  self.reminders = None
593
  self.recent_wins = []
 
594
 
 
 
 
 
 
 
 
595
 
 
596
  def generate_user_interaction_guidelines(self, user_info, client):
597
  logger.info(f"Generating user interaction guidelines for user: {self.user_id}", extra={"user_id": self.user_id, "endpoint": "generate_user_interaction_guidelines"})
598
  # prompt = f"A 'profile' is a document containing rich insights on users for the purpose of \
@@ -628,24 +405,31 @@ class User:
628
 
629
  return user_guideline
630
 
 
631
  def get_recent_run(self):
632
  return self.conversations.assistants['general'].recent_run
633
 
634
- def cancel_run(self, run):
635
- self.conversations.cancel_run(run)
 
 
636
 
 
637
  def update_conversation_state(self, stage, last_interaction):
638
  self.conversation_state['stage'] = stage
639
  self.conversation_state['last_interaction'] = last_interaction
640
 
 
641
  def _get_current_thread(self):
642
  return self.conversations.current_thread
643
 
 
644
  def send_message(self, text):
645
- response, info = self.conversations._run_current_thread(text)
646
- logger.info(f"Info: {info}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
 
647
 
648
- if info.get("message") == "cancelled":
649
  # must do current plan now
650
  action = self.growth_plan.current()
651
  logger.info(f"Current Action: {action}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
@@ -658,7 +442,7 @@ class User:
658
  # Move to the next action
659
  self.growth_plan.next()
660
 
661
- elif info.get("message") == "change_goal":
662
  # send the change goal prompt
663
  logger.info("Sending change goal message...", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
664
  prompt = f"""
@@ -695,6 +479,7 @@ class User:
695
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
696
  return response
697
 
 
698
  def get_reminders(self, date=None):
699
  if self.reminders is None:
700
  return []
@@ -710,6 +495,7 @@ class User:
710
  return [reminder for reminder in self.reminders if reminder['timestamp'].date() == date]
711
  return self.reminders
712
 
 
713
  def find_same_reminder(self, reminder_text):
714
  logger.info(f"Finding similar reminders: {self.reminders} to: {reminder_text}", extra={"user_id": self.user_id, "endpoint": "find_same_reminder"})
715
  response = self.client.beta.chat.completions.parse(
@@ -726,6 +512,7 @@ class User:
726
  logger.info(f"Similar reminder idx: reminders[{index}]", extra={"user_id": self.user_id, "endpoint": "find_same_reminder"})
727
  return index
728
 
 
729
  def set_reminder(self, reminder):
730
  db_params = {
731
  'dbname': 'ourcoach',
@@ -782,6 +569,7 @@ class User:
782
 
783
  logger.info(f"Reminders: {self.reminders}", extra={"user_id": self.user_id, "endpoint": "set_reminder"})
784
 
 
785
  def get_messages(self, exclude_system_msg=True, show_hidden=False):
786
  if not exclude_system_msg:
787
  return self.conversations._get_current_thread_history(False)
@@ -790,9 +578,15 @@ class User:
790
  return list(filter(lambda x: not (x['content'].startswith("** It is a new day:") or x['content'].startswith("Pay attention to the current state you are in") or x['content'].startswith("Date changed to")), self.conversations._get_current_thread_history(exclude_system_msg)))
791
  return list(filter(lambda x: not (x['content'].startswith("** It is a new day:") or x['content'].startswith("Pay attention to the current state you are in") or x['content'].startswith("Date changed to") or x['content'].startswith("[hidden]")), self.conversations._get_current_thread_history(exclude_system_msg)))
792
 
 
 
 
 
 
793
  def set_intro_done(self):
794
  self.conversations.intro_done = True
795
 
 
796
  def do_theme(self, theme, date, day):
797
  logger.info(f"Doing theme: {theme}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
798
 
@@ -863,6 +657,11 @@ class User:
863
 
864
  (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.)
865
 
 
 
 
 
 
866
  Additional System Instruction:
867
  ** Always remember to incorporate your personality (based on your persona) into all of your responses. **
868
  1. Focus on giving more wisdom than questions:
@@ -907,6 +706,7 @@ class User:
907
 
908
  return response, prompt
909
 
 
910
  def change_date(self, date):
911
  logger.info(f"Changing date from {self.conversations.state['date']} to {date}",
912
  extra={"user_id": self.user_id, "endpoint": "user_change_date"})
@@ -960,6 +760,7 @@ class User:
960
  logger.info(f"Date Updated: {self.conversations.state['date']}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
961
  return {'response': response, 'theme_prompt': '[hidden]'+prompt}
962
 
 
963
  def update_user_info(self, new_info):
964
  logger.info(f"Updating user info: [{self.user_info}] with: [{new_info}]", extra={"user_id": self.user_id, "endpoint": "update_user_info"})
965
  # make an api call to gpt4o to compare the current user_info and the new info and create a new consolidated user_info
@@ -988,6 +789,7 @@ class User:
988
  logger.info(f"Updated user info: {self.user_info}", extra={"user_id": self.user_id, "endpoint": "update_user_info"})
989
  return True
990
 
 
991
  def _summarize_zoom(self, zoom_ai_summary):
992
  logger.info(f"Summarizing zoom ai summary", extra={"user_id": self.user_id, "endpoint": "summarize_zoom"})
993
  # make an api call to gpt4o to summarize the zoom_ai_summary and produce a text with a focus on the most amount of user insight and info extracted
@@ -1005,6 +807,7 @@ class User:
1005
  logger.info(f"Summary: {response.choices[0].message.content}", extra={"user_id": self.user_id, "endpoint": "summarize_zoom"})
1006
  return {'overview': response.choices[0].message.content}
1007
 
 
1008
  def _update_user_data(self, data_type, text_input, extra_text=""):
1009
  data_mapping = {
1010
  'micro_actions': {
@@ -1041,32 +844,29 @@ class User:
1041
  f"Text:\n{text_input}"
1042
  )
1043
 
1044
- try:
1045
- current_time = datetime.now(timezone.utc).strftime("%d-%m-%Y %a %H:%M:%S")
1046
-
1047
- response = self.client.beta.chat.completions.parse(
1048
- model="gpt-4o",
1049
- messages=[{"role": "user", "content": prompt}],
1050
- response_format=UserDataResponse,
1051
- temperature=0.2
1052
- )
1053
-
1054
- data = getattr(response.choices[0].message.parsed, 'data')
1055
-
1056
- # Update the common fields for each item
1057
- for item in data:
1058
- item.role = "assistant"
1059
- item.user_id = self.user_id
1060
- item.status = mapping['status']
1061
- item.created_at = current_time
1062
- item.updated_at = current_time
1063
-
1064
- logger.info(f"Updated {data_type}: {data}", extra={"user_id": self.user_id, "endpoint": mapping['endpoint']})
1065
- getattr(self, mapping['attribute']).extend(data)
1066
-
1067
- except Exception as e:
1068
- logger.error(f"Failed to update {data_type}: {e}", extra={"user_id": self.user_id})
1069
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1070
  def update_user_data(self, gg_report):
1071
  self._update_user_data('micro_actions', gg_report[0]['answer'])
1072
 
@@ -1077,6 +877,7 @@ class User:
1077
 
1078
  self._update_goal(gg_report[4]['answer'])
1079
 
 
1080
  def _update_goal(self, goal_text):
1081
  prompt = f"""
1082
  The user has a current goal: {self.get_current_goal()}
@@ -1190,6 +991,7 @@ class User:
1190
  else:
1191
  logger.info(f"User goal remains unchanged.", extra={"user_id": self.user_id, "endpoint": "_update_goal"})
1192
 
 
1193
  def update_micro_action_status(self, completed_micro_action):
1194
  if completed_micro_action:
1195
  self.micro_actions[-1].status = "COMPLETE"
@@ -1201,25 +1003,30 @@ class User:
1201
  self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 10, notes = f"Completing the {num_of_micro_actions_completed}-th micro-action")
1202
  self.add_recent_wins(wins = "You have completed a micro action!", context= self.micro_actions[-1]['content'])
1203
 
 
1204
  def trigger_deep_reflection_point(self, area_of_deep_reflection):
1205
  if len(area_of_deep_reflection)>0:
1206
  for area in area_of_deep_reflection:
1207
  self.add_life_score_point(variable = area, points_added = 5, notes = f"Doing a deep reflection about {area}")
1208
  self.add_recent_wins(wins = f"You have done a deep reflection about your {area}!", context = 'Deep reflection')
1209
 
 
1210
  def add_point_for_booking(self):
1211
  self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 5, notes = "Booking a GG session")
1212
  self.add_recent_wins(wins = "You have booked a Growth Guide session!", context = "Growth Guide is a life coach")
1213
 
 
1214
  def add_point_for_completing_session(self):
1215
  self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 20, notes = "Completing a GG session")
1216
  self.add_recent_wins(wins = "You have completed a Growth Guide session!", context = "Growth Guide is a life coach")
1217
-
 
1218
  def build_ourcoach_report(self, overview, action_plan, gg_session_notes):
1219
  logger.info(f"Building ourcoach report", extra={"user_id": self.user_id, "endpoint": "build_ourcoach_report"})
1220
  ourcoach_report = {'overview': overview['overview'], 'action_plan': action_plan, 'others': gg_session_notes}
1221
  return ourcoach_report
1222
 
 
1223
  def process_growth_guide_session(self, session_data, booking_id):
1224
  logger.info(f"Processing growth guide session data: {session_data}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1225
  self.last_gg_session = booking_id
@@ -1252,6 +1059,7 @@ class User:
1252
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1253
  return response
1254
 
 
1255
  def ask_to_schedule_growth_guide_reminder(self, date):
1256
  prompt = f""" ** The user has scheduled a Growth Guide session for {date} (current date: {self.conversations.state['date']}) **\n\nFirstly, greet the user warmly and excitedly and let them know that they have succesfully booked their Growth Guide session.
1257
  Then, ask the user if they would like a reminder for the Growth Guide session. If they would like a reminder, create a new reminder 1 hour before their scheduled session."""
@@ -1261,189 +1069,27 @@ class User:
1261
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1262
  return response
1263
 
1264
- def __hash__(self) -> int:
1265
- return hash(self.user_id)
1266
-
1267
- def _prepare_growth_guide_report(self, zoom_transcript):
1268
- system_prompt = """You are an AI assistant tasked with transforming a raw Zoom transcript of a coaching session into a well-structured report.
1269
- The report should be organized into the following sections:
1270
- 1) Session Details
1271
- 2) Session Objectives
1272
- 3) Summary of Discussion
1273
- 4) Key Takeaways
1274
- 5) Action Items
1275
- 6) Next Steps
1276
- 7) Additional Notes (if any)
1277
- Ensure that each section is clearly labeled and the information is concise and well-organized.
1278
- Use bullet points or numbered lists where appropriate to enhance readability."""
1279
- prompt = f"Using the above format, convert the provided raw Zoom transcript into a structured report. Ensure clarity, coherence, and completeness in each section.\n\
1280
- Raw Zoom Transcript:\n\n{zoom_transcript}.\n\nKeep the report personalised to the 'user': {self.user_info}."
1281
-
1282
- response = self.client.chat.completions.create(
1283
- model="gpt-4o-mini",
1284
- messages=[
1285
- {"role": "system", "content": system_prompt},
1286
- {"role": "user", "content": prompt}
1287
- ],
1288
- temperature=0.2
1289
- )
1290
- return response.choices[0].message.content
1291
-
1292
- # def summarize_chat_history(self):
1293
- # prompt = f"""
1294
- # # ROLE #
1295
- # You are a world-class life coach dedicated to helping users improve their mental well-being, physical health, relationships, career, financial stability, and personal growth.
1296
- # You have done some dialogues with your client and you need to take some coaching notes to understand the key characteristic of your client and the topics that can be followed up in the next
1297
- # conversation.
1298
- # # TASK #
1299
- # Based on the chat history that is available, you must create a coaching notes that includes these parts:
1300
- # 1. Updates from last session (based on this latest summary). This is the latest coaching note from previous session that might be helpful for you as an additional context for the new coaching note:
1301
- # {request.latest_summary}
1302
- # 2. Celebrations/what has the client tried?
1303
- # 3. Today's focus
1304
- # 4. Recurring themes
1305
- # 5. New awareness created
1306
- # 6. What did I learn about the client
1307
- # 7. Notes to self
1308
- # 8. Client committed to
1309
- # 9. Events/problems to be followed up in the next session
1310
- # ##USER PROFILE##
1311
- # This is the profile of the user that you’re coaching:
1312
- # a) User Name: {request.firstName}
1313
- # b) Pronouns: {request.pronouns}
1314
- # c) Birthday: {request.birthDate}
1315
- # d) Ideal life, according to the user: {request.describeIdealLife}
1316
- # e) What matters most to user (area of focus), according to the user: {request.mattersMost}
1317
- # f) Goals in areas of focus: {request.goals}
1318
- # g) User's source of inspiration: {request.inspiration}
1319
- # h) Email address: {request.email}
1320
- # i) Whatsapp number: {request.phoneNo}
1321
- # k) User's MBTI: {request.mbti}
1322
- # l) User's Love Language: {request.loveLanguage}
1323
- # m) Whether or not the user have tried coaching before: {request.triedCoaching}
1324
- # n) What does the user do for a living:
1325
- # {doLiving}
1326
- # o) The user's current situation: {request.mySituation}
1327
- # p) Who is the most important person for the user:
1328
- # {whoImportant}
1329
- # q) Legendary persona: {request.legendPersona}
1330
- # """
1331
-
1332
- # response = self.client.chat.completions.create(
1333
- # model="gpt-4o",
1334
- # messages=[{"role": "user", "content": prompt}],
1335
- # response_format = {
1336
- # "type": "json_schema",
1337
- # "json_schema": {
1338
- # "name": "goal_determination",
1339
- # "strict": True,
1340
- # "schema": {
1341
- # "type": "object",
1342
- # "properties": {
1343
- # "same_or_not": {
1344
- # "type": "boolean",
1345
- # "description": "Indicates whether the new goal is the same as the current goal."
1346
- # },
1347
- # "goal": {
1348
- # "type": "string",
1349
- # "description": "The final goal determined based on input."
1350
- # },
1351
- # "area": {
1352
- # "type": "string",
1353
- # "description": "The area of the goal.",
1354
- # "enum": [
1355
- # "Personal Growth",
1356
- # "Career Growth",
1357
- # "Relationship",
1358
- # "Mental Well-Being",
1359
- # "Health and Wellness"
1360
- # ]
1361
- # }
1362
- # },
1363
- # "required": [
1364
- # "same_or_not",
1365
- # "goal",
1366
- # "area"
1367
- # ],
1368
- # "additionalProperties": False
1369
- # }
1370
- # }
1371
- # },
1372
- # temperature=0.2
1373
- # )
1374
-
1375
- # final_goal = json.loads(response.choices[0].message.content)['goal']
1376
- # final_goal_area = json.loads(response.choices[0].message.content)['area']
1377
- # # if json.loads(response.choices[0].message.content)['same_or_not']:
1378
- # # final_goal_status = self.get_current_goal()['status']
1379
- # # else:
1380
- # # final_goal_status = 'PENDING'
1381
-
1382
- # if json.loads(response.choices[0].message.content)['same_or_not'] == False:
1383
- # self.set_goal(final_goal, final_goal_area)
1384
- # logger.info(f"User goal updated to: {final_goal}", extra={"user_id": self.user_id, "endpoint": "_update_goal"})
1385
- # else:
1386
- # logger.info(f"User goal remains unchanged.", extra={"user_id": self.user_id, "endpoint": "_update_goal"})
1387
-
1388
- def _infer_follow_ups(self, created, context):
1389
- prompt = f"Infer the datetime of the next follow-up for the user based on the created date:{created} and the context:{context}"
1390
-
1391
- system_prompt = """
1392
- You are an event reminder that excels at estimating when to follow up events with the users. Your task is to infer the next follow-up date and time for a user based on the created date (%d-%m-%Y %a %H:%M:%S) and the provided context.
1393
- Only output a single string representing the follow-up datetime in the format '%d-%m-%Y %a %H:%M:%S'. Ensure that the inferred follow-up date occurs after the current date.
1394
- # Output Format
1395
-
1396
- - Output a single string representing the follow-up date.
1397
- - Format the string as: '%d-%m-%Y %a %H:%M:%S' (e.g., '20-11-2024 Wed 14:30:45').
1398
-
1399
- # Notes
1400
-
1401
- - The follow-up date must be after the current date.
1402
- - Use the context to infer the time. If a time cannot be inferred, then set it as 10:30:00.
1403
- - Only provide the date string, with no additional text.
1404
- # Example
1405
- User: Infer the date of the follow-up for the user based on the created date: '01-01-2024 Mon 10:10:12' and the context: I will have an exam the day after tomorrow
1406
- Assistant: '03-01-2024 Wed 10:30:00'
1407
- User: Infer the date of the follow-up for the user based on the created date: '02-01-2024 Tue 14:00:00' and the context: I will have a lunch tomorrow with friends
1408
- Assistant: '03-01-2024 Wed 12:00:00'
1409
- User: Infer the date of the follow-up for the user based on the created date: '17-11-2024 Sun 11:03:43' and the context: Next Wednesday, i will have a dinner with someone
1410
- Assistant: '20-11-2024 Wed 19:30:00'
1411
- User: Infer the date of the follow-up for the user based on the created date: '20-11-2024 Wed 10:33:15' and the context: I have a weekend trip planned
1412
- Assistant: '22-11-2024 Fri 23:30:00'
1413
- User: Infer the date of the follow-up for the user based on the created date: '20-11-2024 Wed 10:33:15' and the context: I have a lunch this Sunday
1414
- Assistant: '24-11-2024 Sun 12:00:00'
1415
- """
1416
- response = self.client.chat.completions.create(
1417
- model="gpt-4o",
1418
- messages=[
1419
- {"role": "system", "content": system_prompt},
1420
- {"role": "user", "content": prompt}
1421
- ],
1422
- top_p=0.1
1423
- )
1424
- return response.choices[0].message.content
1425
-
1426
  def infer_memento_follow_ups(self):
1427
- try:
1428
- mementos_path = os.path.join("mementos", "to_upload", f"{self.user_id}", "*.json")
1429
- # mementos_path = f"mementos/to_upload/{self.user_id}/*.json"
1430
-
1431
- for file_path in glob.glob(mementos_path):
1432
- with open(file_path, 'r+') as file:
1433
- data = json.load(file)
1434
- infered_follow_up = self._infer_follow_ups(data['created'], data['context'])
1435
- logger.info(f"[Infered Follow Up]: {infered_follow_up}", extra={"user_id": self.user_id, "endpoint": "infer_memento_follow_ups"})
1436
- data['follow_up_on'] = infered_follow_up
1437
- file.seek(0)
1438
- json.dump(data, file, indent=4)
1439
- file.truncate()
1440
- return True
1441
- except Exception as e:
1442
- return False
1443
 
 
1444
  def get_daily_messages(self):
1445
  return self.conversations.get_daily_thread()
1446
 
 
1447
  def change_assistant(self, asst_id):
1448
  self.asst_id = asst_id
1449
  self.conversations.assistants['general'] = Assistant(self.asst_id, self.conversations)
@@ -1495,18 +1141,15 @@ Use bullet points or numbered lists where appropriate to enhance readability."""
1495
 
1496
  def save_user(self):
1497
  # Construct the file path dynamically for cross-platform compatibility
1498
- try:
1499
- file_path = os.path.join("users", "to_upload", f"{self.user_id}.pkl")
1500
-
1501
- # Ensure the directory exists
1502
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
1503
-
1504
- # Save the user object as a pickle file
1505
- with open(file_path, 'wb') as file:
1506
- pickle.dump(self, file)
1507
- return file_path
1508
- except Exception as e:
1509
- return False
1510
 
1511
  @staticmethod
1512
  def load_user(user_id, client):
 
1
  import json
2
  import io
3
  import os
4
+ import openai
5
  import pandas as pd
6
  from datetime import datetime, timezone
7
  import json
 
12
  import logging
13
  import psycopg2
14
  from psycopg2 import sql
15
+ from app.conversation_manager import ConversationManager
16
+ from app.exceptions import BaseOurcoachException, OpenAIRequestError, UserError
17
 
18
  from app.flows import FINAL_SUMMARY_STATE, FINAL_SUMMARY_STATE, MICRO_ACTION_STATE, MOTIVATION_INSPIRATION_STATE, OPEN_DISCUSSION_STATE, POST_GG_STATE, PROGRESS_REFLECTION_STATE, PROGRESS_SUMMARY_STATE, EDUCATION_STATE, FOLLUP_ACTION_STATE, FUNFACT_STATE
19
  from pydantic import BaseModel
 
47
  def get_current_datetime():
48
  return datetime.now(timezone.utc)
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  class User:
51
+ def catch_error(func):
52
+ def wrapper(self, *args, **kwargs):
53
+ try:
54
+ return func(self, *args, **kwargs)
55
+ except (BaseOurcoachException, openai.BadRequestError) as e:
56
+ raise e
57
+ except openai.BadRequestError as e:
58
+ raise OpenAIRequestError(user_id=self.user_id, message="OpenAI Request Error", e=str(e))
59
+ except Exception as e:
60
+ # Handle other exceptions
61
+ logger.error(f"An unexpected error occurred in User: {e}")
62
+ raise UserError(user_id=self.user_id, message="Unexpected error in User", e=str(e))
63
+ return wrapper
64
+
65
  def __init__(self, user_id, user_info, client, asst_id):
66
  self.user_id = user_id
67
  self.client = client
 
81
  self.health_and_wellness_score = 0
82
  self.reminders = None
83
  self.recent_wins = []
84
+ self.recommended_gg_topics = []
85
 
86
  # Read growth_plan.json and store it
87
 
 
117
  self.user_interaction_guidelines = self.generate_user_interaction_guidelines(user_info, client)
118
  self.conversations = ConversationManager(client, self, asst_id)
119
 
120
+ @catch_error
121
  def extend_growth_plan(self):
122
  # Change current growth plan to 14d growth plan
123
  logger.info(f"Changing plan to 14d...", extra={"user_id": self.user_id, "endpoint": "extend_growth_plan"})
124
+ new_growth_plan = {"growthPlan": [
125
+ {
126
+ "day": 1,
127
+ "coachingTheme": "MICRO_ACTION_STATE"
128
+ },
129
+ {
130
+ "day": 2,
131
+ "coachingTheme": "FOLLUP_ACTION_STATE"
132
+ },
133
+ {
134
+ "day": 3,
135
+ "coachingTheme": "OPEN_DISCUSSION_STATE"
136
+ },
137
+ {
138
+ "day": 4,
139
+ "coachingTheme": "MICRO_ACTION_STATE"
140
+ },
141
+ {
142
+ "day": 5,
143
+ "coachingTheme": "FOLLUP_ACTION_STATE"
144
+ },
145
+ {
146
+ "day": 6,
147
+ "coachingTheme": "FUNFACT_STATE"
148
+ },
149
+ {
150
+ "day": 7,
151
+ "coachingTheme": "PROGRESS_REFLECTION_STATE"
152
+ },
153
+ {
154
+ "day": 8,
155
+ "coachingTheme": "MICRO_ACTION_STATE"
156
+ },
157
+ {
158
+ "day": 9,
159
+ "coachingTheme": "FOLLUP_ACTION_STATE"
160
+ },
161
+ {
162
+ "day": 10,
163
+ "coachingTheme": "OPEN_DISCUSSION_STATE"
164
+ },
165
+ {
166
+ "day": 11,
167
+ "coachingTheme": "MICRO_ACTION_STATE"
168
+ },
169
+ {
170
+ "day": 12,
171
+ "coachingTheme": "FOLLUP_ACTION_STATE"
172
+ },
173
+ {
174
+ "day": 13,
175
+ "coachingTheme": "FUNFACT_STATE"
176
+ },
177
+ {
178
+ "day": 14,
179
+ "coachingTheme": "FINAL_SUMMARY_STATE"
180
+ }
181
+ ]
182
+ }
183
+ self.growth_plan = CircularQueue(array=new_growth_plan['growthPlan'], user_id=self.user_id)
184
+ logger.info(f"User Growth Plan: {self.growth_plan} (Day: {self.growth_plan.current()['day']}/{len(self.growth_plan.array)})", extra={"user_id": self.user_id, "endpoint": "user_init"})
185
+ logger.info(f"Success.", extra={"user_id": self.user_id, "endpoint": "extend_growth_plan"})
186
+ return True
187
+
188
+ @catch_error
 
 
 
189
  def add_recent_wins(self, wins, context = None):
190
  prompt = f"""
191
  ## Role
192
+ You are an expert in writing achievement message and progress notification. Your task is to use the user's achievement and context to formulate a short and creative achievement message/progress notification. The output must be a one sentence short message (less than 15 words) in this JSON output schema:
193
 
194
  ```json
195
  {{
 
206
  Output:
207
  ```
208
  {{
209
+ achievement_message: You crushed it! Completing that 10k run is a huge milestone—way to go!
210
  }}
211
  ```
212
 
 
250
  self.recent_wins.pop()
251
  self.recent_wins.insert(0,achievement_message)
252
 
253
+ @catch_error
254
  def add_life_score_point(self, variable, points_added, notes):
255
  if variable == 'Personal Growth':
256
  self.personal_growth_score += points_added
 
267
  elif variable == 'Relationship':
268
  self.relationship_score += points_added
269
  logger.info(f"Added {points_added} points to Relationship for {notes}", extra={"user_id": self.user_id, "endpoint": "add_life_score_point"})
270
+
271
+ @catch_error
272
  def get_current_goal(self, full=False):
273
  # look for most recent goal with status = ONGOING
274
  for goal in self.goal[::-1]:
 
283
  return self.goal[-1].content
284
  return None
285
 
286
+ @catch_error
287
  def update_goal(self, goal, status, content=None):
288
  if goal is None:
289
  # complete the current goal
 
302
  return True
303
  return False
304
 
305
+ @catch_error
306
  def set_goal(self, goal, goal_area, add=True, completed=False):
307
  current_goal = self.get_current_goal()
308
 
 
328
  else:
329
  self.update_goal(current_goal, "ONGOING", content=goal)
330
 
331
+ @catch_error
332
  def update_recommended_micro_action_status(self, micro_action, status):
333
  for ma in self.recommended_micro_actions:
334
  if ma.content == micro_action:
 
337
  return True
338
  return False
339
 
340
+ @catch_error
341
  def add_ai_message(self, text):
342
  self.conversations._add_ai_message(text)
343
  return text
344
 
345
+ @catch_error
346
  def reset_conversations(self):
347
  self.conversations = ConversationManager(self.client, self, self.asst_id)
348
  self.growth_plan.reset()
 
359
  self.health_and_wellness_score = 0
360
  self.reminders = None
361
  self.recent_wins = []
362
+ self.recommended_gg_topics = []
363
 
364
+ @catch_error
365
+ def get_last_user_message(self):
366
+ # find the last message from 'role': 'user' in the conversation history
367
+ messages = self.conversations._get_current_thread_history(remove_system_message=False)
368
+ for msg in messages[::-1]:
369
+ if msg['role'] == 'user':
370
+ return msg['content']
371
 
372
+ @catch_error
373
  def generate_user_interaction_guidelines(self, user_info, client):
374
  logger.info(f"Generating user interaction guidelines for user: {self.user_id}", extra={"user_id": self.user_id, "endpoint": "generate_user_interaction_guidelines"})
375
  # prompt = f"A 'profile' is a document containing rich insights on users for the purpose of \
 
405
 
406
  return user_guideline
407
 
408
+ @catch_error
409
  def get_recent_run(self):
410
  return self.conversations.assistants['general'].recent_run
411
 
412
+ @catch_error
413
+ def cancel_run(self, run, thread=None):
414
+ logger.info(f"(user) Cancelling run: {run}", extra={"user_id": self.user_id, "endpoint": "cancel_run"})
415
+ self.conversations.cancel_run(run, thread)
416
 
417
+ @catch_error
418
  def update_conversation_state(self, stage, last_interaction):
419
  self.conversation_state['stage'] = stage
420
  self.conversation_state['last_interaction'] = last_interaction
421
 
422
+ @catch_error
423
  def _get_current_thread(self):
424
  return self.conversations.current_thread
425
 
426
+ @catch_error
427
  def send_message(self, text):
428
+ response, run = self.conversations._run_current_thread(text)
429
+ message = run.metadata.get("message", "No message")
430
+ logger.info(f"Message: {message}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
431
 
432
+ if message == "start_now":
433
  # must do current plan now
434
  action = self.growth_plan.current()
435
  logger.info(f"Current Action: {action}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
 
442
  # Move to the next action
443
  self.growth_plan.next()
444
 
445
+ elif message == "change_goal":
446
  # send the change goal prompt
447
  logger.info("Sending change goal message...", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
448
  prompt = f"""
 
479
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
480
  return response
481
 
482
+ @catch_error
483
  def get_reminders(self, date=None):
484
  if self.reminders is None:
485
  return []
 
495
  return [reminder for reminder in self.reminders if reminder['timestamp'].date() == date]
496
  return self.reminders
497
 
498
+ @catch_error
499
  def find_same_reminder(self, reminder_text):
500
  logger.info(f"Finding similar reminders: {self.reminders} to: {reminder_text}", extra={"user_id": self.user_id, "endpoint": "find_same_reminder"})
501
  response = self.client.beta.chat.completions.parse(
 
512
  logger.info(f"Similar reminder idx: reminders[{index}]", extra={"user_id": self.user_id, "endpoint": "find_same_reminder"})
513
  return index
514
 
515
+ @catch_error
516
  def set_reminder(self, reminder):
517
  db_params = {
518
  'dbname': 'ourcoach',
 
569
 
570
  logger.info(f"Reminders: {self.reminders}", extra={"user_id": self.user_id, "endpoint": "set_reminder"})
571
 
572
+ @catch_error
573
  def get_messages(self, exclude_system_msg=True, show_hidden=False):
574
  if not exclude_system_msg:
575
  return self.conversations._get_current_thread_history(False)
 
578
  return list(filter(lambda x: not (x['content'].startswith("** It is a new day:") or x['content'].startswith("Pay attention to the current state you are in") or x['content'].startswith("Date changed to")), self.conversations._get_current_thread_history(exclude_system_msg)))
579
  return list(filter(lambda x: not (x['content'].startswith("** It is a new day:") or x['content'].startswith("Pay attention to the current state you are in") or x['content'].startswith("Date changed to") or x['content'].startswith("[hidden]")), self.conversations._get_current_thread_history(exclude_system_msg)))
580
 
581
+ @catch_error
582
+ def set_recommened_gg_topics(self, topics):
583
+ self.recommended_gg_topics = topics
584
+
585
+ @catch_error
586
  def set_intro_done(self):
587
  self.conversations.intro_done = True
588
 
589
+ @catch_error
590
  def do_theme(self, theme, date, day):
591
  logger.info(f"Doing theme: {theme}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
592
 
 
657
 
658
  (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.)
659
 
660
+ Note: If the user has not answered your question yesterday, you must say something like (but warmer) "Hey, you didn't answer my question. Do you still want to continue?"
661
+ If the user says "no", then ask if they want to set a new goal (therefore later, call the change_goal() function)
662
+ But if the user says "yes", then proceed to the theme below.
663
+ Otherwise, you may directly proceed to today's theme below without asking the user.
664
+
665
  Additional System Instruction:
666
  ** Always remember to incorporate your personality (based on your persona) into all of your responses. **
667
  1. Focus on giving more wisdom than questions:
 
706
 
707
  return response, prompt
708
 
709
+ @catch_error
710
  def change_date(self, date):
711
  logger.info(f"Changing date from {self.conversations.state['date']} to {date}",
712
  extra={"user_id": self.user_id, "endpoint": "user_change_date"})
 
760
  logger.info(f"Date Updated: {self.conversations.state['date']}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
761
  return {'response': response, 'theme_prompt': '[hidden]'+prompt}
762
 
763
+ @catch_error
764
  def update_user_info(self, new_info):
765
  logger.info(f"Updating user info: [{self.user_info}] with: [{new_info}]", extra={"user_id": self.user_id, "endpoint": "update_user_info"})
766
  # make an api call to gpt4o to compare the current user_info and the new info and create a new consolidated user_info
 
789
  logger.info(f"Updated user info: {self.user_info}", extra={"user_id": self.user_id, "endpoint": "update_user_info"})
790
  return True
791
 
792
+ @catch_error
793
  def _summarize_zoom(self, zoom_ai_summary):
794
  logger.info(f"Summarizing zoom ai summary", extra={"user_id": self.user_id, "endpoint": "summarize_zoom"})
795
  # make an api call to gpt4o to summarize the zoom_ai_summary and produce a text with a focus on the most amount of user insight and info extracted
 
807
  logger.info(f"Summary: {response.choices[0].message.content}", extra={"user_id": self.user_id, "endpoint": "summarize_zoom"})
808
  return {'overview': response.choices[0].message.content}
809
 
810
+ @catch_error
811
  def _update_user_data(self, data_type, text_input, extra_text=""):
812
  data_mapping = {
813
  'micro_actions': {
 
844
  f"Text:\n{text_input}"
845
  )
846
 
847
+ current_time = datetime.now(timezone.utc).strftime("%d-%m-%Y %a %H:%M:%S")
848
+
849
+ response = self.client.beta.chat.completions.parse(
850
+ model="gpt-4o",
851
+ messages=[{"role": "user", "content": prompt}],
852
+ response_format=UserDataResponse,
853
+ temperature=0.2
854
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
855
 
856
+ data = getattr(response.choices[0].message.parsed, 'data')
857
+
858
+ # Update the common fields for each item
859
+ for item in data:
860
+ item.role = "assistant"
861
+ item.user_id = self.user_id
862
+ item.status = mapping['status']
863
+ item.created_at = current_time
864
+ item.updated_at = current_time
865
+
866
+ logger.info(f"Updated {data_type}: {data}", extra={"user_id": self.user_id, "endpoint": mapping['endpoint']})
867
+ getattr(self, mapping['attribute']).extend(data)
868
+
869
+ @catch_error
870
  def update_user_data(self, gg_report):
871
  self._update_user_data('micro_actions', gg_report[0]['answer'])
872
 
 
877
 
878
  self._update_goal(gg_report[4]['answer'])
879
 
880
+ @catch_error
881
  def _update_goal(self, goal_text):
882
  prompt = f"""
883
  The user has a current goal: {self.get_current_goal()}
 
991
  else:
992
  logger.info(f"User goal remains unchanged.", extra={"user_id": self.user_id, "endpoint": "_update_goal"})
993
 
994
+ @catch_error
995
  def update_micro_action_status(self, completed_micro_action):
996
  if completed_micro_action:
997
  self.micro_actions[-1].status = "COMPLETE"
 
1003
  self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 10, notes = f"Completing the {num_of_micro_actions_completed}-th micro-action")
1004
  self.add_recent_wins(wins = "You have completed a micro action!", context= self.micro_actions[-1]['content'])
1005
 
1006
+ @catch_error
1007
  def trigger_deep_reflection_point(self, area_of_deep_reflection):
1008
  if len(area_of_deep_reflection)>0:
1009
  for area in area_of_deep_reflection:
1010
  self.add_life_score_point(variable = area, points_added = 5, notes = f"Doing a deep reflection about {area}")
1011
  self.add_recent_wins(wins = f"You have done a deep reflection about your {area}!", context = 'Deep reflection')
1012
 
1013
+ @catch_error
1014
  def add_point_for_booking(self):
1015
  self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 5, notes = "Booking a GG session")
1016
  self.add_recent_wins(wins = "You have booked a Growth Guide session!", context = "Growth Guide is a life coach")
1017
 
1018
+ @catch_error
1019
  def add_point_for_completing_session(self):
1020
  self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 20, notes = "Completing a GG session")
1021
  self.add_recent_wins(wins = "You have completed a Growth Guide session!", context = "Growth Guide is a life coach")
1022
+
1023
+ @catch_error
1024
  def build_ourcoach_report(self, overview, action_plan, gg_session_notes):
1025
  logger.info(f"Building ourcoach report", extra={"user_id": self.user_id, "endpoint": "build_ourcoach_report"})
1026
  ourcoach_report = {'overview': overview['overview'], 'action_plan': action_plan, 'others': gg_session_notes}
1027
  return ourcoach_report
1028
 
1029
+ @catch_error
1030
  def process_growth_guide_session(self, session_data, booking_id):
1031
  logger.info(f"Processing growth guide session data: {session_data}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1032
  self.last_gg_session = booking_id
 
1059
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1060
  return response
1061
 
1062
+ @catch_error
1063
  def ask_to_schedule_growth_guide_reminder(self, date):
1064
  prompt = f""" ** The user has scheduled a Growth Guide session for {date} (current date: {self.conversations.state['date']}) **\n\nFirstly, greet the user warmly and excitedly and let them know that they have succesfully booked their Growth Guide session.
1065
  Then, ask the user if they would like a reminder for the Growth Guide session. If they would like a reminder, create a new reminder 1 hour before their scheduled session."""
 
1069
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1070
  return response
1071
 
1072
+ @catch_error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073
  def infer_memento_follow_ups(self):
1074
+ mementos_path = os.path.join("mementos", "to_upload", f"{self.user_id}", "*.json")
1075
+ # mementos_path = f"mementos/to_upload/{self.user_id}/*.json"
1076
+
1077
+ for file_path in glob.glob(mementos_path):
1078
+ with open(file_path, 'r+') as file:
1079
+ data = json.load(file)
1080
+ infered_follow_up = self._infer_follow_ups(data['created'], data['context'])
1081
+ logger.info(f"[Infered Follow Up]: {infered_follow_up}", extra={"user_id": self.user_id, "endpoint": "infer_memento_follow_ups"})
1082
+ data['follow_up_on'] = infered_follow_up
1083
+ file.seek(0)
1084
+ json.dump(data, file, indent=4)
1085
+ file.truncate()
1086
+ return True
 
 
 
1087
 
1088
+ @catch_error
1089
  def get_daily_messages(self):
1090
  return self.conversations.get_daily_thread()
1091
 
1092
+ @catch_error
1093
  def change_assistant(self, asst_id):
1094
  self.asst_id = asst_id
1095
  self.conversations.assistants['general'] = Assistant(self.asst_id, self.conversations)
 
1141
 
1142
  def save_user(self):
1143
  # Construct the file path dynamically for cross-platform compatibility
1144
+ file_path = os.path.join("users", "to_upload", f"{self.user_id}.pkl")
1145
+
1146
+ # Ensure the directory exists
1147
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
1148
+
1149
+ # Save the user object as a pickle file
1150
+ with open(file_path, 'wb') as file:
1151
+ pickle.dump(self, file)
1152
+ return file_path
 
 
 
1153
 
1154
  @staticmethod
1155
  def load_user(user_id, client):
app/utils.py CHANGED
@@ -6,6 +6,7 @@ from dotenv import load_dotenv
6
  from fastapi import FastAPI, HTTPException, Security, Query, status
7
  from fastapi.security import APIKeyHeader
8
  from openai import OpenAI
 
9
  import pandas as pd
10
  from pydantic import BaseModel
11
  import os
@@ -28,6 +29,8 @@ import PyPDF2
28
  import secrets
29
  import string
30
 
 
 
31
  load_dotenv()
32
 
33
  # Environment Variables for API Keys
@@ -45,21 +48,32 @@ logger = logging.getLogger(__name__)
45
  # Replace the simple TTLCache with our custom implementation
46
  user_cache = CustomTTLCache(ttl=120, cleanup_interval=30) # 2 minutes TTL
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  def force_file_move(source, destination):
49
  function_name = force_file_move.__name__
50
  logger.info(f"Attempting to move file from {source} to {destination}", extra={'endpoint': function_name})
51
- try:
52
- # Ensure the destination directory exists
53
- os.makedirs(os.path.dirname(destination), exist_ok=True)
54
-
55
- # Move the file, replacing if it already exists
56
- os.replace(source, destination)
57
- logger.info(f"File moved successfully: {source} -> {destination}", extra={'endpoint': function_name})
58
- except FileNotFoundError:
59
- logger.error(f"Source file not found: {source}", extra={'endpoint': function_name})
60
- except Exception as e:
61
- logger.error(f"An error occurred while moving file: {e}", extra={'endpoint': function_name})
62
 
 
63
  def get_user(user_id):
64
  function_name = get_user.__name__
65
  logger.info(f"Fetching user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
@@ -69,6 +83,8 @@ def get_user(user_id):
69
  return user_cache[user_id]
70
  else:
71
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
 
 
72
  user_file = os.path.join('users', 'data', f'{user_id}.pkl')
73
  # if os.path.exists(user_file):
74
  # with open(user_file, 'rb') as f:
@@ -96,9 +112,10 @@ def get_user(user_id):
96
  user_info = get_user_info(user_id)
97
  if (user_info):
98
  # user has done onboarding but pickle file not created
99
- raise ReferenceError(f"User {user_id} pickle still being created")
100
- raise LookupError(f"User [{user_id}] has not onboarded yet")
101
 
 
102
  def generate_html(json_data, coach_name='Growth Guide', booking_id = None):
103
  function_name = generate_html.__name__
104
  data = json_data["pre_growth_guide_session_report"]
@@ -276,37 +293,32 @@ def generate_html(json_data, coach_name='Growth Guide', booking_id = None):
276
  password = "Ourcoach2024!"
277
 
278
  ## SAVING HTML FILE
279
- try:
280
- # Open the file in write mode
281
- with open(file_path, 'w', encoding='utf-8') as html_file:
282
- html_file.write(html_content)
283
- logger.info(f"File '{booking_id}.html' has been created successfully.", extra={'booking_id': booking_id, 'endpoint': function_name})
284
-
285
- # Saving as PDF File
286
- pdfkit.from_file(file_path, path_to_upload, options={'encoding': 'UTF-8'})
287
- logger.info(f"File '{booking_id}.pdf' has been created successfully.", extra={'booking_id': booking_id, 'endpoint': function_name})
288
-
289
- ## ENCRYPTING PDF
290
- logger.info(f"Encrypting '{booking_id}.pdf'...", extra={'booking_id': booking_id, 'endpoint': function_name})
291
- with open(path_to_upload, 'rb') as file:
292
- pdf_reader = PyPDF2.PdfReader(file)
293
- pdf_writer = PyPDF2.PdfWriter()
294
-
295
- # Add all pages to the writer
296
- for page_num in range(len(pdf_reader.pages)):
297
- pdf_writer.add_page(pdf_reader.pages[page_num])
298
-
299
- # Encrypt the PDF with the given password
300
- pdf_writer.encrypt(password)
301
-
302
- with open(path_to_upload, 'wb') as encrypted_file:
303
- pdf_writer.write(encrypted_file)
304
-
305
- logger.info(f"Succesfully encrypted '{booking_id}.pdf'", extra={'booking_id': booking_id, 'endpoint': function_name})
306
-
307
- except Exception as e:
308
- logger.error(f"An error occurred: {e}", extra={'booking_id': booking_id, 'endpoint': function_name})
309
- raise
310
 
311
  filename = booking_id
312
 
@@ -331,21 +343,18 @@ def generate_html(json_data, coach_name='Growth Guide', booking_id = None):
331
 
332
  # force_file_move(os.path.join('users', 'to_upload', filename), os.path.join('users', 'data', filename))
333
  except (FileNotFoundError, NoCredentialsError, PartialCredentialsError) as e:
334
- logger.error(f"S3 upload failed for {filename}: {e}", extra={'booking_id': booking_id, 'endpoint': function_name})
335
- raise
336
 
 
337
  def get_user_summary(user_id):
338
  function_name = get_user_summary.__name__
339
  logger.info(f"Generating user summary for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
340
 
341
  # Step 1: Call get_user to get user's info
342
- try:
343
- user = get_user(user_id)
344
- user_info = user.user_info
345
- user_messages = user.get_messages()
346
- except LookupError as e:
347
- logger.error(f"Error fetching user data: {e}", extra={'user_id': user_id, 'endpoint': function_name})
348
- raise e
349
 
350
  # Step 2: Construct the Prompt
351
  chat_history = "\n".join(
@@ -418,22 +427,27 @@ def get_user_summary(user_id):
418
  ### **2. User's Growth Guide Preparation Brief**
419
 
420
  **Objective**: Guide the user on what to discuss with the Growth Guide, providing actionable advice and highlighting key areas to focus on during their session, covering the five key areas.
 
421
 
422
- **ALWAYS** be succinct, valuable and personalized!
 
 
 
 
423
 
424
  **Format**:
425
 
426
- Structure the brief with the following sections, and output it as a JSON object with these keys:
427
 
428
- - **reflect**: Give a succinct, valuable and personalized advice to the user to consider what each focus area means to them and which aspects they want to improve.
429
 
430
- - **recall_successes**: Give a succinct, valuable and personalized prompt to the user to think of times when they effectively managed or improved in the five key areas, and what strategies worked for them then.
431
 
432
- - **identify_challenges**: Give a succinct, valuable and personalized advice to the user to be ready to discuss any obstacles they're facing in each area, and to consider possible solutions or resources that might help.
433
 
434
- - **set_goals**: Ask the user to decide what they hope to achieve from the session and how improvements in each area will impact their life.
435
 
436
- - **additional_tips**: Give a succinct, valuable and personalized practical advice for the session, such as choosing a quiet space, having materials ready, and being open to sharing thoughts honestly.
437
 
438
  ---
439
 
@@ -536,23 +550,23 @@ def get_user_summary(user_id):
536
  "users_growth_guide_preparation_brief": [
537
  {
538
  "key": "reflect",
539
- "value": "⁠Reflect on what career growth means to you and the goals you’re striving for."
540
  },
541
  {
542
  "key": "recall_successes",
543
- "value": "⁠Recall moments when you successfully navigated challenges in your career or relationships—what worked"
544
  },
545
  {
546
  "key": "identify_challenges",
547
- "value": "⁠Think about any current obstacles and possible ways to overcome them"
548
  },
549
  {
550
  "key": "set_goals",
551
- "value": "⁠Define what you want to achieve from this session and how those changes could impact your life."
552
  },
553
  {
554
  "key": "additional_tips",
555
- "value": "⁠Prepare by choosing a quiet space, having a notebook ready, and being open to sharing honestly."
556
  }
557
  ],
558
  "30_minute_coaching_session_script": {
@@ -596,282 +610,273 @@ def get_user_summary(user_id):
596
 
597
  # Step 3: Call the OpenAI API using the specified function
598
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
599
- try:
600
- response = client.chat.completions.create(
601
- model="gpt-4o-mini",
602
- messages=[
603
- {
604
- "role": "system",
605
- "content": [
606
- {
607
- "type": "text",
608
- "text": system_prompt
609
- }
610
- ]
611
- },
612
- {
613
- "role": "user",
614
- "content": [
615
- {
616
- "type": "text",
617
- "text": user_context
618
- }
619
- ]
620
- }
621
- ],
622
- response_format={
623
- "type": "json_schema",
624
- "json_schema": {
625
- "name": "growth_guide_session",
626
- "strict": True,
627
- "schema": {
 
 
628
  "type": "object",
 
629
  "properties": {
630
- "pre_growth_guide_session_report": {
631
  "type": "object",
632
- "description": "A comprehensive summary of the user's profile and life context for the Growth Guide.",
633
  "properties": {
634
- "user_overview": {
635
- "type": "object",
636
- "properties": {
637
- "name": {
638
- "type": "string",
639
- "description": "The user's full name."
640
- },
641
- "age_group": {
642
- "type": "string",
643
- "description": "The user's age range (e.g., '30-39')."
644
- },
645
- "primary_goals": {
646
- "type": "string",
647
- "description": "The main goals the user is focusing on."
648
- },
649
- "preferred_coaching_style": {
650
- "type": "string",
651
- "description": "The coaching style the user prefers."
652
- }
653
  },
654
- "required": ["name", "age_group", "primary_goals", "preferred_coaching_style"],
655
- "additionalProperties": False
 
656
  },
657
- "personality_insights": {
658
- "type": "object",
659
- "properties": {
660
- "mbti": {
661
- "type": "string",
662
- "description": "The user's Myers-Briggs Type Indicator personality type."
663
- },
664
- "top_love_languages": {
665
- "type": "array",
666
- "items": {
667
- "type": "string"
668
- },
669
- "description": "A list of the user's top two love languages."
670
- },
671
- "belief_in_astrology": {
672
- "type": "string",
673
- "description": "Whether the user believes in horoscope/astrology."
674
- }
675
- },
676
- "required": ["mbti", "top_love_languages", "belief_in_astrology"],
677
- "additionalProperties": False
678
- },
679
- "progress_snapshot": {
680
- "type": "object",
681
- "properties": {
682
- "mental_well_being": {
683
- "type": "string",
684
- "description": "Summary of the user's mental well-being."
685
- },
686
- "physical_health_and_wellness": {
687
- "type": "string",
688
- "description": "Summary of the user's physical health and wellness."
689
- },
690
- "relationships": {
691
- "type": "string",
692
- "description": "Summary of the user's relationships."
693
- },
694
- "career_growth": {
695
- "type": "string",
696
- "description": "Summary of the user's career growth."
697
- },
698
- "personal_growth": {
699
- "type": "string",
700
- "description": "Summary of the user's personal growth."
701
- }
702
  },
703
- "required": [
704
- "mental_well_being",
705
- "physical_health_and_wellness",
706
- "relationships",
707
- "career_growth",
708
- "personal_growth"
709
- ],
710
- "additionalProperties": False
711
  }
712
  },
713
- "required": ["user_overview", "personality_insights", "progress_snapshot"],
714
  "additionalProperties": False
715
  },
716
- "users_growth_guide_preparation_brief": {
 
 
 
 
 
 
 
717
  "type": "array",
718
- "description": "A brief guiding the user on what to discuss with the Growth Guide, providing actionable advice and highlighting key areas to focus on.",
719
  "items": {
720
- "type": "object",
721
- "properties": {
722
- "key": {
723
- "type": "string",
724
- "description": "The section heading."
725
- },
726
- "value": {
727
- "type": "string",
728
- "description": "Content for the section."
729
- }
730
  },
731
- "required": [
732
- "key",
733
- "value"
734
- ],
735
- "additionalProperties": False
736
  }
737
  },
738
- "30_minute_coaching_session_script": {
 
 
 
739
  "type": "object",
740
- "description": "A detailed, partitioned script to help the coach prepare for the session, following the specified session order and focusing on the user's top three most important areas.",
741
  "properties": {
742
- "session_overview": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
  "type": "array",
744
  "items": {
745
  "type": "string"
746
  },
747
- "description": "Breakdown of the session segments with time frames."
748
  },
749
- "detailed_segments": {
750
  "type": "array",
751
  "items": {
752
- "type": "object",
753
- "properties": {
754
- "segment_title": {
755
- "type": "string",
756
- "description": "Title of the session segment."
757
- },
758
- "coach_dialogue": {
759
- "type": "array",
760
- "items": {
761
- "type": "string"
762
- },
763
- "description": "Suggested coach dialogue during the session"
764
- },
765
- "guidance": {
766
- "type": "array",
767
- "items": {
768
- "type": "string"
769
- },
770
- "description": "Suggestions for the coach on how to navigate responses."
771
- }
772
  },
773
- "required": ["segment_title", "coach_dialogue", "guidance"],
774
- "additionalProperties": False
775
- },
776
- "description": "Detailed information for each session segment."
777
  }
 
 
 
778
  },
779
- "required": [
780
- "session_overview",
781
- "detailed_segments"
782
- ],
783
- "additionalProperties": False
784
  }
785
  },
786
  "required": [
787
- "pre_growth_guide_session_report",
788
- "users_growth_guide_preparation_brief",
789
- "30_minute_coaching_session_script"
790
  ],
791
  "additionalProperties": False
792
  }
 
 
 
 
 
 
 
793
  }
794
- }
795
- ,
796
- temperature=0.5,
797
- max_tokens=3000,
798
- top_p=1,
799
- frequency_penalty=0,
800
- presence_penalty=0
801
- )
 
802
 
803
- # Get response and convert into dictionary
804
- reports = json.loads(response.choices[0].message.content)
805
- # html_output = generate_html(reports, coach_name)
806
- # reports['html_report'] = html_output
807
 
808
- except Exception as e:
809
- logger.error(f"OpenAI API call failed: {e}", extra={'user_id': user_id, 'endpoint': function_name})
810
- raise e
811
 
812
  # Step 4: Return the JSON reports
813
  logger.info(f"User summary generated successfully for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
814
  return reports
815
 
 
816
  def create_pre_gg_report(booking_id):
817
  function_name = create_pre_gg_report.__name__
818
 
819
  # Get user_id from booking_id
 
 
 
 
 
 
 
 
820
  try:
821
- logger.info(f"Retrieving booking details for {booking_id}", extra={'booking_id': booking_id, 'endpoint': function_name})
822
- db_params = {
823
- 'dbname': 'ourcoach',
824
- 'user': 'ourcoach',
825
- 'password': 'hvcTL3kN3pOG5KteT17T',
826
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
827
- 'port': '5432'
828
- }
829
- try:
830
- with psycopg2.connect(**db_params) as conn:
831
- with conn.cursor() as cursor:
832
- query = sql.SQL("""
833
- select user_id
834
- from {table}
835
- where id = %s
836
- """
837
- ).format(table=sql.Identifier('public', 'booking'))
838
- cursor.execute(query, (booking_id,))
839
- row = cursor.fetchone()
840
- if (row):
841
- colnames = [desc[0] for desc in cursor.description]
842
- booking_data = dict(zip(colnames, row))
843
- ### MODIFY THE FORMAT OF USER DATA
844
- user_id = booking_data['user_id']
845
- logger.info(f"User info retrieved successfully for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
846
- else:
847
- logger.warning(f"No user info found for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
848
- except psycopg2.Error as e:
849
- logger.error(f"Database error while retrieving user info for {user_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
850
- raise
851
-
852
- # Run get_user_summary
853
- user_report = get_user_summary(user_id)
854
 
855
- # Run generate_html
856
- generate_html(user_report, booking_id=booking_id)
857
-
858
- return True
859
- except Exception as e:
860
- logger.error(f"An error occured: {e}", extra={'booking_id': booking_id, 'endpoint': function_name})
861
- raise
862
 
 
863
  def get_user_life_status(user_id):
864
  function_name = get_user_life_status.__name__
865
  logger.info(f"Generating user life status for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
866
 
867
- # Step 1: Call get_user to get user's info
868
- try:
869
- user = get_user(user_id)
870
- user_info = user.user_info
871
- user_messages = user.get_messages()
872
- except LookupError as e:
873
- logger.error(f"Error fetching user data: {e}", extra={'user_id': user_id, 'endpoint': function_name})
874
- raise e
875
 
876
  # Step 2: Construct the Prompt
877
  chat_history = "\n".join(
@@ -918,63 +923,58 @@ def get_user_life_status(user_id):
918
 
919
  # Step 3: Call the OpenAI API using the specified function
920
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
921
- try:
922
- response = client.chat.completions.create(
923
- model="gpt-4o-mini",
924
- messages=[
925
- {
926
- "role": "system",
927
- "content": [
928
- {
929
- "type": "text",
930
- "text": system_prompt
931
- }
932
- ]
933
- },
934
- {
935
- "role": "user",
936
- "content": [
937
- {
938
- "type": "text",
939
- "text": user_context
940
- }
941
- ]
942
- }
943
- ],
944
- response_format={
945
- "type": "json_schema",
946
- "json_schema": {
947
- "name": "life_status_report",
948
- "strict": True,
949
- "schema": {
950
- "type": "object",
951
- "properties": {
952
- "mantra_of_the_week": {
953
- "type": "string",
954
- "description": "A very short encouragement quote that encapsulates the user's journey to achieve their goals."
955
- }
956
- },
957
- "required": [
958
- "mantra_of_the_week"
959
- ],
960
- "additionalProperties": False
961
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
962
  }
963
  }
964
- ,
965
- temperature=0.5,
966
- max_tokens=3000,
967
- top_p=1,
968
- frequency_penalty=0,
969
- presence_penalty=0
970
- )
971
-
972
- # Get response and convert into dictionary
973
- mantra = json.loads(response.choices[0].message.content)["mantra_of_the_week"]
974
 
975
- except Exception as e:
976
- logger.error(f"OpenAI API call failed: {e}", extra={'user_id': user_id, 'endpoint': function_name})
977
- raise e
978
 
979
  # Get current life score
980
  life_score = {
@@ -1002,14 +1002,15 @@ def get_user_life_status(user_id):
1002
  logger.info(f"User life status generated successfully for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
1003
  return reports
1004
 
1005
- def get_api_key(api_key_header: str = Security(api_key_header)) -> str:
1006
- if api_key_header == os.getenv("FASTAPI_KEY"):
1007
- return api_key_header
1008
- raise HTTPException(
1009
- status_code=403,
1010
- detail="Could not validate credentials"
1011
- )
1012
 
 
1013
  def get_user_info(user_id):
1014
  function_name = get_user_info.__name__
1015
  logger.info(f"Retrieving user info for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
@@ -1064,11 +1065,13 @@ def get_user_info(user_id):
1064
  return user_data_formatted, user_data_clean.get('mattersMost', ['', '', '', '', ''])
1065
  else:
1066
  logger.warning(f"No user info found for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
1067
- return None
 
1068
  except psycopg2.Error as e:
1069
  logger.error(f"Database error while retrieving user info for {user_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1070
- return None
1071
 
 
1072
  def get_growth_guide_summary(user_id, session_id):
1073
  function_name = get_growth_guide_summary.__name__
1074
  logger.info(f"Retrieving growth guide summary for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
@@ -1095,8 +1098,10 @@ def get_growth_guide_summary(user_id, session_id):
1095
  return None
1096
  except psycopg2.Error as e:
1097
  logger.error(f"Database error while retrieving growth guide summary for user {user_id} and session {session_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1098
- return None
 
1099
 
 
1100
  def get_all_bookings():
1101
  function_name = get_all_bookings.__name__
1102
  logger.info(f"Retrieving all bookings", extra={'endpoint': function_name})
@@ -1117,9 +1122,13 @@ def get_all_bookings():
1117
  logger.info(f"Retrieved {len(bookings)} bookings", extra={'endpoint': function_name})
1118
  return bookings
1119
  except psycopg2.Error as e:
 
1120
  logger.error(f"Database error while retrieving bookings: {e}", extra={'endpoint': function_name})
1121
- return []
 
 
1122
 
 
1123
  def update_growth_guide_summary(user_id, session_id, ourcoach_summary):
1124
  function_name = update_growth_guide_summary.__name__
1125
  logger.info(f"Updating growth guide summary for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
@@ -1145,8 +1154,9 @@ def update_growth_guide_summary(user_id, session_id, ourcoach_summary):
1145
  logger.info(f"Growth guide summary updated successfully for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
1146
  except psycopg2.Error as e:
1147
  logger.error(f"Database error while updating growth guide summary: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1148
- raise e
1149
 
 
1150
  def add_growth_guide_session(user_id, session_id, coach_id, session_started_at, zoom_ai_summary, gg_report, ourcoach_summary):
1151
  function_name = add_growth_guide_session.__name__
1152
  logger.info(f"Adding growth guide session for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
@@ -1182,8 +1192,9 @@ def add_growth_guide_session(user_id, session_id, coach_id, session_started_at,
1182
  logger.info(f"Growth guide session added successfully for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
1183
  except psycopg2.Error as e:
1184
  logger.error(f"Database error while adding growth guide session: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1185
- raise e
1186
 
 
1187
  def get_growth_guide_session(user_id, session_id):
1188
  # returns the zoom_ai_summary and the gg_report columns from the POST_GG table
1189
  function_name = get_growth_guide_session.__name__
@@ -1211,9 +1222,10 @@ def get_growth_guide_session(user_id, session_id):
1211
  return None
1212
  except psycopg2.Error as e:
1213
  logger.error(f"Database error while retrieving growth guide session for user {user_id} and session {session_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1214
- return None
1215
 
1216
 
 
1217
  def download_file_from_s3(filename, bucket):
1218
  user_id = filename.split('.')[0]
1219
  function_name = download_file_from_s3.__name__
@@ -1234,8 +1246,9 @@ def download_file_from_s3(filename, bucket):
1234
  logger.error(f"Error downloading file {filename} from S3: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1235
  if (os.path.exists(file_path)):
1236
  os.remove(file_path)
1237
- return False
1238
 
 
1239
  def add_to_cache(user):
1240
  user_id = user.user_id
1241
  function_name = add_to_cache.__name__
@@ -1244,6 +1257,7 @@ def add_to_cache(user):
1244
  logger.info(f"User {user_id} added to the cache", extra={'user_id': user_id, 'endpoint': function_name})
1245
  return True
1246
 
 
1247
  def pop_cache(user_id):
1248
  if user_id == 'all':
1249
  user_cache.reset_cache()
@@ -1256,13 +1270,13 @@ def pop_cache(user_id):
1256
  # upload file
1257
  logger.info(f"Attempting upload file {user_id}.json to S3", extra={'user_id': user_id, 'endpoint': 'pop_cache'})
1258
  upload_file_to_s3(f"{user_id}.pkl")
1259
- try:
1260
- user_cache.pop(user_id, None)
1261
- logger.info(f"User {user_id} has been removed from the cache", extra={'user_id': user_id, 'endpoint': 'pop_cache'})
1262
- return True
1263
- except:
1264
- return False
1265
 
 
 
 
 
 
 
1266
  def update_user(user):
1267
  user_id = user.user_id
1268
  function_name = update_user.__name__
@@ -1276,6 +1290,7 @@ def update_user(user):
1276
 
1277
  return True
1278
 
 
1279
  def upload_mementos_to_db(user_id):
1280
  function_name = upload_mementos_to_db.__name__
1281
  logger.info(f"Uploading mementos to DB for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
@@ -1345,11 +1360,9 @@ def upload_mementos_to_db(user_id):
1345
  return True
1346
  except psycopg2.Error as e:
1347
  logger.error(f"Database error while uploading mementos: {str(e)}", extra={'user_id': user_id, 'endpoint': function_name})
1348
- raise ConnectionError(f"Database error: {str(e)}")
1349
- except Exception as e:
1350
- logger.error(f"Unexpected error uploading mementos: {str(e)}", extra={'user_id': user_id, 'endpoint': function_name})
1351
- return False
1352
 
 
1353
  def get_users_mementos(user_id, date):
1354
  function_name = get_users_mementos.__name__
1355
  db_params = {
@@ -1383,8 +1396,40 @@ def get_users_mementos(user_id, date):
1383
  logger.info(f"No mementos found for user {user_id} on date {date}", extra={'endpoint': function_name, 'user_id': user_id})
1384
  return []
1385
  except psycopg2.Error as e:
 
1386
  logger.error(f"Database error while retrieving mementos: {e}", extra={'endpoint': function_name, 'user_id': user_id})
1387
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1388
 
1389
  def generate_uuid():
1390
  return str(uuid.uuid4())
 
6
  from fastapi import FastAPI, HTTPException, Security, Query, status
7
  from fastapi.security import APIKeyHeader
8
  from openai import OpenAI
9
+ import openai
10
  import pandas as pd
11
  from pydantic import BaseModel
12
  import os
 
29
  import secrets
30
  import string
31
 
32
+ from app.exceptions import BaseOurcoachException, DBError, OpenAIRequestError, UtilsError
33
+
34
  load_dotenv()
35
 
36
  # Environment Variables for API Keys
 
48
  # Replace the simple TTLCache with our custom implementation
49
  user_cache = CustomTTLCache(ttl=120, cleanup_interval=30) # 2 minutes TTL
50
 
51
+ def catch_error(func):
52
+ def wrapper(*args, **kwargs):
53
+ try:
54
+ return func(*args, **kwargs)
55
+ except BaseOurcoachException as e:
56
+ raise e
57
+ except openai.BadRequestError as e:
58
+ raise OpenAIRequestError(user_id='no-user', message="Bad Request to OpenAI", code="OpenAIError")
59
+ except Exception as e:
60
+ # Handle other exceptions
61
+ logger.error(f"An unexpected error occurred in Utils: {e}")
62
+ raise UtilsError(user_id='no-user', message="Unexpected error in Utils", e=str(e))
63
+ return wrapper
64
+
65
+ @catch_error
66
  def force_file_move(source, destination):
67
  function_name = force_file_move.__name__
68
  logger.info(f"Attempting to move file from {source} to {destination}", extra={'endpoint': function_name})
69
+ # Ensure the destination directory exists
70
+ os.makedirs(os.path.dirname(destination), exist_ok=True)
71
+
72
+ # Move the file, replacing if it already exists
73
+ os.replace(source, destination)
74
+ logger.info(f"File moved successfully: {source} -> {destination}", extra={'endpoint': function_name})
 
 
 
 
 
75
 
76
+ @catch_error
77
  def get_user(user_id):
78
  function_name = get_user.__name__
79
  logger.info(f"Fetching user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
83
  return user_cache[user_id]
84
  else:
85
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
86
+ if not client:
87
+ raise OpenAIRequestError(user_id=user_id, message="Error creating OpenAI client", code="OpenAIError")
88
  user_file = os.path.join('users', 'data', f'{user_id}.pkl')
89
  # if os.path.exists(user_file):
90
  # with open(user_file, 'rb') as f:
 
112
  user_info = get_user_info(user_id)
113
  if (user_info):
114
  # user has done onboarding but pickle file not created
115
+ raise DBError(user_id=user_id, message="User has done onboarding but pickle file not created", code="NoPickleError")
116
+ raise DBError(user_id=user_id, message="User has not onboarded yet", code="NoOnboardingError")
117
 
118
+ @catch_error
119
  def generate_html(json_data, coach_name='Growth Guide', booking_id = None):
120
  function_name = generate_html.__name__
121
  data = json_data["pre_growth_guide_session_report"]
 
293
  password = "Ourcoach2024!"
294
 
295
  ## SAVING HTML FILE
296
+ # Open the file in write mode
297
+ with open(file_path, 'w', encoding='utf-8') as html_file:
298
+ html_file.write(html_content)
299
+ logger.info(f"File '{booking_id}.html' has been created successfully.", extra={'booking_id': booking_id, 'endpoint': function_name})
300
+
301
+ # Saving as PDF File
302
+ pdfkit.from_file(file_path, path_to_upload, options={'encoding': 'UTF-8'})
303
+ logger.info(f"File '{booking_id}.pdf' has been created successfully.", extra={'booking_id': booking_id, 'endpoint': function_name})
304
+
305
+ ## ENCRYPTING PDF
306
+ logger.info(f"Encrypting '{booking_id}.pdf'...", extra={'booking_id': booking_id, 'endpoint': function_name})
307
+ with open(path_to_upload, 'rb') as file:
308
+ pdf_reader = PyPDF2.PdfReader(file)
309
+ pdf_writer = PyPDF2.PdfWriter()
310
+
311
+ # Add all pages to the writer
312
+ for page_num in range(len(pdf_reader.pages)):
313
+ pdf_writer.add_page(pdf_reader.pages[page_num])
314
+
315
+ # Encrypt the PDF with the given password
316
+ pdf_writer.encrypt(password)
317
+
318
+ with open(path_to_upload, 'wb') as encrypted_file:
319
+ pdf_writer.write(encrypted_file)
320
+
321
+ logger.info(f"Succesfully encrypted '{booking_id}.pdf'", extra={'booking_id': booking_id, 'endpoint': function_name})
 
 
 
 
 
322
 
323
  filename = booking_id
324
 
 
343
 
344
  # force_file_move(os.path.join('users', 'to_upload', filename), os.path.join('users', 'data', filename))
345
  except (FileNotFoundError, NoCredentialsError, PartialCredentialsError) as e:
346
+ raise DBError(user_id="no-user", message="Error uploading file to S3", code="S3Error")
 
347
 
348
+ @catch_error
349
  def get_user_summary(user_id):
350
  function_name = get_user_summary.__name__
351
  logger.info(f"Generating user summary for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
352
 
353
  # Step 1: Call get_user to get user's info
354
+ user = get_user(user_id)
355
+ user_info = user.user_info
356
+ user_messages = user.get_messages()
357
+ user_goal = '' if not user.goal else user.goal[-1].content
 
 
 
358
 
359
  # Step 2: Construct the Prompt
360
  chat_history = "\n".join(
 
427
  ### **2. User's Growth Guide Preparation Brief**
428
 
429
  **Objective**: Guide the user on what to discuss with the Growth Guide, providing actionable advice and highlighting key areas to focus on during their session, covering the five key areas.
430
+ You must use the user's current **challenges** and **life goal** to make the preparation brief **personalized**!
431
 
432
+ Important Rules:
433
+ 1. **ALWAYS** be succinct, valuable and personalized! Do **NOT** ask generic question. Ask a personalized question! And bold the key parts of the user brief!
434
+ 2. **Session Length Awareness**: Be realistic about what can be effectively discussed in a 30-minute session. Prioritize the areas that are most pressing or offer the greatest opportunity for positive change.
435
+ 3. **Guidance for Interaction**: Provide specific suggestions for topics to discuss with the **Growth Guide**, you are encouraged to use phrases like "Discuss with your Growth Guide how to...".
436
+ 4. And for the second time, please be succinct and concise!!!
437
 
438
  **Format**:
439
 
440
+ Structure the brief with the following sections, and output it as a JSON object with these keys (don't forget to BE CONCISE! and you **must** bold some words that you think is important! but it does **not** have to be the first few words!):
441
 
442
+ - **reflect**: Provide personalized advice that encourages the user to contemplate their specific experiences, feelings, and thoughts related to each of the five key areas. Help them identify particular aspects they wish to improve, based on their challenges and goals.
443
 
444
+ - **recall_successes**: Prompt the user to remember past occasions when they effectively managed or made improvements in these areas. Encourage them to consider the strategies, habits, or resources that contributed to these successes, and how they might apply them now.
445
 
446
+ - **identify_challenges**: Advise the user to acknowledge current obstacles they are facing in each area. Encourage them to think critically about these challenges and consider potential solutions or support systems that could assist in overcoming them.
447
 
448
+ - **set_goals**: Encourage the user to define clear and achievable objectives for the upcoming session. Guide them to consider how making improvements in each key area can positively impact their overall well-being and life satisfaction.
449
 
450
+ - **additional_tips**: Offer practical advice to help the user prepare for the session. Suggestions may include arranging a quiet and comfortable space, gathering any relevant materials or notes, and approaching the session with openness and honesty.
451
 
452
  ---
453
 
 
550
  "users_growth_guide_preparation_brief": [
551
  {
552
  "key": "reflect",
553
+ "value": "⁠..."
554
  },
555
  {
556
  "key": "recall_successes",
557
+ "value": "⁠..."
558
  },
559
  {
560
  "key": "identify_challenges",
561
+ "value": "..."
562
  },
563
  {
564
  "key": "set_goals",
565
+ "value": "⁠..."
566
  },
567
  {
568
  "key": "additional_tips",
569
+ "value": "⁠..."
570
  }
571
  ],
572
  "30_minute_coaching_session_script": {
 
610
 
611
  # Step 3: Call the OpenAI API using the specified function
612
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
613
+ response = client.chat.completions.create(
614
+ model="gpt-4o",
615
+ messages=[
616
+ {
617
+ "role": "system",
618
+ "content": [
619
+ {
620
+ "type": "text",
621
+ "text": system_prompt
622
+ }
623
+ ]
624
+ },
625
+ {
626
+ "role": "user",
627
+ "content": [
628
+ {
629
+ "type": "text",
630
+ "text": user_context
631
+ }
632
+ ]
633
+ }
634
+ ],
635
+ response_format={
636
+ "type": "json_schema",
637
+ "json_schema": {
638
+ "name": "growth_guide_session",
639
+ "strict": True,
640
+ "schema": {
641
+ "type": "object",
642
+ "properties": {
643
+ "pre_growth_guide_session_report": {
644
  "type": "object",
645
+ "description": "A comprehensive summary of the user's profile and life context for the Growth Guide.",
646
  "properties": {
647
+ "user_overview": {
648
  "type": "object",
 
649
  "properties": {
650
+ "name": {
651
+ "type": "string",
652
+ "description": "The user's full name."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  },
654
+ "age_group": {
655
+ "type": "string",
656
+ "description": "The user's age range (e.g., '30-39')."
657
  },
658
+ "primary_goals": {
659
+ "type": "string",
660
+ "description": "The main goals the user is focusing on."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  },
662
+ "preferred_coaching_style": {
663
+ "type": "string",
664
+ "description": "The coaching style the user prefers."
 
 
 
 
 
665
  }
666
  },
667
+ "required": ["name", "age_group", "primary_goals", "preferred_coaching_style"],
668
  "additionalProperties": False
669
  },
670
+ "personality_insights": {
671
+ "type": "object",
672
+ "properties": {
673
+ "mbti": {
674
+ "type": "string",
675
+ "description": "The user's Myers-Briggs Type Indicator personality type."
676
+ },
677
+ "top_love_languages": {
678
  "type": "array",
 
679
  "items": {
680
+ "type": "string"
 
 
 
 
 
 
 
 
 
681
  },
682
+ "description": "A list of the user's top two love languages."
683
+ },
684
+ "belief_in_astrology": {
685
+ "type": "string",
686
+ "description": "Whether the user believes in horoscope/astrology."
687
  }
688
  },
689
+ "required": ["mbti", "top_love_languages", "belief_in_astrology"],
690
+ "additionalProperties": False
691
+ },
692
+ "progress_snapshot": {
693
  "type": "object",
 
694
  "properties": {
695
+ "mental_well_being": {
696
+ "type": "string",
697
+ "description": "Summary of the user's mental well-being."
698
+ },
699
+ "physical_health_and_wellness": {
700
+ "type": "string",
701
+ "description": "Summary of the user's physical health and wellness."
702
+ },
703
+ "relationships": {
704
+ "type": "string",
705
+ "description": "Summary of the user's relationships."
706
+ },
707
+ "career_growth": {
708
+ "type": "string",
709
+ "description": "Summary of the user's career growth."
710
+ },
711
+ "personal_growth": {
712
+ "type": "string",
713
+ "description": "Summary of the user's personal growth."
714
+ }
715
+ },
716
+ "required": [
717
+ "mental_well_being",
718
+ "physical_health_and_wellness",
719
+ "relationships",
720
+ "career_growth",
721
+ "personal_growth"
722
+ ],
723
+ "additionalProperties": False
724
+ }
725
+ },
726
+ "required": ["user_overview", "personality_insights", "progress_snapshot"],
727
+ "additionalProperties": False
728
+ },
729
+ "users_growth_guide_preparation_brief": {
730
+ "type": "array",
731
+ "description": "A brief guiding the user on what to discuss with the Growth Guide, providing actionable advice and highlighting key areas to focus on.",
732
+ "items": {
733
+ "type": "object",
734
+ "properties": {
735
+ "key": {
736
+ "type": "string",
737
+ "description": "The section heading."
738
+ },
739
+ "value": {
740
+ "type": "string",
741
+ "description": "Content for the section."
742
+ }
743
+ },
744
+ "required": [
745
+ "key",
746
+ "value"
747
+ ],
748
+ "additionalProperties": False
749
+ }
750
+ },
751
+ "30_minute_coaching_session_script": {
752
+ "type": "object",
753
+ "description": "A detailed, partitioned script to help the coach prepare for the session, following the specified session order and focusing on the user's top three most important areas.",
754
+ "properties": {
755
+ "session_overview": {
756
+ "type": "array",
757
+ "items": {
758
+ "type": "string"
759
+ },
760
+ "description": "Breakdown of the session segments with time frames."
761
+ },
762
+ "detailed_segments": {
763
+ "type": "array",
764
+ "items": {
765
+ "type": "object",
766
+ "properties": {
767
+ "segment_title": {
768
+ "type": "string",
769
+ "description": "Title of the session segment."
770
+ },
771
+ "coach_dialogue": {
772
  "type": "array",
773
  "items": {
774
  "type": "string"
775
  },
776
+ "description": "Suggested coach dialogue during the session"
777
  },
778
+ "guidance": {
779
  "type": "array",
780
  "items": {
781
+ "type": "string"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782
  },
783
+ "description": "Suggestions for the coach on how to navigate responses."
 
 
 
784
  }
785
+ },
786
+ "required": ["segment_title", "coach_dialogue", "guidance"],
787
+ "additionalProperties": False
788
  },
789
+ "description": "Detailed information for each session segment."
 
 
 
 
790
  }
791
  },
792
  "required": [
793
+ "session_overview",
794
+ "detailed_segments"
 
795
  ],
796
  "additionalProperties": False
797
  }
798
+ },
799
+ "required": [
800
+ "pre_growth_guide_session_report",
801
+ "users_growth_guide_preparation_brief",
802
+ "30_minute_coaching_session_script"
803
+ ],
804
+ "additionalProperties": False
805
  }
806
+ }
807
+ }
808
+ ,
809
+ temperature=0.5,
810
+ max_tokens=3000,
811
+ top_p=1,
812
+ frequency_penalty=0,
813
+ presence_penalty=0
814
+ )
815
 
816
+ # Get response and convert into dictionary
817
+ reports = json.loads(response.choices[0].message.content)
818
+ # html_output = generate_html(reports, coach_name)
819
+ # reports['html_report'] = html_output
820
 
821
+ # Store users_growth_guide_preparation_brief in the User object
822
+ user.set_recommened_gg_topics(reports['users_growth_guide_preparation_brief'])
 
823
 
824
  # Step 4: Return the JSON reports
825
  logger.info(f"User summary generated successfully for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
826
  return reports
827
 
828
+ @catch_error
829
  def create_pre_gg_report(booking_id):
830
  function_name = create_pre_gg_report.__name__
831
 
832
  # Get user_id from booking_id
833
+ logger.info(f"Retrieving booking details for {booking_id}", extra={'booking_id': booking_id, 'endpoint': function_name})
834
+ db_params = {
835
+ 'dbname': 'ourcoach',
836
+ 'user': 'ourcoach',
837
+ 'password': 'hvcTL3kN3pOG5KteT17T',
838
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
839
+ 'port': '5432'
840
+ }
841
  try:
842
+ with psycopg2.connect(**db_params) as conn:
843
+ with conn.cursor() as cursor:
844
+ query = sql.SQL("""
845
+ select user_id
846
+ from {table}
847
+ where id = %s
848
+ """
849
+ ).format(table=sql.Identifier('public', 'booking'))
850
+ cursor.execute(query, (booking_id,))
851
+ row = cursor.fetchone()
852
+ if (row):
853
+ colnames = [desc[0] for desc in cursor.description]
854
+ booking_data = dict(zip(colnames, row))
855
+ ### MODIFY THE FORMAT OF USER DATA
856
+ user_id = booking_data['user_id']
857
+ logger.info(f"User info retrieved successfully for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
858
+ else:
859
+ logger.warning(f"No user info found for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
860
+ except psycopg2.Error as e:
861
+ logger.error(f"Database error while retrieving user info for {user_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
862
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
863
+
864
+ # Run get_user_summary
865
+ user_report = get_user_summary(user_id)
 
 
 
 
 
 
 
 
 
866
 
867
+ # Run generate_html
868
+ generate_html(user_report, booking_id=booking_id)
869
+
870
+ return True
 
 
 
871
 
872
+ @catch_error
873
  def get_user_life_status(user_id):
874
  function_name = get_user_life_status.__name__
875
  logger.info(f"Generating user life status for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
876
 
877
+ user = get_user(user_id)
878
+ user_info = user.user_info
879
+ user_messages = user.get_messages()
 
 
 
 
 
880
 
881
  # Step 2: Construct the Prompt
882
  chat_history = "\n".join(
 
923
 
924
  # Step 3: Call the OpenAI API using the specified function
925
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
926
+ response = client.chat.completions.create(
927
+ model="gpt-4o-mini",
928
+ messages=[
929
+ {
930
+ "role": "system",
931
+ "content": [
932
+ {
933
+ "type": "text",
934
+ "text": system_prompt
935
+ }
936
+ ]
937
+ },
938
+ {
939
+ "role": "user",
940
+ "content": [
941
+ {
942
+ "type": "text",
943
+ "text": user_context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
944
  }
945
+ ]
946
+ }
947
+ ],
948
+ response_format={
949
+ "type": "json_schema",
950
+ "json_schema": {
951
+ "name": "life_status_report",
952
+ "strict": True,
953
+ "schema": {
954
+ "type": "object",
955
+ "properties": {
956
+ "mantra_of_the_week": {
957
+ "type": "string",
958
+ "description": "A very short encouragement quote that encapsulates the user's journey to achieve their goals."
959
+ }
960
+ },
961
+ "required": [
962
+ "mantra_of_the_week"
963
+ ],
964
+ "additionalProperties": False
965
  }
966
  }
967
+ }
968
+ ,
969
+ temperature=0.5,
970
+ max_tokens=3000,
971
+ top_p=1,
972
+ frequency_penalty=0,
973
+ presence_penalty=0
974
+ )
 
 
975
 
976
+ # Get response and convert into dictionary
977
+ mantra = json.loads(response.choices[0].message.content)["mantra_of_the_week"]
 
978
 
979
  # Get current life score
980
  life_score = {
 
1002
  logger.info(f"User life status generated successfully for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
1003
  return reports
1004
 
1005
+ async def get_api_key(api_key_header: str = Security(api_key_header)) -> str:
1006
+ if api_key_header not in api_keys: # Check against list of valid keys
1007
+ raise HTTPException(
1008
+ status_code=status.HTTP_403_FORBIDDEN,
1009
+ detail="Invalid API key"
1010
+ )
1011
+ return api_key_header
1012
 
1013
+ @catch_error
1014
  def get_user_info(user_id):
1015
  function_name = get_user_info.__name__
1016
  logger.info(f"Retrieving user info for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1065
  return user_data_formatted, user_data_clean.get('mattersMost', ['', '', '', '', ''])
1066
  else:
1067
  logger.warning(f"No user info found for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
1068
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="NoOnboardingError", e=str(e))
1069
+
1070
  except psycopg2.Error as e:
1071
  logger.error(f"Database error while retrieving user info for {user_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1072
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
1073
 
1074
+ @catch_error
1075
  def get_growth_guide_summary(user_id, session_id):
1076
  function_name = get_growth_guide_summary.__name__
1077
  logger.info(f"Retrieving growth guide summary for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1098
  return None
1099
  except psycopg2.Error as e:
1100
  logger.error(f"Database error while retrieving growth guide summary for user {user_id} and session {session_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1101
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
1102
+
1103
 
1104
+ @catch_error
1105
  def get_all_bookings():
1106
  function_name = get_all_bookings.__name__
1107
  logger.info(f"Retrieving all bookings", extra={'endpoint': function_name})
 
1122
  logger.info(f"Retrieved {len(bookings)} bookings", extra={'endpoint': function_name})
1123
  return bookings
1124
  except psycopg2.Error as e:
1125
+ bookings = []
1126
  logger.error(f"Database error while retrieving bookings: {e}", extra={'endpoint': function_name})
1127
+ raise DBError(user_id='no-user', message="Error retrieving user info", code="SQLError", e=str(e))
1128
+ finally:
1129
+ return bookings
1130
 
1131
+ @catch_error
1132
  def update_growth_guide_summary(user_id, session_id, ourcoach_summary):
1133
  function_name = update_growth_guide_summary.__name__
1134
  logger.info(f"Updating growth guide summary for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1154
  logger.info(f"Growth guide summary updated successfully for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
1155
  except psycopg2.Error as e:
1156
  logger.error(f"Database error while updating growth guide summary: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1157
+ raise DBError(user_id=user_id, message="Error updating growth guide summary", code="SQLError", e=str(e))
1158
 
1159
+ @catch_error
1160
  def add_growth_guide_session(user_id, session_id, coach_id, session_started_at, zoom_ai_summary, gg_report, ourcoach_summary):
1161
  function_name = add_growth_guide_session.__name__
1162
  logger.info(f"Adding growth guide session for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1192
  logger.info(f"Growth guide session added successfully for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
1193
  except psycopg2.Error as e:
1194
  logger.error(f"Database error while adding growth guide session: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1195
+ raise DBError(user_id=user_id, message="Error adding growth guide session", code="SQLError", e=str(e))
1196
 
1197
+ @catch_error
1198
  def get_growth_guide_session(user_id, session_id):
1199
  # returns the zoom_ai_summary and the gg_report columns from the POST_GG table
1200
  function_name = get_growth_guide_session.__name__
 
1222
  return None
1223
  except psycopg2.Error as e:
1224
  logger.error(f"Database error while retrieving growth guide session for user {user_id} and session {session_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1225
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
1226
 
1227
 
1228
+ @catch_error
1229
  def download_file_from_s3(filename, bucket):
1230
  user_id = filename.split('.')[0]
1231
  function_name = download_file_from_s3.__name__
 
1246
  logger.error(f"Error downloading file {filename} from S3: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1247
  if (os.path.exists(file_path)):
1248
  os.remove(file_path)
1249
+ raise DBError(user_id=user_id, message="Error downloading file from S3", code="S3Error", e=str(e))
1250
 
1251
+ @catch_error
1252
  def add_to_cache(user):
1253
  user_id = user.user_id
1254
  function_name = add_to_cache.__name__
 
1257
  logger.info(f"User {user_id} added to the cache", extra={'user_id': user_id, 'endpoint': function_name})
1258
  return True
1259
 
1260
+ @catch_error
1261
  def pop_cache(user_id):
1262
  if user_id == 'all':
1263
  user_cache.reset_cache()
 
1270
  # upload file
1271
  logger.info(f"Attempting upload file {user_id}.json to S3", extra={'user_id': user_id, 'endpoint': 'pop_cache'})
1272
  upload_file_to_s3(f"{user_id}.pkl")
 
 
 
 
 
 
1273
 
1274
+ user_cache.pop(user_id, None)
1275
+ logger.info(f"User {user_id} has been removed from the cache", extra={'user_id': user_id, 'endpoint': 'pop_cache'})
1276
+ return True
1277
+
1278
+
1279
+ @catch_error
1280
  def update_user(user):
1281
  user_id = user.user_id
1282
  function_name = update_user.__name__
 
1290
 
1291
  return True
1292
 
1293
+ @catch_error
1294
  def upload_mementos_to_db(user_id):
1295
  function_name = upload_mementos_to_db.__name__
1296
  logger.info(f"Uploading mementos to DB for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1360
  return True
1361
  except psycopg2.Error as e:
1362
  logger.error(f"Database error while uploading mementos: {str(e)}", extra={'user_id': user_id, 'endpoint': function_name})
1363
+ raise DBError(user_id=user_id, message="Error uploading mementos", code="SQLError", e=str(e))
 
 
 
1364
 
1365
+ @catch_error
1366
  def get_users_mementos(user_id, date):
1367
  function_name = get_users_mementos.__name__
1368
  db_params = {
 
1396
  logger.info(f"No mementos found for user {user_id} on date {date}", extra={'endpoint': function_name, 'user_id': user_id})
1397
  return []
1398
  except psycopg2.Error as e:
1399
+ mementos = []
1400
  logger.error(f"Database error while retrieving mementos: {e}", extra={'endpoint': function_name, 'user_id': user_id})
1401
+ raise DBError(user_id=user_id, message="Error retrieving mementos", code="SQLError", e=str(e))
1402
+ finally:
1403
+ return mementos
1404
+
1405
+ @catch_error
1406
+ def get_user_subscriptions(user_id):
1407
+ function_name = get_user_subscriptions.__name__
1408
+ logger.info(f"Retrieving subscriptions for user {user_id}", extra={'endpoint': function_name})
1409
+ db_params = {
1410
+ 'dbname': 'ourcoach',
1411
+ 'user': 'ourcoach',
1412
+ 'password': 'hvcTL3kN3pOG5KteT17T',
1413
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
1414
+ 'port': '5432'
1415
+ }
1416
+ try:
1417
+ with psycopg2.connect(**db_params) as conn:
1418
+ with conn.cursor() as cursor:
1419
+ query = sql.SQL("""
1420
+ SELECT * FROM {table}
1421
+ WHERE user_id = %s
1422
+ ORDER BY period_started DESC
1423
+ """).format(table=sql.Identifier('public', 'user_subscription'))
1424
+ cursor.execute(query, (user_id,))
1425
+ rows = cursor.fetchall()
1426
+ colnames = [desc[0] for desc in cursor.description]
1427
+ df = pd.DataFrame(rows, columns=colnames)
1428
+ logger.info(f"Retrieved {len(df)} subscriptions for user {user_id}", extra={'endpoint': function_name})
1429
+ return df
1430
+ except psycopg2.Error as e:
1431
+ logger.error(f"Database error while retrieving user subscriptions: {e}", extra={'endpoint': function_name})
1432
+ raise DBError(user_id=user_id, message="Error retrieving user subscriptions", code="SQLError", e=str(e))
1433
 
1434
  def generate_uuid():
1435
  return str(uuid.uuid4())