Shageenderan Sapai commited on
Commit
f5fccf5
·
1 Parent(s): d94f728

[FEATURE] Error Handling

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