Persona Integration & Modified User Context

#11
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_id, 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)
@@ -236,8 +257,6 @@ class LoggingMiddleware(BaseHTTPMiddleware):
236
  raise
237
 
238
  # OpenAI Client
239
- # GENERAL_ASSISTANT = os.getenv('OPENAI_GENERAL_ASSISTANT')
240
- GENERAL_ASSISTANT = "asst_vnucWWELJlCWadfAARwyKkCW"
241
 
242
  # Initialize Logging (optional)
243
  # logging.basicConfig(filename='app.log', level=logging.INFO)
@@ -254,229 +273,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 +556,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 +636,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 +702,152 @@ 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 +872,167 @@ 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)
 
257
  raise
258
 
259
  # OpenAI Client
 
 
260
 
261
  # Initialize Logging (optional)
262
  # logging.basicConfig(filename='app.log', level=logging.INFO)
 
273
  user_id: str
274
  message: str
275
 
 
 
 
 
 
 
 
 
276
  class PersonaItem(BaseModel):
277
  user_id: str
278
  persona: str
279
 
280
  class GGItem(BaseModel):
281
+ user_id: str
282
  gg_session_id: str
283
+
284
+ class AssistantItem(BaseModel):
285
  user_id: str
286
+ assistant_id: str
287
 
288
+ class ChangeDateItem(BaseModel):
289
+ user_id: str
290
+ date: str
 
 
291
 
292
  class BookingItem(BaseModel):
293
  booking_id: str
294
 
295
+ def catch_endpoint_error(func):
296
+ """Decorator to handle errors in FastAPI endpoints"""
297
+ @wraps(func) # Add this to preserve endpoint metadata
298
+ async def wrapper(*args, **kwargs):
299
+ try:
300
+ # Extract api_key from kwargs if present and pass it to the wrapped function
301
+ api_key = kwargs.pop('api_key', None)
302
+ return await func(*args, **kwargs)
303
+ except OpenAIRequestError as e:
304
+ # OpenAI service error
305
+ # Try to cancel the run so we dont get "Cannot add message to thread with active run"
306
+ # if e.run_id:
307
+ # user_id = e.user_id
308
+ # if user_id != 'no-user':
309
+ # user = get_user(user_id)
310
+ # user.cancel_run(e.run_id)
311
+ logger.error(f"OpenAI service error in {func.__name__}(...): {str(e)}",
312
+ extra={
313
+ 'user_id': e.user_id,
314
+ 'endpoint': func.__name__
315
+ })
316
+ # Extract thread_id and run_id from error message
317
+ thread_match = re.search(r'thread_(\w+)', str(e))
318
+ run_match = re.search(r'run_(\w+)', str(e))
319
+ if thread_match and run_match:
320
+ thread_id = f"thread_{thread_match.group(1)}"
321
+ run_id = f"run_{run_match.group(1)}"
322
+ user = get_user(e.user_id)
323
+ logger.info(f"Cancelling run {run_id} for thread {thread_id}", extra={"user_id": e.user_id, "endpoint": func.__name__})
324
+ user.cancel_run(run_id, thread_id)
325
+ logger.info(f"Run {run_id} cancelled for thread {thread_id}", extra={"user_id": e.user_id, "endpoint": func.__name__})
326
 
327
+ raise HTTPException(
328
+ status_code=status.HTTP_502_BAD_GATEWAY,
329
+ detail=e.get_formatted_details()
330
+ )
331
+ except DBError as e:
332
+ # check if code is one of ["NoOnboardingError", "NoBookingError"] if yes then return code 404 otherwise 500
333
+ if e.code == "NoOnboardingError" or e.code == "NoBookingError":
334
+ # no onboarding or booking data (user not found)
335
+ status_code = 404
336
+ else:
337
+ status_code = 505
338
+ logger.error(f"Database error in {func.__name__}: {str(e)}",
339
+ extra={
340
+ 'user_id': e.user_id,
341
+ 'endpoint': func.__name__
342
+ })
343
+ raise HTTPException(
344
+ status_code=status_code,
345
+ detail=e.get_formatted_details()
346
+ )
347
+ except (UserError, AssistantError, ConversationManagerError, UtilsError) as e:
348
+ # Known internal errors
349
+ logger.error(f"Internal error in {func.__name__}: {str(e)}",
350
+ extra={
351
+ 'user_id': e.user_id,
352
+ 'endpoint': func.__name__,
353
+ 'traceback': traceback.extract_stack()
354
+ })
355
+ raise HTTPException(
356
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
357
+ # detail = traceback.extract_stack()
358
+ detail=e.get_formatted_details()
359
+ )
360
+ except openai.BadRequestError as e:
361
+ # OpenAI request error
362
+ user_id = kwargs.get('user_id', 'no-user')
363
+ logger.error(f"OpenAI request error in {func.__name__}: {str(e)}",
364
+ extra={
365
+ 'user_id': user_id,
366
+ 'endpoint': func.__name__
367
+ })
368
+ raise HTTPException(
369
+ status_code=status.HTTP_400_BAD_REQUEST,
370
+ detail={
371
+ "type": "OpenAIError",
372
+ "message": str(e),
373
+ "user_id": user_id,
374
+ "at": datetime.now(timezone.utc).isoformat()
375
+ }
376
+ )
377
+ except Exception as e:
378
+ # Unknown errors
379
+ user_id = kwargs.get('user_id', 'no-user')
380
+ if len(args) and hasattr(args[0], 'user_id'):
381
+ user_id = args[0].user_id
382
+
383
+ logger.error(f"Unexpected error in {func.__name__}: {str(e)}",
384
+ extra={
385
+ 'user_id': user_id,
386
+ 'endpoint': func.__name__
387
+ })
388
+ raise HTTPException(
389
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
390
+ detail={
391
+ "type": "FastAPIError",
392
+ "message": str(e),
393
+ "user_id": user_id,
394
+ "at": datetime.now(timezone.utc).isoformat()
395
+ }
396
+ )
397
+ # raise FastAPIError(
398
+ # user_id=user_id,
399
+ # message=f"Unexpected error in {func.__name__}",
400
+ # e=str(e)
401
+ # )
402
+ return wrapper
403
+
404
+ # Apply decorator to all endpoints
405
  @app.post("/set_intro_done")
406
+ @catch_endpoint_error
407
+ async def set_intro_done(
408
+ user_id: str,
409
+ api_key: str = Depends(get_api_key) # Change Security to Depends
410
+ ):
411
  user = get_user(user_id)
412
+
413
  user.set_intro_done()
414
  logger.info("Intro done", extra={"user_id": user_id, "endpoint": "/set_intro_done"})
415
  return {"response": "ok"}
416
 
417
+
418
  @app.post("/set_goal")
419
+ @catch_endpoint_error
420
+ async def set_goal(
421
+ user_id: str,
422
+ goal: str,
423
+ api_key: str = Depends(get_api_key) # Change Security to Depends
424
+ ):
425
+ user = get_user(user_id)
426
+
427
  user.set_goal(goal)
428
  logger.info(f"Goal set: {goal}", extra={"user_id": user_id, "endpoint": "/set_goal"})
429
  return {"response": "ok"}
430
 
431
+ @app.post("/do_micro")
432
+ @catch_endpoint_error
433
+ async def do_micro(
434
+ request: ChangeDateItem,
435
+ day: int,
436
+ api_key: str = Depends(get_api_key) # Change Security to Depends
437
+ ):
438
+ user = get_user(request.user_id)
 
439
  response = user.do_micro(request.date, day)
440
+ logger.info(f"Micro action completed", extra={"user_id": request.user_id, "endpoint": "/do_micro"})
441
+ return {"response": response}
442
+
 
 
 
 
 
 
 
 
 
 
 
 
443
  # endpoint to change user assistant using user.change_to_latest_assistant()
444
  @app.post("/change_assistant")
445
+ @catch_endpoint_error
446
+ async def change_assistant(
447
+ request: AssistantItem,
448
+ api_key: str = Depends(get_api_key) # Change Security to Depends
449
+ ):
450
+ user = get_user(request.user_id)
451
+
452
+ user.change_assistant(request.assistant_id)
453
+ logger.info(f"Assistant changed to {request.assistant_id}",
454
+ extra={"user_id": request.user_id, "endpoint": "/change_assistant"})
455
+ return {"assistant_id": request.assistant_id}
456
+
457
 
458
  @app.post("/clear_cache")
459
+ @catch_endpoint_error
460
+ async def clear_cache(
461
+ api_key: str = Depends(get_api_key) # Change Security to Depends
462
+ ):
463
+ pop_cache(user_id='all')
464
+ logger.info("Cache cleared successfully", extra={"endpoint": "/clear_cache"})
465
+ return {"response": "Cache cleared successfully"}
 
 
 
 
 
 
 
 
466
 
467
  @app.post("/migrate_user")
468
+ @catch_endpoint_error
469
+ async def migrate_user(
470
+ request: CreateUserItem,
471
+ api_key: str = Depends(get_api_key) # Change Security to Depends
472
+ ):
473
+ client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
474
+ if not client:
475
+ raise OpenAIRequestError(
476
+ user_id=request.user_id,
477
+ message="Failed to initialize OpenAI client"
478
+ )
479
+
480
+ user_file = os.path.join('users', 'data', f'{request.user_id}.pkl')
481
+
482
+ download_file_from_s3(f'{request.user_id}.pkl', 'core-ai-assets')
483
+
484
+ with open(user_file, 'rb') as f:
485
+ old_user_object = pickle.load(f)
486
+ user = User(request.user_id, old_user_object.user_info, client, old_user_object.asst_id)
487
+ user.conversations.current_thread = old_user_object.conversations.current_thread
488
+ user.conversations.intro_done = True
489
+ user.done_first_reflection = old_user_object.done_first_reflection
490
+ user.client = client
491
+ user.conversations.client = client
492
+
493
+ api_response = {
494
+ "user": user.user_info,
495
+ "user_messages": user.get_messages(),
496
+ "general_assistant": user.conversations.assistants['general'].id,
497
+ "intro_assistant": user.conversations.assistants['intro'].id,
498
+ "goal": user.goal if user.goal else "No goal is not set yet",
499
+ "current_day": user.growth_plan.current()['day'],
500
+ "micro_actions": user.micro_actions,
501
+ "recommended_actions": user.recommended_micro_actions,
502
+ "challenges": user.challenges,
503
+ "other_focusses": user.other_focusses,
504
+ "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}",
505
+ "recent_wins": user.recent_wins
506
+ }
507
+
508
+ add_to_cache(user)
509
+ pop_cache(user.user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
 
511
+ os.remove(user_file)
512
+ logger.info(f"User {user.user_id} loaded successfully from S3", extra={'user_id': user.user_id, 'endpoint': 'migrate_user'})
513
+ return api_response
514
+
515
  @app.get("/get_user")
516
+ @catch_endpoint_error
517
+ async def get_user_by_id(
518
+ user_id: str,
519
+ api_key: str = Depends(get_api_key) # Change Security to Depends
520
+ ):
521
  print_log("INFO", "Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
522
  logger.info("Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
523
+ user = get_user(user_id)
524
+ print_log("INFO", "Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
525
+ logger.info("Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
526
+ 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}
527
+
528
+ if user.goal:
529
+ api_response["goal"] = user.goal
530
+ else:
531
+ api_response["goal"] = "No goal is not set yet"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
 
533
+ api_response["current_day"] = user.growth_plan.current()['day']
534
+ api_response['micro_actions'] = user.micro_actions
535
+ api_response['recommended_actions'] = user.recommended_micro_actions
536
+ api_response['challenges'] = user.challenges
537
+ api_response['other_focusses'] = user.other_focusses
538
+ api_response['reminders'] = user.reminders
539
+ 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}"
540
+ api_response['recent_wins'] = user.recent_wins
541
+ api_response['last_gg_session'] = user.last_gg_session
542
 
543
+ return api_response
544
 
545
  @app.post("/update_user_persona")
546
+ @catch_endpoint_error
547
  async def update_user_persona(
548
  request: PersonaItem,
549
+ api_key: str = Depends(get_api_key) # Change Security to Depends
550
  ):
551
  """Update user's legendary persona in the database"""
552
  user_id = request.user_id
 
556
  user.update_user_info(f"User's new Legendary Persona is: {persona}")
557
  logger.info(f"Updated persona to {persona}", extra={"user_id": user_id, "endpoint": "/update_user_persona"})
558
 
559
+ # Connect to database
560
+ db_params = {
561
+ 'dbname': 'ourcoach',
562
+ 'user': 'ourcoach',
563
+ 'password': 'hvcTL3kN3pOG5KteT17T',
564
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
565
+ 'port': '5432'
566
+ }
567
+ conn = psycopg2.connect(**db_params)
568
+ cur = conn.cursor()
569
+
570
+ # Get current onboarding data
571
+ cur.execute("SELECT onboarding FROM users WHERE id = %s", (user_id,))
572
+ result = cur.fetchone()
573
+ if not result:
574
+ raise DBError(
575
+ user_id=user_id,
576
+ code="NoOnboardingError",
577
+ message="User not found in database"
 
 
 
 
 
 
 
578
  )
 
579
 
580
+ # Update legendPersona in onboarding JSON
581
+ onboarding = json.loads(result[0])
582
+ onboarding['legendPersona'] = persona
 
583
 
584
+ # Update database
585
+ cur.execute(
586
+ "UPDATE users SET onboarding = %s WHERE id = %s",
587
+ (json.dumps(onboarding), user_id)
588
+ )
589
+ conn.commit()
590
+ if 'cur' in locals():
591
+ cur.close()
592
+ if 'conn' in locals():
593
+ conn.close()
594
+
595
+ return {"status": "success", "message": f"Updated persona to {persona}"}
596
 
597
  @app.post("/add_ai_message")
598
+ @catch_endpoint_error
599
+ async def add_ai_message(
600
+ request: ChatItem,
601
+ api_key: str = Depends(get_api_key) # Change Security to Depends
602
+ ):
603
  user_id = request.user_id
604
  message = request.message
605
  logger.info("Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
606
  print_log("INFO", "Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
607
+
608
+ user = get_user(user_id)
609
+ user.add_ai_message(message)
610
 
611
+ add_to_cache(user)
612
+ pop_cache(user.user_id)
613
+
614
+ print_log("INFO", "AI response added", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
615
+ return {"response": "ok"}
616
+
 
 
 
 
 
 
 
 
 
 
 
 
 
617
 
618
  @app.post("/schedule_gg_reminder")
619
+ @catch_endpoint_error
620
+ async def schedule_gg_reminder(
621
+ request: ChangeDateItem,
622
+ api_key: str = Depends(get_api_key) # Change Security to Depends
623
+ ):
624
  # session_id = request.gg_session_id
625
  user_id = request.user_id
626
  logger.info(f"Scheduling GG session reminder for {request.date}", extra={"user_id": user_id, "endpoint": "/schedule_gg_reminder"})
 
636
  return {"response": response}
637
 
638
  @app.post("/process_gg_session")
639
+ @catch_endpoint_error
640
+ async def process_gg_session(
641
+ request: GGItem,
642
+ api_key: str = Depends(get_api_key) # Change Security to Depends
643
+ ):
644
+ logger.info("Processing growth guide session", extra={"user_id": request.user_id, "endpoint": "/process_gg_session"})
 
 
 
 
 
645
 
646
+ user = get_user(request.user_id)
647
+ session_data = get_growth_guide_session(request.user_id, request.gg_session_id)
648
+ response = user.process_growth_guide_session(session_data, request.gg_session_id)
649
+ add_to_cache(user)
650
+ pop_cache(user.user_id)
651
  return {"response": response}
652
 
653
+
654
  @app.get("/user_daily_messages")
655
+ @catch_endpoint_error
656
+ async def get_daily_message(
657
+ user_id: str,
658
+ api_key: str = Depends(get_api_key) # Change Security to Depends
659
+ ):
660
  logger.info("Getting daily messages", extra={"user_id": user_id, "endpoint": "/user_daily_messages"})
661
  user = get_user(user_id)
662
  daily_messages = user.get_daily_messages()
663
  return {"response": daily_messages}
664
 
665
  @app.post("/batch_refresh_users")
666
+ @catch_endpoint_error
667
+ async def refresh_multiple_users(
668
+ user_ids: List[str],
669
+ api_key: str = Depends(get_api_key) # Change Security to Depends
670
+ ):
671
  logger.info("Refreshing multiple users", extra={"endpoint": "/batch_refresh_users"})
672
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
673
  failed_users = []
674
 
675
  for i,user_id in enumerate(user_ids):
676
+ old_user = get_user(user_id)
677
+ user = old_user.refresh(client)
678
+ add_to_cache(user)
679
+ pop_cache(user.user_id)
680
+ logger.info(f"Successfully refreshed user {i+1}/{len(user_ids)}", extra={"user_id": user_id, "endpoint": "/batch_refresh_users"})
 
 
 
 
681
 
682
  if failed_users:
683
  return {"status": "partial", "failed_users": failed_users}
684
  return {"status": "success", "failed_users": []}
685
 
686
  @app.post("/refresh_user")
687
+ @catch_endpoint_error
688
+ async def refresh_user(
689
+ request: CreateUserItem,
690
+ api_key: str = Depends(get_api_key) # Change Security to Depends
691
+ ):
692
  print_log("INFO","Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
693
  logger.info("Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
694
 
 
702
  return {"response": "ok"}
703
 
704
  @app.post("/create_user")
705
+ @catch_endpoint_error
706
+ async def create_user(
707
+ request: CreateUserItem,
708
+ api_key: str = Depends(get_api_key) # Change Security to Depends
709
+ ):
710
+ logger.info("Creating new user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
711
 
712
+ client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
713
+ if not client:
714
+ raise OpenAIRequestError("client_init", "Failed to initialize OpenAI client")
 
 
 
 
715
 
716
+ if os.path.exists(f'users/data/{request.user_id}.pkl'):
717
+ return {"message": f"[OK] User already exists: {request.user_id}"}
718
+
719
+ user_info, persona = get_user_info(request.user_id)
720
+ # Persona is one of ["Coach Steve", "Coach Aris", "Coach Teresa"]
721
+ # we map each of the above to assistant_ids = ["asst_mUm6MBcW544p1iVov9mwIC96", "asst_4WcktKgYdDnXA1QUlWvrNfWV", "asst_4UVkFK6r2pbz6NK6kNzG4sTW"]
722
+ persona_to_assistant = {
723
+ "Coach Steve": "asst_mUm6MBcW544p1iVov9mwIC96",
724
+ "Coach Aris": "asst_4WcktKgYdDnXA1QUlWvrNfWV",
725
+ "Coach Teresa": "asst_4UVkFK6r2pbz6NK6kNzG4sTW"
726
+ }
727
 
728
+ if persona not in persona_to_assistant:
729
+ logger.warning(f"Invalid persona: {persona}, defaulting to: Coach Steve", extra={"user_id": request.user_id, "endpoint": "/create_user"})
730
+ # For testing we default to steve
731
+ persona = "Coach Steve"
732
+ # raise FastAPIError(
733
+ # message="Invalid persona",
734
+ # e="Persona must be one of ['Coach Steve', 'Coach Aris', 'Coach Teresa']"
735
+ # )
 
 
 
 
 
 
736
 
737
+ selected_persona = persona_to_assistant[persona]
738
 
739
+ logger.info(f"Creating user with persona {persona}:{selected_persona}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
 
740
 
741
+ user = User(request.user_id, user_info, client, selected_persona)
742
+ folder_path = os.path.join("mementos", "to_upload", request.user_id)
743
+ os.makedirs(folder_path, exist_ok=True)
 
744
 
745
+ add_to_cache(user)
746
+ pop_cache(request.user_id)
747
 
748
+ logger.info(f"Successfully created user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
749
+ return {"message": {"info": f"[OK] User created: {user}", "messages": user.get_messages()}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
 
751
  @app.post("/chat")
752
+ @catch_endpoint_error
753
+ async def chat(
754
+ request: ChatItem,
755
+ api_key: str = Depends(get_api_key) # Change Security to Depends
756
+ ):
757
  logger.info("Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
758
+ user = get_user(request.user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
 
760
+ response = user.send_message(request.message)
761
+ logger.info(f"Assistant response generated", extra={"user_id": request.user_id, "endpoint": "/chat"})
762
+ return {"response": response}
763
+
764
  @app.get("/reminders")
765
+ @catch_endpoint_error
766
+ async def get_reminders(
767
+ user_id: str,
768
+ date:str,
769
+ api_key: str = Depends(get_api_key) # Change Security to Depends
770
+ ):
771
  print_log("INFO","Getting reminders", extra={"user_id": user_id, "endpoint": "/reminders"})
772
  logger.info("Getting reminders", extra={"user_id": user_id, "endpoint": "/reminders"})
773
+
774
+ user = get_user(user_id)
775
+ reminders = user.get_reminders(date)
776
+ if len(reminders) == 0:
777
+ print_log("INFO",f"No reminders for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
778
+ logger.info(f"No reminders for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
779
+ reminders = None
780
+
781
+ print_log("INFO",f"Successfully retrieved reminders: {reminders}", extra={"user_id": user_id, "endpoint": "/reminders"})
782
+ logger.info(f"Successfully retrieved reminders: {reminders} for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
783
+ return {"reminders": reminders}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
784
 
785
  @app.post("/change_date")
786
+ @catch_endpoint_error
787
+ async def change_date(
788
+ request: ChangeDateItem,
789
+ api_key: str = Depends(get_api_key) # Change Security to Depends
790
+ ):
791
+ logger.info(f"Processing date change request", extra={"user_id": request.user_id, "endpoint": "/change_date"})
792
+
793
+ user = get_user(request.user_id)
794
+
795
+ # Validate date format
796
  try:
797
+ datetime.strptime(request.date, "%d-%m-%Y %a %H:%M:%S")
798
+ except ValueError:
799
+ # HF format is YYYY-MM-DD
800
+ try:
801
+ request.date = datetime.strptime(request.date, "%Y-%m-%d")
802
+ # convert to '%d-%m-%Y %a 10:00:00'
803
+ request.date = request.date.strftime("%d-%m-%Y %a 10:00:00")
804
+ except ValueError as e:
805
+ raise FastAPIError(
806
+ message="Invalid date format",
807
+ e=str(e)
808
+ )
809
 
810
+ # Upload mementos to DB
811
+ upload_mementos_to_db(request.user_id)
812
 
813
+ # Change date and get response
814
+ response = user.change_date(request.date)
815
+ response['user_id'] = request.user_id
816
+
817
+ # Update cache
818
+ add_to_cache(user)
819
+ pop_cache(user.user_id)
820
+
821
+ return response
822
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
823
  @app.post("/reset_user_messages")
824
+ @catch_endpoint_error
825
+ async def reset_user_messages(
826
+ request: CreateUserItem,
827
+ api_key: str = Depends(get_api_key) # Change Security to Depends
828
+ ):
829
  print_log("INFO","Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
830
  logger.info("Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
831
+
832
+ user = get_user(request.user_id)
833
+ user.reset_conversations()
834
+ print_log("INFO",f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
835
+ logger.info(f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
836
+
837
+ add_to_cache(user)
838
+ update = pop_cache(user.user_id)
839
+
840
+ print_log("INFO",f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
841
+ logger.info(f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
842
+
843
+ return {"response": "ok"}
844
+
 
 
 
 
 
 
 
 
 
 
 
 
 
845
 
846
  @app.get("/get_logs")
847
+ @catch_endpoint_error
848
+ async def get_logs(
849
+ user_id: str = Query(default="", description="User ID to fetch logs for")
850
+ ):
851
  if (user_id):
852
  log_file_path = os.path.join('logs', 'users', f'{user_id}.log')
853
  if not os.path.exists(log_file_path):
 
872
  )
873
 
874
  @app.get("/is_user_responsive")
875
+ @catch_endpoint_error
876
+ async def is_user_responsive(
877
+ user_id: str,
878
+ api_key: str = Depends(get_api_key) # Change Security to Depends
879
+ ):
880
  logger.info("Checking if user is responsive", extra={"user_id": user_id, "endpoint": "/is_user_responsive"})
881
+
882
+ user = get_user(user_id)
883
+ messages = user.get_messages()
884
+ if len(messages) >= 3 and messages[-1]['role'] == 'assistant' and messages[-2]['role'] == 'assistant':
885
+ return {"response": False}
886
+ else:
887
+ return {"response": True}
888
+
 
 
 
 
 
 
 
 
 
 
 
889
 
890
  @app.get("/get_user_summary")
891
+ @catch_endpoint_error
892
+ async def get_summary_by_id(
893
+ user_id: str,
894
+ api_key: str = Depends(get_api_key) # Change Security to Depends
895
+ ):
896
  print_log("INFO", "Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
897
  logger.info("Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
898
+
899
+ # NOTE: This call will also update the users recommended topics to discuss with GG.
900
+ # This is so that the AI response will align with the dashboard.
901
+ user_summary = get_user_summary(user_id, update_rec_topics=True)
902
+ print_log("INFO", "Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
903
+ logger.info("Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
904
+ return user_summary
905
+
 
 
 
 
 
 
 
 
 
 
 
906
 
907
  @app.get("/get_life_status")
908
+ @catch_endpoint_error
909
+ async def get_life_status_by_id(
910
+ user_id: str,
911
+ api_key: str = Depends(get_api_key) # Change Security to Depends
912
+ ):
913
  print_log("INFO", "Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
914
  logger.info("Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
915
+
916
+ life_status = get_user_life_status(user_id)
917
+ print_log("INFO", "Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
918
+ logger.info("Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
919
+ return life_status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
 
921
  @app.post("/add_booking_point")
922
+ @catch_endpoint_error
923
+ async def add_booking_point_by_user(
924
+ user_id: str,
925
+ api_key: str = Depends(get_api_key) # Change Security to Depends
926
+ ):
927
+ user = get_user(user_id)
928
+ user.add_point_for_booking()
929
+ return {"response": "ok"}
930
+
 
 
 
931
 
932
  @app.post("/add_session_completion_point")
933
+ @catch_endpoint_error
934
+ async def add_session_completion_point_by_user(
935
+ user_id: str,
936
+ api_key: str = Depends(get_api_key) # Change Security to Depends
937
+ ):
938
+ user = get_user(user_id)
939
+ user.add_point_for_completing_session()
940
+ return {"response": "ok"}
941
+
 
 
 
942
 
943
  @app.post("/create_pre_gg_report")
944
+ @catch_endpoint_error
945
+ async def create_pre_gg_by_booking(
946
+ request: BookingItem,
947
+ api_key: str = Depends(get_api_key) # Change Security to Depends
948
+ ):
949
+ create_pre_gg_report(request.booking_id)
950
+ return {"response": "ok"}
951
+
 
 
 
952
 
953
  @app.get("/get_user_persona")
954
+ @catch_endpoint_error
955
+ async def get_user_persona(
956
+ user_id: str,
957
+ api_key: str = Depends(get_api_key) # Change Security to Depends
958
+ ):
959
  """Get user's legendary persona from the database"""
960
  logger.info("Getting user's persona", extra={"user_id": user_id, "endpoint": "/get_user_persona"})
961
 
962
+ # Connect to database
963
+ db_params = {
964
+ 'dbname': 'ourcoach',
965
+ 'user': 'ourcoach',
966
+ 'password': 'hvcTL3kN3pOG5KteT17T',
967
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
968
+ 'port': '5432'
969
+ }
970
+ conn = psycopg2.connect(**db_params)
971
+ cur = conn.cursor()
972
+
973
+ # Get onboarding data
974
+ cur.execute("SELECT onboarding FROM users WHERE id = %s", (user_id,))
975
+ result = cur.fetchone()
976
+ if not result:
977
+ raise DBError(
978
+ user_id=user_id,
979
+ code="NoOnboardingError",
980
+ message="User not found in database"
981
+ )
982
+ # Extract persona from onboarding JSON
983
+ onboarding = json.loads(result[0])
984
+ persona = onboarding.get('legendPersona', '')
985
+
986
+ if 'cur' in locals():
987
+ cur.close()
988
+ if 'conn' in locals():
989
+ conn.close()
990
 
991
+ return {"persona": persona}
992
 
993
+
 
 
 
 
 
 
 
 
994
 
995
  @app.get("/get_recent_booking")
996
+ @catch_endpoint_error
997
+ async def get_recent_booking(
998
+ user_id: str,
999
+ api_key: str = Depends(get_api_key) # Change Security to Depends
1000
+ ):
1001
  """Get the most recent booking ID for a user"""
1002
  logger.info("Getting recent booking", extra={"user_id": user_id, "endpoint": "/get_recent_booking"})
1003
 
1004
+ # Connect to database
1005
+ db_params = {
1006
+ 'dbname': 'ourcoach',
1007
+ 'user': 'ourcoach',
1008
+ 'password': 'hvcTL3kN3pOG5KteT17T',
1009
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
1010
+ 'port': '5432'
1011
+ }
1012
+ conn = psycopg2.connect(**db_params)
1013
+ cur = conn.cursor()
1014
+
1015
+ # Get most recent booking where status == 2
1016
+ cur.execute("""
1017
+ SELECT booking_id
1018
+ FROM public.user_notes
1019
+ WHERE user_id = %s
1020
+ ORDER BY created_at DESC
1021
+ LIMIT 1
1022
+ """, (user_id,))
1023
+ result = cur.fetchone()
1024
+
1025
+ if not result:
1026
+ raise DBError(
1027
+ user_id=user_id,
1028
+ code="NoBookingError",
1029
+ message="No bookings found for user"
1030
+ )
 
 
 
 
 
 
 
 
 
 
1031
 
1032
+ booking_id = result[0]
1033
+ logger.info(f"Found recent booking: {booking_id}", extra={"user_id": user_id, "endpoint": "/get_recent_booking"})
1034
+ if 'cur' in locals():
1035
+ cur.close()
1036
+ if 'conn' in locals():
1037
+ conn.close()
1038
+ 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)
@@ -1461,6 +1107,7 @@ Use bullet points or numbered lists where appropriate to enhance readability."""
1461
 
1462
  def __str__(self):
1463
  return f"""User(user_id={self.user_id}
 
1464
  micro_actions={self.micro_actions}
1465
  recommended_actions={self.recommended_micro_actions}
1466
  challenge={self.challenges}
@@ -1472,6 +1119,7 @@ Use bullet points or numbered lists where appropriate to enhance readability."""
1472
 
1473
  def __repr__(self):
1474
  return f"""User(user_id={self.user_id}
 
1475
  micro_actions={self.micro_actions}
1476
  recommended_actions={self.recommended_micro_actions}
1477
  challenge={self.challenges}
@@ -1483,7 +1131,7 @@ Use bullet points or numbered lists where appropriate to enhance readability."""
1483
 
1484
  def refresh(self, client):
1485
  # copy user by creating new user object
1486
- user = User(self.user_id, self.user_info, client, "asst_07u7sucvSXGJOjcjXr5EL6nD", self.user_interaction_guidelines)
1487
  user.conversations = self.conversations.clone(client)
1488
 
1489
  if len(user.get_messages()) >= 1:
@@ -1495,18 +1143,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)
 
1107
 
1108
  def __str__(self):
1109
  return f"""User(user_id={self.user_id}
1110
+ persona_is={self.asst_id}
1111
  micro_actions={self.micro_actions}
1112
  recommended_actions={self.recommended_micro_actions}
1113
  challenge={self.challenges}
 
1119
 
1120
  def __repr__(self):
1121
  return f"""User(user_id={self.user_id}
1122
+ persona_is={self.asst_id}
1123
  micro_actions={self.micro_actions}
1124
  recommended_actions={self.recommended_micro_actions}
1125
  challenge={self.challenges}
 
1131
 
1132
  def refresh(self, client):
1133
  # copy user by creating new user object
1134
+ user = User(self.user_id, self.user_info, client, self.asst_id, self.user_interaction_guidelines)
1135
  user.conversations = self.conversations.clone(client)
1136
 
1137
  if len(user.get_messages()) >= 1:
 
1143
 
1144
  def save_user(self):
1145
  # Construct the file path dynamically for cross-platform compatibility
1146
+ file_path = os.path.join("users", "to_upload", f"{self.user_id}.pkl")
1147
+
1148
+ # Ensure the directory exists
1149
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
1150
+
1151
+ # Save the user object as a pickle file
1152
+ with open(file_path, 'wb') as file:
1153
+ pickle.dump(self, file)
1154
+ return file_path
 
 
 
1155
 
1156
  @staticmethod
1157
  def load_user(user_id, client):
app/utils.py CHANGED
@@ -1,13 +1,13 @@
1
  import logging
2
  import boto3
3
- from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError
4
  import os
5
  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
12
  import logging
13
  import json
@@ -16,17 +16,13 @@ from psycopg2 import sql
16
  import os
17
  from dotenv import load_dotenv
18
  from datetime import datetime, timezone
19
- import threading
20
  import pickle # Replace dill with pickle
21
- from cachetools import TTLCache
22
- import threading
23
- import time
24
  import uuid
25
  from app.cache import CustomTTLCache, upload_file_to_s3
26
  import pdfkit
27
  import PyPDF2
28
- import secrets
29
- import string
30
 
31
  load_dotenv()
32
 
@@ -45,21 +41,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 +76,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 +105,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 +286,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 +336,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 +420,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 +543,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 +603,274 @@ 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 +917,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 +996,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})
@@ -1042,7 +1037,6 @@ def get_user_info(user_id):
1042
  {user_data_clean.get('firstName', '')}'s challenges (You **must** use this information for the PLANNING STATE):
1043
  {challenges}
1044
 
1045
- {user_data_clean.get('firstName', '')}'s Legendary Persona: {user_data_clean.get('legendPersona', '')}
1046
  Pronouns: {user_data_clean.get('pronouns', '')}
1047
  Birthday: {user_data_clean.get('birthDate', '')}
1048
  {user_data_clean.get('firstName', '')}'s MBTI: {user_data_clean.get('mbti', '')}
@@ -1061,14 +1055,16 @@ def get_user_info(user_id):
1061
  {whoImportant}
1062
  """
1063
  logger.info(f"User info retrieved successfully for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
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 +1091,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 +1115,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 +1147,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 +1185,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 +1215,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 +1239,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 +1250,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 +1263,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 +1283,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 +1353,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 +1389,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())
 
1
  import logging
2
  import boto3
3
+ from botocore.exceptions import NoCredentialsError, PartialCredentialsError
4
  import os
5
  from dotenv import load_dotenv
6
+ from fastapi import HTTPException, Security, Query, status
7
  from fastapi.security import APIKeyHeader
8
  from openai import OpenAI
9
+ import openai
10
  import pandas as pd
 
11
  import os
12
  import logging
13
  import json
 
16
  import os
17
  from dotenv import load_dotenv
18
  from datetime import datetime, timezone
 
19
  import pickle # Replace dill with pickle
 
 
 
20
  import uuid
21
  from app.cache import CustomTTLCache, upload_file_to_s3
22
  import pdfkit
23
  import PyPDF2
24
+
25
+ from app.exceptions import BaseOurcoachException, DBError, OpenAIRequestError, UtilsError
26
 
27
  load_dotenv()
28
 
 
41
  # Replace the simple TTLCache with our custom implementation
42
  user_cache = CustomTTLCache(ttl=120, cleanup_interval=30) # 2 minutes TTL
43
 
44
+ def catch_error(func):
45
+ def wrapper(*args, **kwargs):
46
+ try:
47
+ return func(*args, **kwargs)
48
+ except BaseOurcoachException as e:
49
+ raise e
50
+ except openai.BadRequestError as e:
51
+ raise OpenAIRequestError(user_id='no-user', message="Bad Request to OpenAI", code="OpenAIError")
52
+ except Exception as e:
53
+ # Handle other exceptions
54
+ logger.error(f"An unexpected error occurred in Utils: {e}")
55
+ raise UtilsError(user_id='no-user', message="Unexpected error in Utils", e=str(e))
56
+ return wrapper
57
+
58
+ @catch_error
59
  def force_file_move(source, destination):
60
  function_name = force_file_move.__name__
61
  logger.info(f"Attempting to move file from {source} to {destination}", extra={'endpoint': function_name})
62
+ # Ensure the destination directory exists
63
+ os.makedirs(os.path.dirname(destination), exist_ok=True)
64
+
65
+ # Move the file, replacing if it already exists
66
+ os.replace(source, destination)
67
+ logger.info(f"File moved successfully: {source} -> {destination}", extra={'endpoint': function_name})
 
 
 
 
 
68
 
69
+ @catch_error
70
  def get_user(user_id):
71
  function_name = get_user.__name__
72
  logger.info(f"Fetching user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
76
  return user_cache[user_id]
77
  else:
78
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
79
+ if not client:
80
+ raise OpenAIRequestError(user_id=user_id, message="Error creating OpenAI client", code="OpenAIError")
81
  user_file = os.path.join('users', 'data', f'{user_id}.pkl')
82
  # if os.path.exists(user_file):
83
  # with open(user_file, 'rb') as f:
 
105
  user_info = get_user_info(user_id)
106
  if (user_info):
107
  # user has done onboarding but pickle file not created
108
+ raise DBError(user_id=user_id, message="User has done onboarding but pickle file not created", code="NoPickleError")
109
+ raise DBError(user_id=user_id, message="User has not onboarded yet", code="NoOnboardingError")
110
 
111
+ @catch_error
112
  def generate_html(json_data, coach_name='Growth Guide', booking_id = None):
113
  function_name = generate_html.__name__
114
  data = json_data["pre_growth_guide_session_report"]
 
286
  password = "Ourcoach2024!"
287
 
288
  ## SAVING HTML FILE
289
+ # Open the file in write mode
290
+ with open(file_path, 'w', encoding='utf-8') as html_file:
291
+ html_file.write(html_content)
292
+ logger.info(f"File '{booking_id}.html' has been created successfully.", extra={'booking_id': booking_id, 'endpoint': function_name})
293
+
294
+ # Saving as PDF File
295
+ pdfkit.from_file(file_path, path_to_upload, options={'encoding': 'UTF-8'})
296
+ logger.info(f"File '{booking_id}.pdf' has been created successfully.", extra={'booking_id': booking_id, 'endpoint': function_name})
297
+
298
+ ## ENCRYPTING PDF
299
+ logger.info(f"Encrypting '{booking_id}.pdf'...", extra={'booking_id': booking_id, 'endpoint': function_name})
300
+ with open(path_to_upload, 'rb') as file:
301
+ pdf_reader = PyPDF2.PdfReader(file)
302
+ pdf_writer = PyPDF2.PdfWriter()
303
+
304
+ # Add all pages to the writer
305
+ for page_num in range(len(pdf_reader.pages)):
306
+ pdf_writer.add_page(pdf_reader.pages[page_num])
307
+
308
+ # Encrypt the PDF with the given password
309
+ pdf_writer.encrypt(password)
310
+
311
+ with open(path_to_upload, 'wb') as encrypted_file:
312
+ pdf_writer.write(encrypted_file)
313
+
314
+ logger.info(f"Succesfully encrypted '{booking_id}.pdf'", extra={'booking_id': booking_id, 'endpoint': function_name})
 
 
 
 
 
315
 
316
  filename = booking_id
317
 
 
336
 
337
  # force_file_move(os.path.join('users', 'to_upload', filename), os.path.join('users', 'data', filename))
338
  except (FileNotFoundError, NoCredentialsError, PartialCredentialsError) as e:
339
+ raise DBError(user_id="no-user", message="Error uploading file to S3", code="S3Error")
 
340
 
341
+ @catch_error
342
+ def get_user_summary(user_id, update_rec_topics=False):
343
  function_name = get_user_summary.__name__
344
  logger.info(f"Generating user summary for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
345
 
346
  # Step 1: Call get_user to get user's info
347
+ user = get_user(user_id)
348
+ user_info = user.user_info
349
+ user_messages = user.get_messages()
350
+ user_goal = '' if not user.goal else user.goal[-1].content
 
 
 
351
 
352
  # Step 2: Construct the Prompt
353
  chat_history = "\n".join(
 
420
  ### **2. User's Growth Guide Preparation Brief**
421
 
422
  **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.
423
+ You must use the user's current **challenges** and **life goal** to make the preparation brief **personalized**!
424
 
425
+ Important Rules:
426
+ 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!
427
+ 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.
428
+ 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...".
429
+ 4. And for the second time, please be succinct and concise!!!
430
 
431
  **Format**:
432
 
433
+ 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!):
434
 
435
+ - **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.
436
 
437
+ - **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.
438
 
439
+ - **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.
440
 
441
+ - **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.
442
 
443
+ - **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.
444
 
445
  ---
446
 
 
543
  "users_growth_guide_preparation_brief": [
544
  {
545
  "key": "reflect",
546
+ "value": "⁠..."
547
  },
548
  {
549
  "key": "recall_successes",
550
+ "value": "⁠..."
551
  },
552
  {
553
  "key": "identify_challenges",
554
+ "value": "..."
555
  },
556
  {
557
  "key": "set_goals",
558
+ "value": "⁠..."
559
  },
560
  {
561
  "key": "additional_tips",
562
+ "value": "⁠..."
563
  }
564
  ],
565
  "30_minute_coaching_session_script": {
 
603
 
604
  # Step 3: Call the OpenAI API using the specified function
605
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
606
+ response = client.chat.completions.create(
607
+ model="gpt-4o",
608
+ messages=[
609
+ {
610
+ "role": "system",
611
+ "content": [
612
+ {
613
+ "type": "text",
614
+ "text": system_prompt
615
+ }
616
+ ]
617
+ },
618
+ {
619
+ "role": "user",
620
+ "content": [
621
+ {
622
+ "type": "text",
623
+ "text": user_context
624
+ }
625
+ ]
626
+ }
627
+ ],
628
+ response_format={
629
+ "type": "json_schema",
630
+ "json_schema": {
631
+ "name": "growth_guide_session",
632
+ "strict": True,
633
+ "schema": {
634
+ "type": "object",
635
+ "properties": {
636
+ "pre_growth_guide_session_report": {
637
  "type": "object",
638
+ "description": "A comprehensive summary of the user's profile and life context for the Growth Guide.",
639
  "properties": {
640
+ "user_overview": {
641
  "type": "object",
 
642
  "properties": {
643
+ "name": {
644
+ "type": "string",
645
+ "description": "The user's full name."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646
  },
647
+ "age_group": {
648
+ "type": "string",
649
+ "description": "The user's age range (e.g., '30-39')."
650
  },
651
+ "primary_goals": {
652
+ "type": "string",
653
+ "description": "The main goals the user is focusing on."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  },
655
+ "preferred_coaching_style": {
656
+ "type": "string",
657
+ "description": "The coaching style the user prefers."
 
 
 
 
 
658
  }
659
  },
660
+ "required": ["name", "age_group", "primary_goals", "preferred_coaching_style"],
661
  "additionalProperties": False
662
  },
663
+ "personality_insights": {
664
+ "type": "object",
665
+ "properties": {
666
+ "mbti": {
667
+ "type": "string",
668
+ "description": "The user's Myers-Briggs Type Indicator personality type."
669
+ },
670
+ "top_love_languages": {
671
  "type": "array",
 
672
  "items": {
673
+ "type": "string"
 
 
 
 
 
 
 
 
 
674
  },
675
+ "description": "A list of the user's top two love languages."
676
+ },
677
+ "belief_in_astrology": {
678
+ "type": "string",
679
+ "description": "Whether the user believes in horoscope/astrology."
680
  }
681
  },
682
+ "required": ["mbti", "top_love_languages", "belief_in_astrology"],
683
+ "additionalProperties": False
684
+ },
685
+ "progress_snapshot": {
686
  "type": "object",
 
687
  "properties": {
688
+ "mental_well_being": {
689
+ "type": "string",
690
+ "description": "Summary of the user's mental well-being."
691
+ },
692
+ "physical_health_and_wellness": {
693
+ "type": "string",
694
+ "description": "Summary of the user's physical health and wellness."
695
+ },
696
+ "relationships": {
697
+ "type": "string",
698
+ "description": "Summary of the user's relationships."
699
+ },
700
+ "career_growth": {
701
+ "type": "string",
702
+ "description": "Summary of the user's career growth."
703
+ },
704
+ "personal_growth": {
705
+ "type": "string",
706
+ "description": "Summary of the user's personal growth."
707
+ }
708
+ },
709
+ "required": [
710
+ "mental_well_being",
711
+ "physical_health_and_wellness",
712
+ "relationships",
713
+ "career_growth",
714
+ "personal_growth"
715
+ ],
716
+ "additionalProperties": False
717
+ }
718
+ },
719
+ "required": ["user_overview", "personality_insights", "progress_snapshot"],
720
+ "additionalProperties": False
721
+ },
722
+ "users_growth_guide_preparation_brief": {
723
+ "type": "array",
724
+ "description": "A brief guiding the user on what to discuss with the Growth Guide, providing actionable advice and highlighting key areas to focus on.",
725
+ "items": {
726
+ "type": "object",
727
+ "properties": {
728
+ "key": {
729
+ "type": "string",
730
+ "description": "The section heading."
731
+ },
732
+ "value": {
733
+ "type": "string",
734
+ "description": "Content for the section."
735
+ }
736
+ },
737
+ "required": [
738
+ "key",
739
+ "value"
740
+ ],
741
+ "additionalProperties": False
742
+ }
743
+ },
744
+ "30_minute_coaching_session_script": {
745
+ "type": "object",
746
+ "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.",
747
+ "properties": {
748
+ "session_overview": {
749
+ "type": "array",
750
+ "items": {
751
+ "type": "string"
752
+ },
753
+ "description": "Breakdown of the session segments with time frames."
754
+ },
755
+ "detailed_segments": {
756
+ "type": "array",
757
+ "items": {
758
+ "type": "object",
759
+ "properties": {
760
+ "segment_title": {
761
+ "type": "string",
762
+ "description": "Title of the session segment."
763
+ },
764
+ "coach_dialogue": {
765
  "type": "array",
766
  "items": {
767
  "type": "string"
768
  },
769
+ "description": "Suggested coach dialogue during the session"
770
  },
771
+ "guidance": {
772
  "type": "array",
773
  "items": {
774
+ "type": "string"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
775
  },
776
+ "description": "Suggestions for the coach on how to navigate responses."
 
 
 
777
  }
778
+ },
779
+ "required": ["segment_title", "coach_dialogue", "guidance"],
780
+ "additionalProperties": False
781
  },
782
+ "description": "Detailed information for each session segment."
 
 
 
 
783
  }
784
  },
785
  "required": [
786
+ "session_overview",
787
+ "detailed_segments"
 
788
  ],
789
  "additionalProperties": False
790
  }
791
+ },
792
+ "required": [
793
+ "pre_growth_guide_session_report",
794
+ "users_growth_guide_preparation_brief",
795
+ "30_minute_coaching_session_script"
796
+ ],
797
+ "additionalProperties": False
798
  }
799
+ }
800
+ }
801
+ ,
802
+ temperature=0.5,
803
+ max_tokens=3000,
804
+ top_p=1,
805
+ frequency_penalty=0,
806
+ presence_penalty=0
807
+ )
808
 
809
+ # Get response and convert into dictionary
810
+ reports = json.loads(response.choices[0].message.content)
811
+ # html_output = generate_html(reports, coach_name)
812
+ # reports['html_report'] = html_output
813
 
814
+ # Store users_growth_guide_preparation_brief in the User object
815
+ if update_rec_topics:
816
+ user.set_recommened_gg_topics(reports['users_growth_guide_preparation_brief'])
817
 
818
  # Step 4: Return the JSON reports
819
  logger.info(f"User summary generated successfully for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
820
  return reports
821
 
822
+ @catch_error
823
  def create_pre_gg_report(booking_id):
824
  function_name = create_pre_gg_report.__name__
825
 
826
  # Get user_id from booking_id
827
+ logger.info(f"Retrieving booking details for {booking_id}", extra={'booking_id': booking_id, 'endpoint': function_name})
828
+ db_params = {
829
+ 'dbname': 'ourcoach',
830
+ 'user': 'ourcoach',
831
+ 'password': 'hvcTL3kN3pOG5KteT17T',
832
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
833
+ 'port': '5432'
834
+ }
835
  try:
836
+ with psycopg2.connect(**db_params) as conn:
837
+ with conn.cursor() as cursor:
838
+ query = sql.SQL("""
839
+ select user_id
840
+ from {table}
841
+ where id = %s
842
+ """
843
+ ).format(table=sql.Identifier('public', 'booking'))
844
+ cursor.execute(query, (booking_id,))
845
+ row = cursor.fetchone()
846
+ if (row):
847
+ colnames = [desc[0] for desc in cursor.description]
848
+ booking_data = dict(zip(colnames, row))
849
+ ### MODIFY THE FORMAT OF USER DATA
850
+ user_id = booking_data['user_id']
851
+ logger.info(f"User info retrieved successfully for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
852
+ else:
853
+ logger.warning(f"No user info found for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
854
+ except psycopg2.Error as e:
855
+ logger.error(f"Database error while retrieving user info for {user_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
856
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
857
+
858
+ # Run get_user_summary
859
+ user_report = get_user_summary(user_id)
 
 
 
 
 
 
 
 
 
860
 
861
+ # Run generate_html
862
+ generate_html(user_report, booking_id=booking_id)
863
+
864
+ return True
 
 
 
865
 
866
+ @catch_error
867
  def get_user_life_status(user_id):
868
  function_name = get_user_life_status.__name__
869
  logger.info(f"Generating user life status for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
870
 
871
+ user = get_user(user_id)
872
+ user_info = user.user_info
873
+ user_messages = user.get_messages()
 
 
 
 
 
874
 
875
  # Step 2: Construct the Prompt
876
  chat_history = "\n".join(
 
917
 
918
  # Step 3: Call the OpenAI API using the specified function
919
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
920
+ response = client.chat.completions.create(
921
+ model="gpt-4o-mini",
922
+ messages=[
923
+ {
924
+ "role": "system",
925
+ "content": [
926
+ {
927
+ "type": "text",
928
+ "text": system_prompt
929
+ }
930
+ ]
931
+ },
932
+ {
933
+ "role": "user",
934
+ "content": [
935
+ {
936
+ "type": "text",
937
+ "text": user_context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938
  }
939
+ ]
940
+ }
941
+ ],
942
+ response_format={
943
+ "type": "json_schema",
944
+ "json_schema": {
945
+ "name": "life_status_report",
946
+ "strict": True,
947
+ "schema": {
948
+ "type": "object",
949
+ "properties": {
950
+ "mantra_of_the_week": {
951
+ "type": "string",
952
+ "description": "A very short encouragement quote that encapsulates the user's journey to achieve their goals."
953
+ }
954
+ },
955
+ "required": [
956
+ "mantra_of_the_week"
957
+ ],
958
+ "additionalProperties": False
959
  }
960
  }
961
+ }
962
+ ,
963
+ temperature=0.5,
964
+ max_tokens=3000,
965
+ top_p=1,
966
+ frequency_penalty=0,
967
+ presence_penalty=0
968
+ )
 
 
969
 
970
+ # Get response and convert into dictionary
971
+ mantra = json.loads(response.choices[0].message.content)["mantra_of_the_week"]
 
972
 
973
  # Get current life score
974
  life_score = {
 
996
  logger.info(f"User life status generated successfully for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
997
  return reports
998
 
999
+ async def get_api_key(api_key_header: str = Security(api_key_header)) -> str:
1000
+ if api_key_header not in api_keys: # Check against list of valid keys
1001
+ raise HTTPException(
1002
+ status_code=status.HTTP_403_FORBIDDEN,
1003
+ detail="Invalid API key"
1004
+ )
1005
+ return api_key_header
1006
 
1007
+ @catch_error
1008
  def get_user_info(user_id):
1009
  function_name = get_user_info.__name__
1010
  logger.info(f"Retrieving user info for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1037
  {user_data_clean.get('firstName', '')}'s challenges (You **must** use this information for the PLANNING STATE):
1038
  {challenges}
1039
 
 
1040
  Pronouns: {user_data_clean.get('pronouns', '')}
1041
  Birthday: {user_data_clean.get('birthDate', '')}
1042
  {user_data_clean.get('firstName', '')}'s MBTI: {user_data_clean.get('mbti', '')}
 
1055
  {whoImportant}
1056
  """
1057
  logger.info(f"User info retrieved successfully for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
1058
+ return user_data_formatted, user_data_clean.get('legendPersona', '')
1059
  else:
1060
  logger.warning(f"No user info found for {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
1061
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="NoOnboardingError", e=str(e))
1062
+
1063
  except psycopg2.Error as e:
1064
  logger.error(f"Database error while retrieving user info for {user_id}: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1065
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
1066
 
1067
+ @catch_error
1068
  def get_growth_guide_summary(user_id, session_id):
1069
  function_name = get_growth_guide_summary.__name__
1070
  logger.info(f"Retrieving growth guide summary for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1091
  return None
1092
  except psycopg2.Error as e:
1093
  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})
1094
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
1095
+
1096
 
1097
+ @catch_error
1098
  def get_all_bookings():
1099
  function_name = get_all_bookings.__name__
1100
  logger.info(f"Retrieving all bookings", extra={'endpoint': function_name})
 
1115
  logger.info(f"Retrieved {len(bookings)} bookings", extra={'endpoint': function_name})
1116
  return bookings
1117
  except psycopg2.Error as e:
1118
+ bookings = []
1119
  logger.error(f"Database error while retrieving bookings: {e}", extra={'endpoint': function_name})
1120
+ raise DBError(user_id='no-user', message="Error retrieving user info", code="SQLError", e=str(e))
1121
+ finally:
1122
+ return bookings
1123
 
1124
+ @catch_error
1125
  def update_growth_guide_summary(user_id, session_id, ourcoach_summary):
1126
  function_name = update_growth_guide_summary.__name__
1127
  logger.info(f"Updating growth guide summary for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1147
  logger.info(f"Growth guide summary updated successfully for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
1148
  except psycopg2.Error as e:
1149
  logger.error(f"Database error while updating growth guide summary: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1150
+ raise DBError(user_id=user_id, message="Error updating growth guide summary", code="SQLError", e=str(e))
1151
 
1152
+ @catch_error
1153
  def add_growth_guide_session(user_id, session_id, coach_id, session_started_at, zoom_ai_summary, gg_report, ourcoach_summary):
1154
  function_name = add_growth_guide_session.__name__
1155
  logger.info(f"Adding growth guide session for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1185
  logger.info(f"Growth guide session added successfully for user {user_id} and session {session_id}", extra={'user_id': user_id, 'endpoint': function_name})
1186
  except psycopg2.Error as e:
1187
  logger.error(f"Database error while adding growth guide session: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1188
+ raise DBError(user_id=user_id, message="Error adding growth guide session", code="SQLError", e=str(e))
1189
 
1190
+ @catch_error
1191
  def get_growth_guide_session(user_id, session_id):
1192
  # returns the zoom_ai_summary and the gg_report columns from the POST_GG table
1193
  function_name = get_growth_guide_session.__name__
 
1215
  return None
1216
  except psycopg2.Error as e:
1217
  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})
1218
+ raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
1219
 
1220
 
1221
+ @catch_error
1222
  def download_file_from_s3(filename, bucket):
1223
  user_id = filename.split('.')[0]
1224
  function_name = download_file_from_s3.__name__
 
1239
  logger.error(f"Error downloading file {filename} from S3: {e}", extra={'user_id': user_id, 'endpoint': function_name})
1240
  if (os.path.exists(file_path)):
1241
  os.remove(file_path)
1242
+ raise DBError(user_id=user_id, message="Error downloading file from S3", code="S3Error", e=str(e))
1243
 
1244
+ @catch_error
1245
  def add_to_cache(user):
1246
  user_id = user.user_id
1247
  function_name = add_to_cache.__name__
 
1250
  logger.info(f"User {user_id} added to the cache", extra={'user_id': user_id, 'endpoint': function_name})
1251
  return True
1252
 
1253
+ @catch_error
1254
  def pop_cache(user_id):
1255
  if user_id == 'all':
1256
  user_cache.reset_cache()
 
1263
  # upload file
1264
  logger.info(f"Attempting upload file {user_id}.json to S3", extra={'user_id': user_id, 'endpoint': 'pop_cache'})
1265
  upload_file_to_s3(f"{user_id}.pkl")
 
 
 
 
 
 
1266
 
1267
+ user_cache.pop(user_id, None)
1268
+ logger.info(f"User {user_id} has been removed from the cache", extra={'user_id': user_id, 'endpoint': 'pop_cache'})
1269
+ return True
1270
+
1271
+
1272
+ @catch_error
1273
  def update_user(user):
1274
  user_id = user.user_id
1275
  function_name = update_user.__name__
 
1283
 
1284
  return True
1285
 
1286
+ @catch_error
1287
  def upload_mementos_to_db(user_id):
1288
  function_name = upload_mementos_to_db.__name__
1289
  logger.info(f"Uploading mementos to DB for user {user_id}", extra={'user_id': user_id, 'endpoint': function_name})
 
1353
  return True
1354
  except psycopg2.Error as e:
1355
  logger.error(f"Database error while uploading mementos: {str(e)}", extra={'user_id': user_id, 'endpoint': function_name})
1356
+ raise DBError(user_id=user_id, message="Error uploading mementos", code="SQLError", e=str(e))
 
 
 
1357
 
1358
+ @catch_error
1359
  def get_users_mementos(user_id, date):
1360
  function_name = get_users_mementos.__name__
1361
  db_params = {
 
1389
  logger.info(f"No mementos found for user {user_id} on date {date}", extra={'endpoint': function_name, 'user_id': user_id})
1390
  return []
1391
  except psycopg2.Error as e:
1392
+ mementos = []
1393
  logger.error(f"Database error while retrieving mementos: {e}", extra={'endpoint': function_name, 'user_id': user_id})
1394
+ raise DBError(user_id=user_id, message="Error retrieving mementos", code="SQLError", e=str(e))
1395
+ finally:
1396
+ return mementos
1397
+
1398
+ @catch_error
1399
+ def get_user_subscriptions(user_id):
1400
+ function_name = get_user_subscriptions.__name__
1401
+ logger.info(f"Retrieving subscriptions for user {user_id}", extra={'endpoint': function_name})
1402
+ db_params = {
1403
+ 'dbname': 'ourcoach',
1404
+ 'user': 'ourcoach',
1405
+ 'password': 'hvcTL3kN3pOG5KteT17T',
1406
+ 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
1407
+ 'port': '5432'
1408
+ }
1409
+ try:
1410
+ with psycopg2.connect(**db_params) as conn:
1411
+ with conn.cursor() as cursor:
1412
+ query = sql.SQL("""
1413
+ SELECT * FROM {table}
1414
+ WHERE user_id = %s
1415
+ ORDER BY period_started DESC
1416
+ """).format(table=sql.Identifier('public', 'user_subscription'))
1417
+ cursor.execute(query, (user_id,))
1418
+ rows = cursor.fetchall()
1419
+ colnames = [desc[0] for desc in cursor.description]
1420
+ df = pd.DataFrame(rows, columns=colnames)
1421
+ logger.info(f"Retrieved {len(df)} subscriptions for user {user_id}", extra={'endpoint': function_name})
1422
+ return df
1423
+ except psycopg2.Error as e:
1424
+ logger.error(f"Database error while retrieving user subscriptions: {e}", extra={'endpoint': function_name})
1425
+ raise DBError(user_id=user_id, message="Error retrieving user subscriptions", code="SQLError", e=str(e))
1426
 
1427
  def generate_uuid():
1428
  return str(uuid.uuid4())