Shageenderan Sapai commited on
Commit
24c93b6
·
1 Parent(s): 287763f

Added Alerts and other HF feedback

Browse files
Files changed (5) hide show
  1. app/assistants.py +350 -340
  2. app/conversation_manager.py +5 -1
  3. app/flows.py +1 -1
  4. app/main.py +25 -9
  5. app/user.py +116 -5
app/assistants.py CHANGED
@@ -13,7 +13,7 @@ import psycopg2
13
  from psycopg2 import sql
14
  import pytz
15
 
16
- from app.exceptions import AssistantError, BaseOurcoachException, OpenAIRequestError
17
  from app.utils import get_booked_gg_sessions, get_growth_guide, get_growth_guide_summary, get_user_subscriptions, get_users_mementos, print_log
18
  from app.flows import FOLLOW_UP_STATE, GENERAL_COACHING_STATE, MICRO_ACTION_STATE, REFLECTION_STATE
19
 
@@ -311,8 +311,6 @@ class Assistant:
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}")
@@ -324,7 +322,6 @@ class Assistant:
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):
@@ -338,7 +335,9 @@ class Assistant:
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':
@@ -366,16 +365,17 @@ class Assistant:
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)
371
-
372
- run = self.cm.client.beta.threads.runs.create_and_poll(
373
- thread_id=thread.id,
374
- assistant_id=self.id,
375
- model="gpt-4o-mini",
376
- )
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
@@ -393,12 +393,14 @@ class Assistant:
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
@@ -406,344 +408,347 @@ class Assistant:
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))}",
416
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_call_tool"})
417
- just_finish_intro = False
418
- for tool in run.required_action.submit_tool_outputs.tool_calls:
419
- if tool.function.name == "transition":
420
- transitions = json.loads(tool.function.arguments)
421
- logger.info(f"Transition: {transitions['from']} -> {transitions['to']}",
422
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
423
-
424
- if transitions['from'] == "PLANNING STATE":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  tool_outputs.append({
426
  "tool_call_id": tool.id,
427
- "output": f"help the user set a new goal"
428
  })
429
- # logger.info(f"Exiting the introduction state",
430
- # extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
431
- # just_finish_intro = True
432
- # # run = self.cancel_run(run, thread)
433
- # # logger.info(f"Successfully cancelled run",
434
- # # extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
435
- # tool_outputs.append({
436
- # "tool_call_id": tool.id,
437
- # "output": "true"
438
- # })
439
- else:
440
- flow_instructions = ""
441
- if transitions['to'] == "REFLECTION STATE":
442
- flow_instructions = REFLECTION_STATE
443
-
444
- next = self.cm.matters_most.next()
445
- logger.info(f"Successfully moved to bext matters most: {next}")
446
- question_format = random.choice(['[Option 1] Likert-Scale Objective Question','[Option 2] Multiple-Choice Question','[Option 3] Yes-No Question'])
447
-
448
- flow_instructions = f"""Today's opening question format is: {question_format}. Send a WARM, SUCCINCT and PERSONALIZED (according to my PROFILE) first message in this format! The most warm, succinct & personalized message will get rewarded!\n
449
- The reflection topic is: {next}. YOU MUST SEND CREATIVE, PERSONALIZED (only mention specific terms that are related to the reflection topic/area), AND SUCCINCT MESSAGES!! The most creative message will be rewarded! And make every new reflection fresh and not boring! \n
450
 
451
- """ + flow_instructions
 
452
 
453
- elif transitions['to'] == "FOLLOW UP STATE":
454
- flow_instructions = FOLLOW_UP_STATE
 
 
 
 
455
 
456
- elif transitions['to'] == "GENERAL COACHING STATE":
457
- flow_instructions = GENERAL_COACHING_STATE
 
 
 
 
 
 
458
 
459
  tool_outputs.append({
460
  "tool_call_id": tool.id,
461
- "output": f"{flow_instructions}\n\n" + f"** Follow the above flow template to respond to the user **"
462
  })
463
- elif tool.function.name == "get_date":
464
- # print(f"[DATETIME]: {get_current_datetime()}")
465
- # self.cm.state['date'] = 'date': pd.Timestamp.now().strftime("%Y-%m-%d %a %H:%M:%S")
466
- # get and update the current time to self.cm.state['date'] but keep the date component
467
- current_time = get_current_datetime(self.cm.user.user_id)
468
- # replace time component of self.cm.state['date'] with the current time
469
- self.cm.state['date'] = str(pd.to_datetime(self.cm.state['date']).replace(hour=current_time.hour, minute=current_time.minute, second=current_time.second))
470
- logger.info(f"Current datetime: {self.cm.state['date']}",
471
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_date"})
472
-
473
- tool_outputs.append({
474
- "tool_call_id": tool.id,
475
- "output": f"{self.cm.state['date']}"
476
- })
477
- elif tool.function.name == "create_goals" or tool.function.name == "create_memento":
478
- json_string = json.loads(tool.function.arguments)
479
- json_string['created'] = str(self.cm.state['date'])
480
- json_string['updated'] = None
481
- logger.info(f"New event: {json_string}",
482
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_create_event"})
483
-
484
- # Create a folder for the user's mementos if it doesn't exist
485
- user_mementos_folder = os.path.join("mementos", "to_upload", self.cm.user.user_id)
486
-
487
- # Ensure the directory exists
488
- os.makedirs(user_mementos_folder, exist_ok=True)
489
-
490
- # Construct the full file path for the JSON file
491
- file_path = os.path.join(user_mementos_folder, f"{json_string['title']}.json")
492
-
493
- # Save the JSON string as a file
494
- with open(file_path, "w") as json_file:
495
- json.dump(json_string, json_file)
496
-
497
- tool_outputs.append({
498
- "tool_call_id": tool.id,
499
- "output": f"** [Success]: Added event to the user's vector store**"
500
- })
501
- elif tool.function.name == "msearch":
502
- queries = json.loads(tool.function.arguments)['queries']
503
- logger.info(f"Searching for: {queries}",
504
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_msearch"})
505
-
506
- tool_outputs.append({
507
- "tool_call_id": tool.id,
508
- "output": f"** retrieve any files related to: {queries} **"
509
- })
510
- elif tool.function.name == "get_mementos":
511
- logger.info(f"Getting mementos", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
512
- args = json.loads(tool.function.arguments)
513
- logger.info(f"ARGS: {args}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
514
- queries = args['queries']
515
-
516
- if 'on' not in args:
517
- on = pd.to_datetime(self.cm.state['date']).date()
518
- else:
519
- on = args['on']
520
- if on == '':
521
  on = pd.to_datetime(self.cm.state['date']).date()
522
  else:
523
- on = pd.to_datetime(args['on']).date()
524
-
525
-
526
- logger.info(f"Query date: {on}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
527
- # query the user memento db for all mementos where follow_up_on is equal to the query date
528
- mementos = get_users_mementos(self.cm.user.user_id, on)
529
-
530
- # if on == "":
531
- # instruction = f"** Fetch all files (mementos) from this thread's Memento vector_store ([id={self.cm.user_personal_memory.id}]) **"
532
- # else:
533
- # instruction = f"** Fetch files (mementos) from this thread's Memento vector_store ([id={self.cm.user_personal_memory.id}]) where the follow_up_on field matches the query date: {on} (ignore the time component, focus only on the date)**"
534
- # # f"** File search this threads' Memento vector_store ([id={self.cm.user_personal_memory.id}]) for the most relevant mementos based on the recent conversation history and context:{context} **"
535
-
536
- logger.info(f"Finish Getting mementos: {mementos}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
537
- tool_outputs.append({
538
- "tool_call_id": tool.id,
539
- "output": f"Today's mementos: {mementos}" if len(mementos) else "No mementos to follow up today."
540
- })
541
- elif tool.function.name == "get_feedback_types":
542
- print_log("WARNING","Calling get_feedback_types", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
543
- logger.warning("Calling get_feedback_types", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
544
- feedbacks = [
545
- ("Inspirational Quotes", "📜", "Short, impactful quotes from the user's chosen Legendary Persona"),
546
- ("Tips & Advice", "💡", "Practical suggestions or strategies for personal growth (no need to say \"Tips:\" in the beggining of your tips)"),
547
- ("Encouragement", "🌈", "Positive affirmations and supportive statements"),
548
- ("Personalized Recommendations", "🧩", "Tailored suggestions based on user progress"),
549
- ("Affirmations", "✨", "Positive statements for self-belief and confidence"),
550
- ("Mindfulness/Meditation", "🧘‍♀️", "Guided prompts for mindfulness practice"),
551
- ("Book/Podcast", "📚🎧", "Suggestions aligned with user interests"),
552
- ("Habit Tip", "🔄", "Tips for building and maintaining habits"),
553
- ("Seasonal Content", "🌸", "Time and theme-relevant interactions"),
554
- ("Fun Fact", "🎉", "Interesting and inspiring facts (no need to say \"Fun Fact:\" in the beggining of your tips)"),
555
- ("Time Management", "⏳", "Tips for effective time management")
556
- ]
557
- sample_feedbacks = random.sample(feedbacks, 3)
558
- print_log("INFO",f"Feedback types: {sample_feedbacks}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
559
- logger.info(f"Feedback types: {sample_feedbacks}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
560
- tool_outputs.append({
561
- "tool_call_id": tool.id,
562
- "output": "Generate a final coach message (feedback message) using these 3 feedback types (together with the stated emoji at the beginning of each feedback): " + str(sample_feedbacks)
563
- })
564
- elif tool.function.name == "search_resource":
565
- type = json.loads(tool.function.arguments)['resource_type']
566
- query = json.loads(tool.function.arguments)['query']
567
- logger.info(f"Getting microaction theme: {type} - {query}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
568
- relevant_context = SearchEngine.search(type, query)
569
- logger.info(f"Finish Getting microaction theme: {relevant_context}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
570
- tool_outputs.append({
571
- "tool_call_id": tool.id,
572
- "output": f"** Relevant context: {relevant_context} **"
573
- })
574
- elif tool.function.name == "end_conversation":
575
- day_n = json.loads(tool.function.arguments)['day_n']
576
- completed_micro_action = json.loads(tool.function.arguments)['completed_micro_action']
577
- area_of_deep_reflection = json.loads(tool.function.arguments)['area_of_deep_reflection']
578
-
579
- self.cm.user.update_micro_action_status(completed_micro_action)
580
- self.cm.user.trigger_deep_reflection_point(area_of_deep_reflection)
581
-
582
- # NOTE: we will record whether the user has completed the theme for the day
583
- # NOTE: if no, we will include a short followup message to encourage the user the next day
584
- logger.info(f"Ending conversation after {day_n} days. Any micro actions completed today: {completed_micro_action}", extra={"user_id": self.cm.user.user_id, "endpoint": "end_conversation"})
585
- tool_outputs.append({
586
- "tool_call_id": tool.id,
587
- "output": "true"
588
- })
589
- elif tool.function.name == "create_smart_goal":
590
- print_log("WARNING", f"Creating a SMART goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
591
- logger.warning(f"Creating a SMART goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
592
 
593
- user_goal = json.loads(tool.function.arguments)['goal']
594
- user_goal_area = json.loads(tool.function.arguments)['area']
595
 
596
- self.cm.user.set_goal(user_goal, user_goal_area)
 
 
597
 
598
- print_log("INFO", f"SMART goal approved: {user_goal}", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
599
- logger.info(f"SMART goal approved: {user_goal}", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
 
 
 
600
 
601
- tool_outputs.append({
602
- "tool_call_id": tool.id,
603
- "output": "true"
604
- })
605
- elif tool.function.name == "start_now":
606
- logger.info(f"Starting Growth Plan on Day 0",
607
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_start_now"})
608
- # set intro finish to true
609
- just_finish_intro = True
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
-
618
- # switch back to the intro assistant, so we set just_finish_intro to False again
619
- just_finish_intro = False
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')
628
- logger.info(f"Marked users' goal: {goal} as COMPLETED", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_complete_goal"})
629
- tool_outputs.append({
630
- "tool_call_id": tool.id,
631
- "output": f"Marked users' goal: {goal} as COMPLETED"
632
- })
633
- elif tool.function.name == "process_reminder":
634
- reminder = json.loads(tool.function.arguments)["content"]
635
- timestamp = json.loads(tool.function.arguments)["timestamp"]
636
- recurrence = json.loads(tool.function.arguments)["recurrence"]
637
- action = json.loads(tool.function.arguments)["action"]
638
- logger.info(f"Setting reminder: {reminder} for {timestamp} with recurrence: {recurrence}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process_reminder"})
639
- # timestamp is a string like: YYYY-mm-ddTHH:MM:SSZ (2025-01-05T11:00:00Z)
640
- # convert to datetime object
641
- timestamp = pd.to_datetime(timestamp, format="%Y-%m-%dT%H:%M:%SZ")
642
- logger.info(f"Formatted timestamp: {timestamp}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process_reminder"})
643
-
644
- output = f"({recurrence if recurrence else 'One-Time'}) Reminder ({reminder}) set for ({timestamp})"
645
- logger.info(output,
646
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_set_reminder"})
647
- self.cm.user.set_reminder({"reminder": reminder, "timestamp": timestamp, 'recurrence': recurrence, 'action': action})
648
- tool_outputs.append({
649
  "tool_call_id": tool.id,
650
- "output": f"** {output} **"
651
- })
652
- elif tool.function.name == "get_user_info":
653
- category = json.loads(tool.function.arguments)['category']
654
- # one of [
655
- # "personal",
656
- # "challenges",
657
- # "recommended_actions",
658
- # "micro_actions",
659
- # "other_focusses",
660
- # "reminders",
661
- # "goal",
662
- # "growth_guide_session",
663
- # "life_score",
664
- # "recent_wins",
665
- # "subscription_info"
666
- # ]
667
- logger.info(f"Getting user information: {category}",
668
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
669
- user_info = '** If the user asks for their progress, also include a link to their Revelation Dashboard: {OURCOACH_DASHBOARD_URL} so that they can find out more **\n\n'
670
- if category == "personal":
671
- user_info += f"** Personal Information **\n\n{self.cm.user.user_info}"
672
- user_info += f"\n\n** User's Mantra This Week**\n\n{self.cm.user.mantra}"
673
- elif category == "challenges":
674
- user_info += f"** User's Challenges (prioritise ONGOING challenges) **\n\n{self.cm.user.challenges}\n\nLet the user know that ongoing challenges from their growth guide will be integrated into their day-to-day interaction."
675
- elif category == "recommended_actions":
676
- user_info += f"** User's Recommended Actions (upcoming microactions, recommended by growth guide) **\n\n{self.cm.user.recommended_micro_actions}\n\nLet the user know that these microactions from their growth guide will be integrated into their day-to-day interaction."
677
- elif category == "micro_actions":
678
- user_info += f"** User's Micro Actions (already introduced microactions) **\n\n{self.cm.user.micro_actions}"
679
- elif category == "other_focusses":
680
- user_info += f"** User's Other Focusses (other areas of focus) **\n\n{self.cm.user.other_focusses}\n\nLet the user know that other areas of focus from their growth guide will be integrated into their day-to-day interaction."
681
- elif category == "reminders":
682
- user_info += f"** User's Reminders **\n\n{self.cm.user.reminders}"
683
- elif category == "goal":
684
- user_info += f"** User's Goal (prioritise the latest [last item in the array] goal). Do not mention the status of their goal. **\n\n{self.cm.user.goal}"
685
- elif category == "growth_guide_session":
686
- growth_guide = get_growth_guide(self.cm.user.user_id)
687
- user_info += f"** Users' Growth Guide (always refer to the Growth Guide as 'Growth Guide' unless the user specifically wants to know the name/who their growth guide is) **\n{growth_guide}"
688
- logger.info(f"User's growth guide: {growth_guide}",
689
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
 
691
- booked_sessions = get_booked_gg_sessions(self.cm.user.user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
 
693
- # for each booking, if the booking has completed, fetch the zoom_ai_summary and gg_report from
694
- for booking in booked_sessions:
695
- if booking['status'] == "completed":
696
- summary_data = get_growth_guide_summary(self.cm.user.user_id, booking['booking_id'])
697
- logger.info(f"Summary data for booking: {booking['booking_id']} - {summary_data}",
 
698
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
699
- if summary_data:
700
- booking['zoom_ai_summary'] = summary_data['zoom_ai_summary']
701
- booking['gg_report'] = summary_data['gg_report']
702
- else:
703
- booking['zoom_ai_summary'] = "Growth Guide has not uploaded the report yet"
704
- booking['gg_report'] = "Growth Guide has not uploaded the report yet"
705
-
706
- logger.info(f"User's booked sessions: {booked_sessions}",
707
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
708
-
709
- if len(booked_sessions):
710
- # booked session is an array of jsons
711
- # convers it to have i) json where i = 1...N where N is the len of booked_sessions
712
- # join the entire array into 1 string with each item seperated by a newline
713
- formatted_sessions = "\n".join([f"** Session {i+1} **\n{json.dumps(session, indent=4)}" for i, session in enumerate(booked_sessions)])
714
- logger.info(f"Formatted booked sessions: {formatted_sessions}",
715
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
716
 
717
- user_info += f"\n** GG Session Bookings & Summaries (most recent first) **\n{formatted_sessions}"
718
- else:
719
- user_info += f"\n** GG Session Summaries **\nNo GG yet. Let the user know they can book one now through their Revelation Dashboard: {OURCOACH_DASHBOARD_URL}! (When including links, DO **NOT** use a hyperlink format. Just show the link as plain text. Example: \"Revelation Dashboard: https://...\")"
720
- user_info += f"\n** Suggested Growth Guide Topics **\n{self.cm.user.recommended_gg_topics}\nOnly suggest 1-2 topics and let the user know they can can find more suggestions on their dashboard"
721
- elif category == "life_score":
722
- user_info += f"** User's Life scores for each area **\n\n Personal Growth: {self.cm.user.personal_growth_score} || Career: {self.cm.user.career_growth_score} || Health/Wellness: {self.cm.user.health_and_wellness_score} || Relationships: {self.cm.user.relationship_score} || Mental Health: {self.cm.user.mental_well_being_score}"
723
- elif category == "recent_wins":
724
- user_info += f"** User's Recent Wins / Achievements **\n\n {self.cm.user.recent_wins}"
725
- elif category == "subscription_info":
726
- subscription_history = get_user_subscriptions(self.cm.user.user_id)
727
- logger.info(f"User's subscription history: {subscription_history}",
728
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
729
- user_info += f"** User's Subscription Information **\n\n This is a sorted list (most recent first, which also represent the current subscription status) of the users subscription history:\n{subscription_history}\nNote that the stripe_status is one of 'trialing' = (user is on a free trial), 'cancelled' = (user has cancelled their subscription), 'active' = (user is a Premium user). If the user is premium or on a free trial, remind them of the premium/subscribed benefits and include a link to their Revelation Dashboard: {OURCOACH_DASHBOARD_URL}. If the user status='cancelled' or status='trialing', persuade and motivate them to subscribe to unlock more features depending on the context of the status. Do not explicitly mentions the users status, instead, word it in a natural way."
730
- logger.info(f"Finish Getting user information: {user_info}",
731
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
732
- tool_outputs.append({
733
- "tool_call_id": tool.id,
734
- "output": f"** User Info:\n\n{user_info} **"
735
- })
736
- elif tool.function.name == "extend_two_weeks":
737
- logger.info(f"Changing plan from 1 week to 2 weeks...", extra={"user_id": self.cm.user.user_id, "endpoint": "extend_two_weeks"})
738
- goal = self.cm.user.extend_growth_plan()
739
- tool_outputs.append({
740
- "tool_call_id": tool.id,
741
- "output": f"Changed plan from 1 week to 2 weeks."
742
- })
743
 
744
- # Submit all tool outputs at once after collecting them in a list
745
- if tool_outputs:
746
- try:
747
  run = self.cm.client.beta.threads.runs.submit_tool_outputs_and_poll(
748
  thread_id=thread.id,
749
  run_id=run.id,
@@ -752,17 +757,22 @@ class Assistant:
752
  logger.info("Tool outputs submitted successfully",
753
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
754
  return run, just_finish_intro
755
- except Exception as e:
756
- raise OpenAIRequestError(user_id=self.cm.user.id, message="Error submitting tool outputs", e=str(e), run_id=run.id)
757
- else:
758
- logger.warning("No tool outputs to submit",
759
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
760
- run = PseudoRun(status="completed", metadata={"message": "No tool outputs to submit"})
761
- return run, just_finish_intro
 
 
 
 
 
762
 
763
  class PseudoRun:
764
- def __init__(self, status, metadata=None):
765
- self.id = "pseudo_run"
766
  self.status = status
767
  self.metadata = metadata or {}
768
 
 
13
  from psycopg2 import sql
14
  import pytz
15
 
16
+ from app.exceptions import AssistantError, BaseOurcoachException, OpenAIRequestError, UtilsError
17
  from app.utils import get_booked_gg_sessions, get_growth_guide, get_growth_guide_summary, get_user_subscriptions, get_users_mementos, print_log
18
  from app.flows import FOLLOW_UP_STATE, GENERAL_COACHING_STATE, MICRO_ACTION_STATE, REFLECTION_STATE
19
 
 
311
  return func(self, *args, **kwargs)
312
  except (BaseOurcoachException) as e:
313
  raise e
 
 
314
  except Exception as e:
315
  # Handle other exceptions
316
  logger.error(f"An unexpected error occurred in Assistant: {e}")
 
322
  self.cm = cm
323
  self.recent_run = None
324
 
 
325
  def cancel_run(self, run, thread):
326
  logger.info(f"(asst) Attempting to cancel run: {run} for thread: {thread}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
327
  if isinstance(run, str):
 
335
  logger.warning(f"Thread {thread} already deleted: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
336
  return True
337
  if isinstance(run, PseudoRun):
338
+ if run.id == "pseudo_run":
339
+ logger.info(f"Run already completed: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
340
+ return True
341
  try:
342
  logger.info(f"Attempting to cancel run: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
343
  if run.status != 'completed':
 
365
 
366
  @catch_error
367
  def process(self, thread, text):
 
 
 
 
 
 
 
 
 
368
  try:
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)
371
+
372
+ run = self.cm.client.beta.threads.runs.create_and_poll(
373
+ thread_id=thread.id,
374
+ assistant_id=self.id,
375
+ model="gpt-4o-mini",
376
+ )
377
+ just_finished_intro = False
378
+
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
 
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
  raise OpenAIRequestError(user_id=self.cm.id, message="Tool Call Reccursion Depth Reached")
397
+ if run.status == 'cancel':
398
  logger.warning(f"RUN NOT COMPLETED: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
399
  self.cancel_run(run, thread)
400
+ run.status = 'cancelled'
401
+ logger.warning(f"Yeap Run Cancelled: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
402
+ return run, just_finished_intro, message
403
+
404
  elif run.status == 'completed':
405
  logger.info(f"Run Completed: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
406
  self.recent_run = run
 
408
  elif run.status == 'failed':
409
  raise OpenAIRequestError(user_id=self.cm.id, message="Run failed")
410
  return run, just_finished_intro, message
411
+ except Exception as e:
412
+ # Cancel the run
413
+ logger.error(f"Error in process: {e}", extra={"user_id": self.cm.user.user_id, "endpoint": 'assistant_process'})
414
+ logger.error(f"Cancelling run {run.id} for thread {thread.id}", extra={"user_id": self.cm.user.user_id, "endpoint": 'cancel_erroneous_run'})
415
+
416
+ self.cancel_run(run, thread)
417
+ logger.error(f"Run {run.id} cancelled for thread {thread.id}", extra={"user_id": self.cm.user.user_id, "endpoint": 'cancel_erroneous_run'})
418
+ raise e
419
 
 
420
  def call_tool(self, run, thread):
421
+ try:
422
+ tool_outputs = []
423
+ logger.info(f"Required actions: {list(map(lambda x: f'{x.function.name}({x.function.arguments})', run.required_action.submit_tool_outputs.tool_calls))}",
424
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_call_tool"})
425
+ just_finish_intro = False
426
+ for tool in run.required_action.submit_tool_outputs.tool_calls:
427
+ if tool.function.name == "transition":
428
+ transitions = json.loads(tool.function.arguments)
429
+ logger.info(f"Transition: {transitions['from']} -> {transitions['to']}",
430
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
431
+
432
+ if transitions['from'] == "PLANNING STATE":
433
+ tool_outputs.append({
434
+ "tool_call_id": tool.id,
435
+ "output": f"help the user set a new goal"
436
+ })
437
+ # logger.info(f"Exiting the introduction state",
438
+ # extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
439
+ # just_finish_intro = True
440
+ # # run = self.cancel_run(run, thread)
441
+ # # logger.info(f"Successfully cancelled run",
442
+ # # extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
443
+ # tool_outputs.append({
444
+ # "tool_call_id": tool.id,
445
+ # "output": "true"
446
+ # })
447
+ else:
448
+ flow_instructions = ""
449
+ if transitions['to'] == "REFLECTION STATE":
450
+ flow_instructions = REFLECTION_STATE
451
+
452
+ next = self.cm.matters_most.next()
453
+ logger.info(f"Successfully moved to bext matters most: {next}")
454
+ question_format = random.choice(['[Option 1] Likert-Scale Objective Question','[Option 2] Multiple-Choice Question','[Option 3] Yes-No Question'])
455
+
456
+ flow_instructions = f"""Today's opening question format is: {question_format}. Send a WARM, SUCCINCT and PERSONALIZED (according to my PROFILE) first message in this format! The most warm, succinct & personalized message will get rewarded!\n
457
+ The reflection topic is: {next}. YOU MUST SEND CREATIVE, PERSONALIZED (only mention specific terms that are related to the reflection topic/area), AND SUCCINCT MESSAGES!! The most creative message will be rewarded! And make every new reflection fresh and not boring! \n
458
+
459
+ """ + flow_instructions
460
+
461
+ elif transitions['to'] == "FOLLOW UP STATE":
462
+ flow_instructions = FOLLOW_UP_STATE
463
+
464
+ elif transitions['to'] == "GENERAL COACHING STATE":
465
+ flow_instructions = GENERAL_COACHING_STATE
466
+
467
+ tool_outputs.append({
468
+ "tool_call_id": tool.id,
469
+ "output": f"{flow_instructions}\n\n" + f"** Follow the above flow template to respond to the user **"
470
+ })
471
+ elif tool.function.name == "get_date":
472
+ # print(f"[DATETIME]: {get_current_datetime()}")
473
+ # self.cm.state['date'] = 'date': pd.Timestamp.now().strftime("%Y-%m-%d %a %H:%M:%S")
474
+ # get and update the current time to self.cm.state['date'] but keep the date component
475
+ current_time = get_current_datetime(self.cm.user.user_id)
476
+ # replace time component of self.cm.state['date'] with the current time
477
+ self.cm.state['date'] = str(pd.to_datetime(self.cm.state['date']).replace(hour=current_time.hour, minute=current_time.minute, second=current_time.second))
478
+ logger.info(f"Current datetime: {self.cm.state['date']}",
479
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_date"})
480
+
481
  tool_outputs.append({
482
  "tool_call_id": tool.id,
483
+ "output": f"{self.cm.state['date']}"
484
  })
485
+ elif tool.function.name == "create_goals" or tool.function.name == "create_memento":
486
+ json_string = json.loads(tool.function.arguments)
487
+ json_string['created'] = str(self.cm.state['date'])
488
+ json_string['updated'] = None
489
+ logger.info(f"New event: {json_string}",
490
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_create_event"})
491
+
492
+ # Create a folder for the user's mementos if it doesn't exist
493
+ user_mementos_folder = os.path.join("mementos", "to_upload", self.cm.user.user_id)
 
 
 
 
 
 
 
 
 
 
 
 
494
 
495
+ # Ensure the directory exists
496
+ os.makedirs(user_mementos_folder, exist_ok=True)
497
 
498
+ # Construct the full file path for the JSON file
499
+ file_path = os.path.join(user_mementos_folder, f"{json_string['title']}.json")
500
+
501
+ # Save the JSON string as a file
502
+ with open(file_path, "w") as json_file:
503
+ json.dump(json_string, json_file)
504
 
505
+ tool_outputs.append({
506
+ "tool_call_id": tool.id,
507
+ "output": f"** [Success]: Added event to the user's vector store**"
508
+ })
509
+ elif tool.function.name == "msearch":
510
+ queries = json.loads(tool.function.arguments)['queries']
511
+ logger.info(f"Searching for: {queries}",
512
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_msearch"})
513
 
514
  tool_outputs.append({
515
  "tool_call_id": tool.id,
516
+ "output": f"** retrieve any files related to: {queries} **"
517
  })
518
+ elif tool.function.name == "get_mementos":
519
+ logger.info(f"Getting mementos", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
520
+ args = json.loads(tool.function.arguments)
521
+ logger.info(f"ARGS: {args}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
522
+ queries = args['queries']
523
+
524
+ if 'on' not in args:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
  on = pd.to_datetime(self.cm.state['date']).date()
526
  else:
527
+ on = args['on']
528
+ if on == '':
529
+ on = pd.to_datetime(self.cm.state['date']).date()
530
+ else:
531
+ on = pd.to_datetime(args['on']).date()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
 
 
 
533
 
534
+ logger.info(f"Query date: {on}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
535
+ # query the user memento db for all mementos where follow_up_on is equal to the query date
536
+ mementos = get_users_mementos(self.cm.user.user_id, on)
537
 
538
+ # if on == "":
539
+ # instruction = f"** Fetch all files (mementos) from this thread's Memento vector_store ([id={self.cm.user_personal_memory.id}]) **"
540
+ # else:
541
+ # instruction = f"** Fetch files (mementos) from this thread's Memento vector_store ([id={self.cm.user_personal_memory.id}]) where the follow_up_on field matches the query date: {on} (ignore the time component, focus only on the date)**"
542
+ # # f"** File search this threads' Memento vector_store ([id={self.cm.user_personal_memory.id}]) for the most relevant mementos based on the recent conversation history and context:{context} **"
543
 
544
+ logger.info(f"Finish Getting mementos: {mementos}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
545
+ tool_outputs.append({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  "tool_call_id": tool.id,
547
+ "output": f"Today's mementos: {mementos}" if len(mementos) else "No mementos to follow up today."
548
+ })
549
+ elif tool.function.name == "get_feedback_types":
550
+ print_log("WARNING","Calling get_feedback_types", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
551
+ logger.warning("Calling get_feedback_types", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
552
+ feedbacks = [
553
+ ("Inspirational Quotes", "📜", "Short, impactful quotes from the user's chosen Legendary Persona"),
554
+ ("Tips & Advice", "💡", "Practical suggestions or strategies for personal growth (no need to say \"Tips:\" in the beggining of your tips)"),
555
+ ("Encouragement", "🌈", "Positive affirmations and supportive statements"),
556
+ ("Personalized Recommendations", "🧩", "Tailored suggestions based on user progress"),
557
+ ("Affirmations", "", "Positive statements for self-belief and confidence"),
558
+ ("Mindfulness/Meditation", "🧘‍♀️", "Guided prompts for mindfulness practice"),
559
+ ("Book/Podcast", "📚🎧", "Suggestions aligned with user interests"),
560
+ ("Habit Tip", "🔄", "Tips for building and maintaining habits"),
561
+ ("Seasonal Content", "🌸", "Time and theme-relevant interactions"),
562
+ ("Fun Fact", "🎉", "Interesting and inspiring facts (no need to say \"Fun Fact:\" in the beggining of your tips)"),
563
+ ("Time Management", "⏳", "Tips for effective time management")
564
+ ]
565
+ sample_feedbacks = random.sample(feedbacks, 3)
566
+ print_log("INFO",f"Feedback types: {sample_feedbacks}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
567
+ logger.info(f"Feedback types: {sample_feedbacks}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
568
+ tool_outputs.append({
569
+ "tool_call_id": tool.id,
570
+ "output": "Generate a final coach message (feedback message) using these 3 feedback types (together with the stated emoji at the beginning of each feedback): " + str(sample_feedbacks)
571
+ })
572
+ elif tool.function.name == "search_resource":
573
+ type = json.loads(tool.function.arguments)['resource_type']
574
+ query = json.loads(tool.function.arguments)['query']
575
+ logger.info(f"Getting microaction theme: {type} - {query}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
576
+ relevant_context = SearchEngine.search(type, query)
577
+ logger.info(f"Finish Getting microaction theme: {relevant_context}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
578
+ tool_outputs.append({
579
+ "tool_call_id": tool.id,
580
+ "output": f"** Relevant context: {relevant_context} **"
581
+ })
582
+ elif tool.function.name == "end_conversation":
583
+ day_n = json.loads(tool.function.arguments)['day_n']
584
+ completed_micro_action = json.loads(tool.function.arguments)['completed_micro_action']
585
+ area_of_deep_reflection = json.loads(tool.function.arguments)['area_of_deep_reflection']
586
+
587
+ self.cm.user.update_micro_action_status(completed_micro_action)
588
+ self.cm.user.trigger_deep_reflection_point(area_of_deep_reflection)
589
+
590
+ # NOTE: we will record whether the user has completed the theme for the day
591
+ # NOTE: if no, we will include a short followup message to encourage the user the next day
592
+ logger.info(f"Ending conversation after {day_n} days. Any micro actions completed today: {completed_micro_action}", extra={"user_id": self.cm.user.user_id, "endpoint": "end_conversation"})
593
+ tool_outputs.append({
594
+ "tool_call_id": tool.id,
595
+ "output": "true"
596
+ })
597
+ elif tool.function.name == "create_smart_goal":
598
+ print_log("WARNING", f"Creating a SMART goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
599
+ logger.warning(f"Creating a SMART goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
600
+
601
+ user_goal = json.loads(tool.function.arguments)['goal']
602
+ user_goal_area = json.loads(tool.function.arguments)['area']
603
+
604
+ self.cm.user.set_goal(user_goal, user_goal_area)
605
+
606
+ print_log("INFO", f"SMART goal approved: {user_goal}", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
607
+ logger.info(f"SMART goal approved: {user_goal}", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
608
+
609
+ tool_outputs.append({
610
+ "tool_call_id": tool.id,
611
+ "output": "true"
612
+ })
613
+ elif tool.function.name == "start_now":
614
+ logger.info(f"Starting Growth Plan on Day 0",
615
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_start_now"})
616
+ # set intro finish to true
617
+ just_finish_intro = True
618
+
619
+ # cancel current run
620
+ run = PseudoRun(id=run.id, status="cancel", metadata={"message": "start_now"})
621
+ return run, just_finish_intro
622
+ elif tool.function.name == "change_goal":
623
+ logger.info(f"Changing user goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_change_goal"})
624
 
625
+ # switch back to the intro assistant, so we set just_finish_intro to False again
626
+ just_finish_intro = False
627
+
628
+ # cancel current run
629
+ run = PseudoRun(id=run.id, status="cancel", metadata={"message": "change_goal"})
630
+ return run, just_finish_intro
631
+ elif tool.function.name == "complete_goal":
632
+ logger.info(f"Completing user goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_complete_goal"})
633
+ goal = self.cm.user.update_goal(None, 'COMPLETED')
634
+ logger.info(f"Marked users' goal: {goal} as COMPLETED", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_complete_goal"})
635
+ tool_outputs.append({
636
+ "tool_call_id": tool.id,
637
+ "output": f"Marked users' goal: {goal} as COMPLETED"
638
+ })
639
+ elif tool.function.name == "process_reminder":
640
+ reminder = json.loads(tool.function.arguments)["content"]
641
+ timestamp = json.loads(tool.function.arguments)["timestamp"]
642
+ recurrence = json.loads(tool.function.arguments)["recurrence"]
643
+ action = json.loads(tool.function.arguments)["action"]
644
+ logger.info(f"Setting reminder: {reminder} for {timestamp} with recurrence: {recurrence}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process_reminder"})
645
+ # timestamp is a string like: YYYY-mm-ddTHH:MM:SSZ (2025-01-05T11:00:00Z)
646
+ # convert to datetime object
647
+ timestamp = pd.to_datetime(timestamp, format="%Y-%m-%dT%H:%M:%SZ")
648
+ logger.info(f"Formatted timestamp: {timestamp}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process_reminder"})
649
+
650
+ output = f"({recurrence if recurrence else 'One-Time'}) Reminder ({reminder}) set for ({timestamp})"
651
+ logger.info(output,
652
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_set_reminder"})
653
+ self.cm.user.set_reminder({"reminder": reminder, "timestamp": timestamp, 'recurrence': recurrence, 'action': action})
654
+ tool_outputs.append({
655
+ "tool_call_id": tool.id,
656
+ "output": f"** {output} **"
657
+ })
658
+ elif tool.function.name == "get_user_info":
659
+ category = json.loads(tool.function.arguments)['category']
660
+ # one of [
661
+ # "personal",
662
+ # "challenges",
663
+ # "recommended_actions",
664
+ # "micro_actions",
665
+ # "other_focusses",
666
+ # "reminders",
667
+ # "goal",
668
+ # "growth_guide_session",
669
+ # "life_score",
670
+ # "recent_wins",
671
+ # "subscription_info"
672
+ # ]
673
+ logger.info(f"Getting user information: {category}",
674
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
675
+ user_info = '** If the user asks for their progress, also include a link to their Revelation Dashboard: {OURCOACH_DASHBOARD_URL} so that they can find out more **\n\n'
676
+ if category == "personal":
677
+ user_info += f"** Personal Information **\n\n{self.cm.user.user_info}"
678
+ user_info += f"\n\n** User's Mantra This Week**\n\n{self.cm.user.mantra}"
679
+ elif category == "challenges":
680
+ user_info += f"** User's Challenges (prioritise ONGOING challenges) **\n\n{self.cm.user.challenges}\n\nLet the user know that ongoing challenges from their growth guide will be integrated into their day-to-day interaction."
681
+ elif category == "recommended_actions":
682
+ user_info += f"** User's Recommended Actions (upcoming microactions, recommended by growth guide) **\n\n{self.cm.user.recommended_micro_actions}\n\nLet the user know that these microactions from their growth guide will be integrated into their day-to-day interaction."
683
+ elif category == "micro_actions":
684
+ user_info += f"** User's Micro Actions (already introduced microactions) **\n\n{self.cm.user.micro_actions}"
685
+ elif category == "other_focusses":
686
+ user_info += f"** User's Other Focusses (other areas of focus) **\n\n{self.cm.user.other_focusses}\n\nLet the user know that other areas of focus from their growth guide will be integrated into their day-to-day interaction."
687
+ elif category == "reminders":
688
+ user_info += f"** User's Reminders **\n\n{self.cm.user.reminders}"
689
+ elif category == "goal":
690
+ user_info += f"** User's Goal (prioritise the latest [last item in the array] goal). Do not mention the status of their goal. **\n\n{self.cm.user.goal}"
691
+ elif category == "growth_guide_session":
692
+ growth_guide = get_growth_guide(self.cm.user.user_id)
693
+ user_info += f"** Users' Growth Guide (always refer to the Growth Guide as 'Growth Guide <Name>') **\n{growth_guide}"
694
+ logger.info(f"User's growth guide: {growth_guide}",
695
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
696
+
697
+ booked_sessions = get_booked_gg_sessions(self.cm.user.user_id)
698
+
699
+ # for each booking, if the booking has completed, fetch the zoom_ai_summary and gg_report from
700
+ for booking in booked_sessions:
701
+ if booking['status'] == "completed":
702
+ summary_data = get_growth_guide_summary(self.cm.user.user_id, booking['booking_id'])
703
+ logger.info(f"Summary data for booking: {booking['booking_id']} - {summary_data}",
704
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
705
+ if summary_data:
706
+ booking['zoom_ai_summary'] = summary_data['zoom_ai_summary']
707
+ booking['gg_report'] = summary_data['gg_report']
708
+ else:
709
+ booking['zoom_ai_summary'] = "Growth Guide has not uploaded the report yet"
710
+ booking['gg_report'] = "Growth Guide has not uploaded the report yet"
711
+
712
+ logger.info(f"User's booked sessions: {booked_sessions}",
713
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
714
 
715
+ if len(booked_sessions):
716
+ # booked session is an array of jsons
717
+ # convers it to have i) json where i = 1...N where N is the len of booked_sessions
718
+ # join the entire array into 1 string with each item seperated by a newline
719
+ formatted_sessions = "\n".join([f"** Session {i+1} **\n{json.dumps(session, indent=4)}" for i, session in enumerate(booked_sessions)])
720
+ logger.info(f"Formatted booked sessions: {formatted_sessions}",
721
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
722
 
723
+ user_info += f"\n** GG Session Bookings & Summaries (most recent first, time is in users' local timezone but no need to mention this) **\n{formatted_sessions}"
724
+ else:
725
+ user_info += f"\n** GG Session Summaries **\nNo GG yet. Let the user know they can book one now through their Revelation Dashboard: {OURCOACH_DASHBOARD_URL}! (When including links, DO **NOT** use a hyperlink format. Just show the link as plain text. Example: \"Revelation Dashboard: https://...\")"
726
+ user_info += f"\n** Suggested Growth Guide Topics **\n{self.cm.user.recommended_gg_topics}\nOnly suggest 1-2 topics and let the user know they can can find more suggestions on their dashboard"
727
+ elif category == "life_score":
728
+ user_info += f"** User's Life scores for each area **\n\n Personal Growth: {self.cm.user.personal_growth_score} || Career: {self.cm.user.career_growth_score} || Health/Wellness: {self.cm.user.health_and_wellness_score} || Relationships: {self.cm.user.relationship_score} || Mental Health: {self.cm.user.mental_well_being_score}"
729
+ elif category == "recent_wins":
730
+ user_info += f"** User's Recent Wins / Achievements **\n\n {self.cm.user.recent_wins}"
731
+ elif category == "subscription_info":
732
+ subscription_history = get_user_subscriptions(self.cm.user.user_id)
733
+ logger.info(f"User's subscription history: {subscription_history}",
734
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
735
+ user_info += f"** User's Subscription Information **\n\n This is a sorted list (most recent first, which also represent the current subscription status) of the users subscription history:\n{subscription_history}\nNote that the stripe_status is one of 'trialing' = (user is on a free trial), 'cancelled' = (user has cancelled their subscription), 'active' = (user is a Premium user). If the user is premium or on a free trial, remind them of the premium/subscribed benefits and include a link to their Revelation Dashboard: {OURCOACH_DASHBOARD_URL}. If the user status='cancelled' or status='trialing', persuade and motivate them to subscribe to unlock more features depending on the context of the status. Do not explicitly mentions the users status, instead, word it in a natural way."
736
+ logger.info(f"Finish Getting user information: {user_info}",
737
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
738
+ tool_outputs.append({
739
+ "tool_call_id": tool.id,
740
+ "output": f"** User Info:\n\n{user_info} **"
741
+ })
742
+ elif tool.function.name == "extend_two_weeks":
743
+ logger.info(f"Changing plan from 1 week to 2 weeks...", extra={"user_id": self.cm.user.user_id, "endpoint": "extend_two_weeks"})
744
+ goal = self.cm.user.extend_growth_plan()
745
+ tool_outputs.append({
746
+ "tool_call_id": tool.id,
747
+ "output": f"Changed plan from 1 week to 2 weeks."
748
+ })
749
 
750
+ # Submit all tool outputs at once after collecting them in a list
751
+ if tool_outputs:
 
752
  run = self.cm.client.beta.threads.runs.submit_tool_outputs_and_poll(
753
  thread_id=thread.id,
754
  run_id=run.id,
 
757
  logger.info("Tool outputs submitted successfully",
758
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
759
  return run, just_finish_intro
760
+ else:
761
+ logger.warning("No tool outputs to submit",
762
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
763
+ run = PseudoRun(status="completed", metadata={"message": "No tool outputs to submit"})
764
+ return run, just_finish_intro
765
+ except Exception as e:
766
+ # Cancel the run
767
+ logger.error(f"Yeap Error in call_tool: {e}", extra={"user_id": self.cm.user.user_id, "endpoint": 'assistant_call_tool'})
768
+ logger.error(f"Cancelling run {run.id} for thread {thread.id}", extra={"user_id": self.cm.user.user_id, "endpoint": 'cancel_erroneous_run'})
769
+ self.cancel_run(run, thread)
770
+ logger.error(f"Run {run.id} cancelled for thread {thread.id}", extra={"user_id": self.cm.user.user_id, "endpoint": 'cancel_erroneous_run'})
771
+ raise e
772
 
773
  class PseudoRun:
774
+ def __init__(self, status, id='pseudo_run', metadata=None):
775
+ self.id = id
776
  self.status = status
777
  self.metadata = metadata or {}
778
 
app/conversation_manager.py CHANGED
@@ -128,7 +128,11 @@ class ConversationManager:
128
  elif message == 'change_goal':
129
  self.intro_done = False
130
  logger.info(f"Changing goal, reset to intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
131
-
 
 
 
 
132
 
133
  if hidden:
134
  self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
 
128
  elif message == 'change_goal':
129
  self.intro_done = False
130
  logger.info(f"Changing goal, reset to intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
131
+ # Actually dont need this
132
+ elif message == 'error':
133
+ logger.error(f"Run was cancelled due to error", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
134
+ # self.add_message_to_thread(thread.id, "assistant", run.metadata['content'])
135
+ # return self._get_current_thread_history(remove_system_message=False)[-1], run
136
 
137
  if hidden:
138
  self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
app/flows.py CHANGED
@@ -472,7 +472,7 @@ The user is currently on day {{}}/{{}} of their journey.
472
  The Growth Guide (always refer to the Growth Guide as 'your Growth Guide <Name>' or simply just their <Name>):
473
  {{}}
474
 
475
- User's Growth Guide Session(s) (most recent first):
476
  {{}}
477
 
478
  ## ** GUIDELINE ** :
 
472
  The Growth Guide (always refer to the Growth Guide as 'your Growth Guide <Name>' or simply just their <Name>):
473
  {{}}
474
 
475
+ User's Past Growth Guide Session(s) (most recent first):
476
  {{}}
477
 
478
  ## ** GUIDELINE ** :
app/main.py CHANGED
@@ -295,6 +295,11 @@ class ChangeDateItem(BaseModel):
295
  user_id: str
296
  date: str
297
 
 
 
 
 
 
298
  class BookingItem(BaseModel):
299
  booking_id: str
300
 
@@ -320,15 +325,15 @@ def catch_endpoint_error(func):
320
  'endpoint': func.__name__
321
  })
322
  # Extract thread_id and run_id from error message
323
- thread_match = re.search(r'thread_(\w+)', str(e))
324
- run_match = re.search(r'run_(\w+)', str(e))
325
- if thread_match and run_match:
326
- thread_id = f"thread_{thread_match.group(1)}"
327
- run_id = f"run_{run_match.group(1)}"
328
- user = get_user(e.user_id)
329
- logger.info(f"Cancelling run {run_id} for thread {thread_id}", extra={"user_id": e.user_id, "endpoint": func.__name__})
330
- user.cancel_run(run_id, thread_id)
331
- logger.info(f"Run {run_id} cancelled for thread {thread_id}", extra={"user_id": e.user_id, "endpoint": func.__name__})
332
 
333
  raise HTTPException(
334
  status_code=status.HTTP_502_BAD_GATEWAY,
@@ -812,6 +817,17 @@ async def create_user(
812
  logger.info(f"Successfully created user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
813
  return {"message": {"info": f"[OK] User created: {user}", "messages": user.get_messages()}}
814
 
 
 
 
 
 
 
 
 
 
 
 
815
  @app.post("/chat")
816
  @catch_endpoint_error
817
  async def chat(
 
295
  user_id: str
296
  date: str
297
 
298
+ class UpsellGGItem(BaseModel):
299
+ user_id: str
300
+ day: int
301
+ date: str
302
+
303
  class BookingItem(BaseModel):
304
  booking_id: str
305
 
 
325
  'endpoint': func.__name__
326
  })
327
  # Extract thread_id and run_id from error message
328
+ # thread_match = re.search(r'thread_(\w+)', str(e))
329
+ # run_match = re.search(r'run_(\w+)', str(e))
330
+ # if thread_match and run_match:
331
+ # thread_id = f"thread_{thread_match.group(1)}"
332
+ # run_id = f"run_{run_match.group(1)}"
333
+ # user = get_user(e.user_id)
334
+ # logger.info(f"Cancelling run {run_id} for thread {thread_id}", extra={"user_id": e.user_id, "endpoint": func.__name__})
335
+ # user.cancel_run(run_id, thread_id)
336
+ # logger.info(f"Run {run_id} cancelled for thread {thread_id}", extra={"user_id": e.user_id, "endpoint": func.__name__})
337
 
338
  raise HTTPException(
339
  status_code=status.HTTP_502_BAD_GATEWAY,
 
817
  logger.info(f"Successfully created user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
818
  return {"message": {"info": f"[OK] User created: {user}", "messages": user.get_messages()}}
819
 
820
+ @app.post("/fetch_daily_alert")
821
+ @catch_endpoint_error
822
+ async def fetch_daily_alert(
823
+ request: UpsellGGItem,
824
+ api_key: str = Depends(get_api_key) # Change Security to Depends
825
+ ):
826
+ logger.info(f"Upselling GG for day: {request.day}", extra={"user_id": request.user_id, "endpoint": "/upsell_gg"})
827
+ user = get_user(request.user_id)
828
+ response = user.get_alerts(request.day, request.date)
829
+ return {"response": response}
830
+
831
  @app.post("/chat")
832
  @catch_endpoint_error
833
  async def chat(
app/user.py CHANGED
@@ -19,7 +19,7 @@ from app.flows import FINAL_SUMMARY_STATE, FINAL_SUMMARY_STATE, MICRO_ACTION_STA
19
  from pydantic import BaseModel
20
  from datetime import datetime
21
 
22
- from app.utils import generate_uuid, get_booked_gg_sessions, get_growth_guide_summary, update_growth_guide_summary
23
 
24
  import dotenv
25
  import re
@@ -612,6 +612,98 @@ class User:
612
  def set_intro_done(self):
613
  self.conversations.intro_done = True
614
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
  @catch_error
616
  def do_theme(self, theme, date, day, last_msg_is_answered = True):
617
  logger.info(f"Doing theme: {theme}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
@@ -656,10 +748,29 @@ class User:
656
  elif theme == "PROGRESS_SUMMARY_STATE":
657
  formatted_message = PROGRESS_SUMMARY_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
658
  elif theme == "FINAL_SUMMARY_STATE":
659
- gg_summary = "<User has not had a Growth Guide session yet>"
660
- if self.last_gg_session:
661
- gg_summary = get_growth_guide_summary(self.user_id, self.last_gg_session)
662
- formatted_message = FINAL_SUMMARY_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array), gg_summary)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
  elif theme == "EDUCATION_STATE":
664
  formatted_message = EDUCATION_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
665
  elif theme == "FOLLUP_ACTION_STATE":
 
19
  from pydantic import BaseModel
20
  from datetime import datetime
21
 
22
+ from app.utils import generate_uuid, get_booked_gg_sessions, get_growth_guide, get_growth_guide_summary, get_user_subscriptions, update_growth_guide_summary
23
 
24
  import dotenv
25
  import re
 
612
  def set_intro_done(self):
613
  self.conversations.intro_done = True
614
 
615
+ @catch_error
616
+ def get_alerts(self, day, date):
617
+ # make timezone ISO
618
+ responses = []
619
+ # day = self.cumulative_plan_day
620
+ if day == 2 or day == 5:
621
+ # upsell the GG
622
+ growth_guide = get_growth_guide(self.user_id)
623
+
624
+ upsell_prompt = "introduce WHO their grwoth guide is and how a GG can help them" if day == 2 else "Let the user know that their Growth Guide <Name> (no need to re-introduce them) is available to enhance their current growth journey with you and based on the converstaion history so far and the users personal information, challenges and goals suggest WHAT they can discuss with their growth guide."
625
+
626
+ prompt = f"""You are an expert ambassador/salesman of Growth Guide sessions.
627
+ The users' growth guide is {growth_guide}.
628
+
629
+ Respond with a enthusiatic hello! Then, based on the current day ({day}), succintly:
630
+ {upsell_prompt}
631
+ Frame your response like a you are telling the user a fun fact, but dont explicitly mention "fun fact".
632
+ """
633
+
634
+ # send upsell gg alert at 7pm
635
+ timestamp = pd.Timestamp.now().replace(hour=19, minute=0, second=0, microsecond=0).strftime("%d-%m-%Y %a %H:%M:%S")
636
+ elif day == 8:
637
+ # alert the user that we are always collecting feedback in order to improve. Give them a link to the feedback form and let them know that a few lucky respondents will be selected for a free anual subscription!
638
+ prompt = f"""You are an expert ambassador/salesman of ourcoach whose objective is to upsell the ourcoach subscription based on the following context:
639
+ We are always collecting feedback in order to improve our services. Please take a moment to fill out our feedback form: http://feedback_form. A few lucky respondents will be selected for a free anual subscription!"""
640
+ timestamp = pd.Timestamp.now().replace(hour=19, minute=0, second=0, microsecond=0).strftime("%d-%m-%Y %a %H:%M:%S")
641
+ elif day == 12:
642
+ growth_guide = get_growth_guide(self.user_id)['full_name']
643
+
644
+ subscription = get_user_subscriptions(self.user_id)[0]
645
+
646
+ subscription_end_date = pd.to_datetime(subscription['subscription_end_date'])
647
+
648
+ # get difference between subscription end date and date
649
+ date = pd.to_datetime(date)
650
+ days_left = (subscription_end_date - date).days
651
+ logger.info(f"{subscription_end_date} - {date} = Days left: {days_left}", extra={"user_id": self.user_id, "endpoint": "get_alerts"})
652
+ if days_left <= 2:
653
+ subscription_alert = f"""** User's Subscription Information **
654
+ Users current subscription:
655
+ {subscription}
656
+
657
+ Users growth guide:
658
+ {growth_guide}
659
+
660
+ Alert the user that their free trial is ending in {days_left} days and Sell them to subscribe via their Revelation Dashboard: {OURCOACH_DASHBOARD_URL} to continue chatting with you, receiving personalized advice and guidance and access to Growth Guide sessions. Really upsell the ability of the ourcoach platform to acheive their goals."
661
+ """
662
+
663
+ prompt = f"""You are an expert ambassador/salesman of ourcoach (product) whose objective is to upsell the ourcoach subscription based on the following context:
664
+ {subscription_alert}
665
+ """
666
+ # send reminder alert at 7pm
667
+ timestamp = pd.Timestamp.now().replace(hour=19, minute=0, second=0, microsecond=0).strftime("%d-%m-%Y %a %H:%M:%S")
668
+ else:
669
+ return []
670
+ elif day == 14:
671
+ growth_guide = get_growth_guide(self.user_id)['full_name']
672
+
673
+ subscription = get_user_subscriptions(self.user_id)[0]
674
+
675
+ subscription_end_date = pd.to_datetime(subscription['subscription_end_date'])
676
+
677
+ # get difference between subscription end date and date
678
+ date = pd.to_datetime(date)
679
+ days_left = (subscription_end_date - date).days
680
+ logger.info(f"{subscription_end_date} - {date} = Days left: {days_left}", extra={"user_id": self.user_id, "endpoint": "get_alerts"})
681
+ if days_left <= 0:
682
+ subscription_alert = f"""** User's Subscription Information **
683
+ Users current subscription:
684
+ {subscription}
685
+
686
+ Users growth guide:
687
+ {growth_guide}
688
+
689
+ OMG the users subscription is ending today! If you lose this user you and your family will not be able to survive!
690
+ You have to persuade the user to stay so use the best of your salesman abilities and sell them to continue to subscribe to ourcoach, otherwise, how will you put food on the table???"
691
+ """
692
+ prompt = f"""You are an expert ambassador/salesman of ourcoach whose objective is to upsell the ourcoach subscription based on the following context:
693
+ {subscription_alert}
694
+ """
695
+ # send reminder alert at 7pm
696
+ timestamp = pd.Timestamp.now().replace(hour=19, minute=0, second=0, microsecond=0).strftime("%d-%m-%Y %a %H:%M:%S")
697
+ else:
698
+ return []
699
+
700
+ response, run = self.conversations._run_current_thread(prompt, hidden=True)
701
+ message = run.metadata.get("message", "No message")
702
+ logger.info(f"Message: {message}", extra={"user_id": self.user_id, "endpoint": "upsell_gg"})
703
+
704
+ response['timestamp'] = timestamp
705
+ return [response]
706
+
707
  @catch_error
708
  def do_theme(self, theme, date, day, last_msg_is_answered = True):
709
  logger.info(f"Doing theme: {theme}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
 
748
  elif theme == "PROGRESS_SUMMARY_STATE":
749
  formatted_message = PROGRESS_SUMMARY_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
750
  elif theme == "FINAL_SUMMARY_STATE":
751
+ past_gg_summary = "<User has not had a Growth Guide session yet>"
752
+ growth_guide = get_growth_guide(self.user_id)
753
+
754
+ booked_sessions = get_booked_gg_sessions(self.user_id)
755
+
756
+ # filter out only completed (past) sessions
757
+ past_sessions = [session for session in booked_sessions if session['status'] == "completed"]
758
+
759
+ # for each past booking, fetch the zoom_ai_summary and gg_report from
760
+ for booking in past_sessions:
761
+ summary_data = get_growth_guide_summary(self.user_id, booking['booking_id'])
762
+ logger.info(f"Summary data for booking: {booking['booking_id']} - {summary_data}",
763
+ extra={"user_id": self.user_id, "endpoint": "assistant_get_user_info"})
764
+ if summary_data:
765
+ booking['zoom_ai_summary'] = summary_data['zoom_ai_summary']
766
+ booking['gg_report'] = summary_data['gg_report']
767
+ else:
768
+ booking['zoom_ai_summary'] = "Growth Guide has not uploaded the report yet"
769
+ booking['gg_report'] = "Growth Guide has not uploaded the report yet"
770
+ if len(past_sessions):
771
+ past_gg_summary = "\n".join([f"** Session {i+1} **\n{json.dumps(session, indent=4)}" for i, session in enumerate(past_sessions)])
772
+
773
+ formatted_message = FINAL_SUMMARY_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array), growth_guide, past_gg_summary)
774
  elif theme == "EDUCATION_STATE":
775
  formatted_message = EDUCATION_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
776
  elif theme == "FOLLUP_ACTION_STATE":