.github/workflows/deployment.yml CHANGED
@@ -55,7 +55,7 @@ jobs:
55
  env:
56
  IMAGE_TAG: ${{ github.sha }}
57
  run: |
58
- docker build --build-arg FASTAPI_KEY=${{secrets.FASTAPI_KEY}} --build-arg BING_API_KEY=${{secrets.BING_API_KEY}} --build-arg OPENAI_API_KEY=${{secrets.OPENAI_API_KEY}} -t ${{inputs.ecr_url}}:$IMAGE_TAG .
59
  docker push ${{inputs.ecr_url}}:$IMAGE_TAG
60
  echo "image=${{inputs.ecr_url}}:$IMAGE_TAG" >> $GITHUB_OUTPUT
61
 
 
55
  env:
56
  IMAGE_TAG: ${{ github.sha }}
57
  run: |
58
+ docker build --build-arg FASTAPI_KEY=${{secrets.FASTAPI_KEY}} --build-arg OPENAI_API_KEY=${{secrets.OPENAI_API_KEY}} -t ${{inputs.ecr_url}}:$IMAGE_TAG .
59
  docker push ${{inputs.ecr_url}}:$IMAGE_TAG
60
  echo "image=${{inputs.ecr_url}}:$IMAGE_TAG" >> $GITHUB_OUTPUT
61
 
.gitignore CHANGED
@@ -3,6 +3,4 @@ mementos/
3
  users/
4
  templates/
5
  .env
6
- __pycache__
7
- _*flows.py
8
- _*users.py
 
3
  users/
4
  templates/
5
  .env
6
+ __pycache__
 
 
Dockerfile CHANGED
@@ -4,13 +4,6 @@ FROM python:3.10.9
4
  # Set up a new user named "user" with user ID 1000
5
  RUN useradd -m -u 1000 user
6
 
7
- # Install wkhtmltopdf and its dependencies as root
8
- USER root
9
- RUN apt-get update && apt-get install -y --no-install-recommends \
10
- wkhtmltopdf \
11
- && apt-get clean \
12
- && rm -rf /var/lib/apt/lists/*
13
-
14
  # Switch to the "user" user
15
  USER user
16
 
@@ -28,15 +21,11 @@ EXPOSE 7860
28
  ARG FASTAPI_KEY
29
  ARG OPENAI_API_KEY
30
  ARG OPENAI_GENERAL_ASSISTANT
31
- ARG SENTRY_DSN
32
- ARG BING_API_KEY
33
 
34
  ENV FASTAPI_KEY=$FASTAPI_KEY
35
  ENV OPENAI_API_KEY=$OPENAI_API_KEY
36
  ENV OPENAI_GENERAL_ASSISTANT=$OPENAI_GENERAL_ASSISTANT
37
  ENV PYTHONUNBUFFERED=1
38
- ENV SENTRY_DSN=$SENTRY_DSN
39
- ENV BING_API_KEY=$BING_API_KEY
40
 
41
  # Set the working directory
42
  # WORKDIR /code
 
4
  # Set up a new user named "user" with user ID 1000
5
  RUN useradd -m -u 1000 user
6
 
 
 
 
 
 
 
 
7
  # Switch to the "user" user
8
  USER user
9
 
 
21
  ARG FASTAPI_KEY
22
  ARG OPENAI_API_KEY
23
  ARG OPENAI_GENERAL_ASSISTANT
 
 
24
 
25
  ENV FASTAPI_KEY=$FASTAPI_KEY
26
  ENV OPENAI_API_KEY=$OPENAI_API_KEY
27
  ENV OPENAI_GENERAL_ASSISTANT=$OPENAI_GENERAL_ASSISTANT
28
  ENV PYTHONUNBUFFERED=1
 
 
29
 
30
  # Set the working directory
31
  # WORKDIR /code
app/__pycache__/assistants.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/assistants.cpython-312.pyc and b/app/__pycache__/assistants.cpython-312.pyc differ
 
app/__pycache__/flows.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/flows.cpython-312.pyc and b/app/__pycache__/flows.cpython-312.pyc differ
 
app/__pycache__/main.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ
 
app/__pycache__/user.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/user.cpython-312.pyc and b/app/__pycache__/user.cpython-312.pyc differ
 
app/__pycache__/utils.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/utils.cpython-312.pyc and b/app/__pycache__/utils.cpython-312.pyc differ
 
app/assistants.py CHANGED
@@ -1,29 +1,182 @@
1
  import json
2
  import io
3
  import os
4
- from datetime import datetime, timezone
5
  import json
6
  import random
7
  from time import sleep
8
- import openai
9
  import pandas as pd
10
  from dotenv import load_dotenv
11
  import logging
12
- import psycopg2
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
- from app.web_search import SearchEngine
20
 
21
  load_dotenv()
22
 
23
  logger = logging.getLogger(__name__)
24
- OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
25
 
 
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  class FeedbackContent:
29
  def __init__(self, content, role):
@@ -125,472 +278,290 @@ class FeedbackContent:
125
  # return result
126
  return selected
127
 
128
- def get_current_datetime(user_id):
129
- db_params = {
130
- 'dbname': 'ourcoach',
131
- 'user': 'ourcoach',
132
- 'password': 'hvcTL3kN3pOG5KteT17T',
133
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
134
- 'port': '5432'
135
- }
136
- with psycopg2.connect(**db_params) as conn:
137
- with conn.cursor() as cursor:
138
- query = sql.SQL("SELECT * FROM {table} WHERE id = %s").format(table=sql.Identifier('public', 'users'))
139
- cursor.execute(query, (user_id,))
140
- row = cursor.fetchone()
141
- if (row):
142
- colnames = [desc[0] for desc in cursor.description]
143
- user_data = dict(zip(colnames, row))
144
- user_timezone = user_data['timezone']
145
-
146
- return datetime.now().astimezone(pytz.timezone(user_timezone))
147
 
148
  class Assistant:
149
- def catch_error(func):
150
- def wrapper(self, *args, **kwargs):
151
- try:
152
- return func(self, *args, **kwargs)
153
- except (BaseOurcoachException) as e:
154
- raise e
155
- except Exception as e:
156
- # Handle other exceptions
157
- logger.error(f"An unexpected error occurred in Assistant: {e}")
158
- raise AssistantError(user_id=self.cm.user.user_id, message="Unexpected error in Assistant", e=str(e))
159
- return wrapper
160
-
161
  def __init__(self, id, cm):
162
  self.id = id
163
  self.cm = cm
164
  self.recent_run = None
165
 
166
  def cancel_run(self, run, thread):
167
- logger.info(f"(asst) Attempting to cancel run: {run} for thread: {thread}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
168
- if isinstance(run, str):
169
- try:
170
- run = self.cm.client.beta.threads.runs.retrieve(
171
- thread_id=thread,
172
- run_id=run
 
173
  )
174
- thread = self.cm.client.beta.threads.retrieve(thread_id=thread)
175
- except openai.NotFoundError:
176
- logger.warning(f"Thread {thread} already deleted: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
177
- return True
178
- if isinstance(run, PseudoRun):
179
- if run.id == "pseudo_run":
180
- logger.info(f"Run already completed: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
181
- return True
182
- try:
183
- logger.info(f"Attempting to cancel run: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
184
- if run.status != 'completed':
185
- cancel = self.cm.client.beta.threads.runs.cancel(thread_id=thread.id, run_id=run.id)
186
- while cancel.status != 'cancelled':
187
- sleep(0.05)
188
- cancel = self.cm.client.beta.threads.runs.retrieve(
189
- thread_id=thread.id,
190
- run_id=cancel.id
191
- )
192
- logger.info(f"Succesfully cancelled run: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
193
- return True
194
- else:
195
- logger.info(f"Run already completed: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
196
- return False
197
- except openai.BadRequestError:
198
- # check if run has expired. run has a field 'expires_at' like run.expires_at = 1735008568
199
- # if expired, return True and log run already expired
200
- if run.expires_at < get_current_datetime().timestamp():
201
- logger.warning(f"Run already expired: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
202
- return True
203
- else:
204
- logger.warning(f"Error cancelling run: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
205
- return False
206
-
207
- @catch_error
208
  def process(self, thread, text):
 
 
 
 
 
 
 
 
 
209
  try:
210
- # 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:")
211
- message = self.cm.add_message_to_thread(thread.id, "user", text)
212
-
213
- run = self.cm.client.beta.threads.runs.create_and_poll(
214
- thread_id=thread.id,
215
- assistant_id=self.id,
216
- model="gpt-4o-mini",
217
- )
218
- just_finished_intro = False
219
-
220
  if run.status == 'completed':
221
- logger.info(f"Run Completed: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
222
- self.recent_run = run
223
  return run, just_finished_intro, message
224
 
225
- elif run.status == 'failed':
226
- raise OpenAIRequestError(user_id=self.cm.user.user_id, message="Run failed", run_id=run.id)
227
-
228
- elif run.status == 'requires_action':
229
- reccursion = 0
230
- logger.info(f"[Run Pending] Status: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
231
- while run.status == 'requires_action':
232
- logger.info(f"Run Calling tool [{reccursion}]: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
233
- run, just_finished_intro = self.call_tool(run, thread)
234
- reccursion += 1
235
- if reccursion > 10:
236
- 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"})
237
- raise OpenAIRequestError(user_id=self.cm.id, message="Tool Call Reccursion Depth Reached")
238
- if run.status == 'cancel':
239
- logger.warning(f"RUN NOT COMPLETED: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
240
- self.cancel_run(run, thread)
241
- run.status = 'cancelled'
242
- logger.warning(f"Yeap Run Cancelled: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
243
- return run, just_finished_intro, message
244
-
245
- elif run.status == 'completed':
246
- logger.info(f"Run Completed: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
247
- self.recent_run = run
248
- return run, just_finished_intro, message
249
- elif run.status == 'failed':
250
- raise OpenAIRequestError(user_id=self.cm.id, message="Run failed")
251
- return run, just_finished_intro, message
252
- except Exception as e:
253
- # Cancel the run
254
- logger.error(f"Error in process: {e}", extra={"user_id": self.cm.user.user_id, "endpoint": 'assistant_process'})
255
- logger.error(f"Cancelling run {run.id} for thread {thread.id}", extra={"user_id": self.cm.user.user_id, "endpoint": 'cancel_erroneous_run'})
256
-
257
- self.cancel_run(run, thread)
258
- logger.error(f"Run {run.id} cancelled for thread {thread.id}", extra={"user_id": self.cm.user.user_id, "endpoint": 'cancel_erroneous_run'})
259
- raise e
260
-
261
- def call_tool(self, run, thread):
262
- try:
263
- tool_outputs = []
264
- logger.info(f"Required actions: {list(map(lambda x: f'{x.function.name}({x.function.arguments})', run.required_action.submit_tool_outputs.tool_calls))}",
265
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_call_tool"})
266
- just_finish_intro = False
267
- for tool in run.required_action.submit_tool_outputs.tool_calls:
268
- if tool.function.name == "transition":
269
- transitions = json.loads(tool.function.arguments)
270
- logger.info(f"Transition: {transitions['from']} -> {transitions['to']}",
271
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
272
-
273
- if transitions['from'] == "PLANNING STATE":
274
- tool_outputs.append({
275
- "tool_call_id": tool.id,
276
- "output": f"help the user set a new goal"
277
- })
278
- # logger.info(f"Exiting the introduction state",
279
- # extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
280
- # just_finish_intro = True
281
- # # run = self.cancel_run(run, thread)
282
- # # logger.info(f"Successfully cancelled run",
283
- # # extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
284
- # tool_outputs.append({
285
- # "tool_call_id": tool.id,
286
- # "output": "true"
287
- # })
288
- else:
289
- flow_instructions = ""
290
- if transitions['to'] == "REFLECTION STATE":
291
- flow_instructions = REFLECTION_STATE
292
-
293
- next = self.cm.matters_most.next()
294
- logger.info(f"Successfully moved to bext matters most: {next}")
295
- question_format = random.choice(['[Option 1] Likert-Scale Objective Question','[Option 2] Multiple-Choice Question','[Option 3] Yes-No Question'])
296
-
297
- 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
298
- 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
299
-
300
- """ + flow_instructions
301
-
302
- elif transitions['to'] == "FOLLOW UP STATE":
303
- flow_instructions = FOLLOW_UP_STATE
304
-
305
- elif transitions['to'] == "GENERAL COACHING STATE":
306
- flow_instructions = GENERAL_COACHING_STATE
307
-
308
- tool_outputs.append({
309
- "tool_call_id": tool.id,
310
- "output": f"{flow_instructions}\n\n" + f"** Follow the above flow template to respond to the user **"
311
- })
312
- elif tool.function.name == "get_date":
313
- # print(f"[DATETIME]: {get_current_datetime()}")
314
- # self.cm.state['date'] = 'date': pd.Timestamp.now().strftime("%Y-%m-%d %a %H:%M:%S")
315
- # get and update the current time to self.cm.state['date'] but keep the date component
316
- current_time = get_current_datetime(self.cm.user.user_id)
317
- # replace time component of self.cm.state['date'] with the current time
318
- 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))
319
- logger.info(f"Current datetime: {self.cm.state['date']}",
320
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_date"})
321
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  tool_outputs.append({
323
  "tool_call_id": tool.id,
324
- "output": f"{self.cm.state['date']}"
325
  })
326
- elif tool.function.name == "create_goals" or tool.function.name == "create_memento":
327
- json_string = json.loads(tool.function.arguments)
328
- json_string['created'] = str(self.cm.state['date'])
329
- json_string['updated'] = None
330
- logger.info(f"New event: {json_string}",
331
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_create_event"})
332
-
333
- # Create a folder for the user's mementos if it doesn't exist
334
- user_mementos_folder = os.path.join("mementos", "to_upload", self.cm.user.user_id)
335
-
336
- # Ensure the directory exists
337
- os.makedirs(user_mementos_folder, exist_ok=True)
 
 
 
 
 
 
 
 
 
338
 
339
- # Construct the full file path for the JSON file
340
- file_path = os.path.join(user_mementos_folder, f"{json_string['title']}.json")
341
 
342
- # Save the JSON string as a file
343
- with open(file_path, "w") as json_file:
344
- json.dump(json_string, json_file)
345
 
346
- tool_outputs.append({
347
- "tool_call_id": tool.id,
348
- "output": f"** [Success]: Added event to the user's vector store**"
349
- })
350
- elif tool.function.name == "msearch":
351
- queries = json.loads(tool.function.arguments)['queries']
352
- logger.info(f"Searching for: {queries}",
353
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_msearch"})
354
 
355
  tool_outputs.append({
356
  "tool_call_id": tool.id,
357
- "output": f"** retrieve any files related to: {queries} **"
358
  })
359
- elif tool.function.name == "get_mementos":
360
- logger.info(f"Getting mementos", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
361
- args = json.loads(tool.function.arguments)
362
- logger.info(f"ARGS: {args}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
363
- queries = args['queries']
364
-
365
- if 'on' not in args:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  on = pd.to_datetime(self.cm.state['date']).date()
367
  else:
368
- on = args['on']
369
- if on == '':
370
- on = pd.to_datetime(self.cm.state['date']).date()
371
- else:
372
- on = pd.to_datetime(args['on']).date()
373
-
374
-
375
- logger.info(f"Query date: {on}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
376
- # query the user memento db for all mementos where follow_up_on is equal to the query date
377
- mementos = get_users_mementos(self.cm.user.user_id, on)
378
-
379
- # if on == "":
380
- # instruction = f"** Fetch all files (mementos) from this thread's Memento vector_store ([id={self.cm.user_personal_memory.id}]) **"
381
- # else:
382
- # 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)**"
383
- # # 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} **"
384
-
385
- logger.info(f"Finish Getting mementos: {mementos}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
386
- tool_outputs.append({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  "tool_call_id": tool.id,
388
- "output": f"Today's mementos: {mementos}" if len(mementos) else "No mementos to follow up today."
389
- })
390
- elif tool.function.name == "get_feedback_types":
391
- print_log("WARNING","Calling get_feedback_types", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
392
- logger.warning("Calling get_feedback_types", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
393
- feedbacks = [
394
- ("Inspirational Quotes", "📜", "Short, impactful quotes from the user's chosen Legendary Persona"),
395
- ("Tips & Advice", "💡", "Practical suggestions or strategies for personal growth (no need to say \"Tips:\" in the beggining of your tips)"),
396
- ("Encouragement", "🌈", "Positive affirmations and supportive statements"),
397
- ("Personalized Recommendations", "🧩", "Tailored suggestions based on user progress"),
398
- ("Affirmations", "✨", "Positive statements for self-belief and confidence"),
399
- ("Mindfulness/Meditation", "🧘‍♀️", "Guided prompts for mindfulness practice"),
400
- ("Book/Podcast", "📚🎧", "Suggestions aligned with user interests"),
401
- ("Habit Tip", "🔄", "Tips for building and maintaining habits"),
402
- ("Seasonal Content", "🌸", "Time and theme-relevant interactions"),
403
- ("Fun Fact", "🎉", "Interesting and inspiring facts (no need to say \"Fun Fact:\" in the beggining of your tips)"),
404
- ("Time Management", "⏳", "Tips for effective time management")
405
- ]
406
- sample_feedbacks = random.sample(feedbacks, 3)
407
- print_log("INFO",f"Feedback types: {sample_feedbacks}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
408
- logger.info(f"Feedback types: {sample_feedbacks}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
409
- tool_outputs.append({
410
- "tool_call_id": tool.id,
411
- "output": "Generate a final coach message (feedback message) using these 3 feedback types (together with the stated emoji at the beginning of each feedback): " + str(sample_feedbacks)
412
- })
413
- elif tool.function.name == "search_web":
414
- type = json.loads(tool.function.arguments)['resource_type']
415
- query = json.loads(tool.function.arguments)['query']
416
- logger.info(f"Getting microaction theme: {type} - {query}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
417
- relevant_context = SearchEngine.search(type, query, self.cm.user.user_id)
418
- logger.info(f"Finish Getting microaction theme: {relevant_context}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
419
- tool_outputs.append({
420
- "tool_call_id": tool.id,
421
- "output": f"** Relevant context: {relevant_context} **"
422
- })
423
- elif tool.function.name == "end_conversation":
424
- day_n = json.loads(tool.function.arguments)['day_n']
425
- completed_micro_action = json.loads(tool.function.arguments)['completed_micro_action']
426
- area_of_deep_reflection = json.loads(tool.function.arguments)['area_of_deep_reflection']
427
-
428
- self.cm.user.update_micro_action_status(completed_micro_action)
429
- self.cm.user.trigger_deep_reflection_point(area_of_deep_reflection)
430
-
431
- # NOTE: we will record whether the user has completed the theme for the day
432
- # NOTE: if no, we will include a short followup message to encourage the user the next day
433
- 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"})
434
- tool_outputs.append({
435
- "tool_call_id": tool.id,
436
- "output": "true"
437
- })
438
- elif tool.function.name == "create_smart_goal":
439
- print_log("WARNING", f"Creating a SMART goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
440
- logger.warning(f"Creating a SMART goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
441
-
442
- user_goal = json.loads(tool.function.arguments)['goal']
443
- user_goal_area = json.loads(tool.function.arguments)['area']
444
 
445
- self.cm.user.set_goal(user_goal, user_goal_area)
 
 
446
 
447
- print_log("INFO", f"SMART goal approved: {user_goal}", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
448
- logger.info(f"SMART goal approved: {user_goal}", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
449
 
450
- tool_outputs.append({
451
- "tool_call_id": tool.id,
452
- "output": "true"
453
- })
454
- elif tool.function.name == "start_now":
455
- logger.info(f"Starting Growth Plan on Day 0",
456
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_start_now"})
457
- # set intro finish to true
458
- just_finish_intro = True
459
-
460
- # cancel current run
461
- run = PseudoRun(id=run.id, status="cancel", metadata={"message": "start_now"})
462
- return run, just_finish_intro
463
- elif tool.function.name == "change_goal":
464
- logger.info(f"Changing user goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_change_goal"})
465
-
466
- # switch back to the intro assistant, so we set just_finish_intro to False again
467
- just_finish_intro = False
468
- self.cm.user.reset_cumulative_plan_day()
469
-
470
- # cancel current run
471
- run = PseudoRun(id=run.id, status="cancel", metadata={"message": "change_goal"})
472
- return run, just_finish_intro
473
- elif tool.function.name == "complete_goal":
474
- logger.info(f"Completing user goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_complete_goal"})
475
- goal = self.cm.user.update_goal(None, 'COMPLETED')
476
- logger.info(f"Marked users' goal: {goal} as COMPLETED", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_complete_goal"})
477
- tool_outputs.append({
478
- "tool_call_id": tool.id,
479
- "output": f"Marked users' goal: {goal} as COMPLETED"
480
- })
481
- elif tool.function.name == "process_reminder":
482
- reminder = json.loads(tool.function.arguments)["content"]
483
- timestamp = json.loads(tool.function.arguments)["timestamp"]
484
- recurrence = json.loads(tool.function.arguments)["recurrence"]
485
- action = json.loads(tool.function.arguments)["action"]
486
- logger.info(f"Setting reminder: {reminder} for {timestamp} with recurrence: {recurrence}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process_reminder"})
487
- # timestamp is a string like: YYYY-mm-ddTHH:MM:SSZ (2025-01-05T11:00:00Z)
488
- # convert to datetime object
489
- timestamp = pd.to_datetime(timestamp, format="%Y-%m-%dT%H:%M:%SZ")
490
- logger.info(f"Formatted timestamp: {timestamp}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process_reminder"})
491
-
492
- output = f"({recurrence if recurrence else 'One-Time'}) Reminder ({reminder}) set for ({timestamp})"
493
- logger.info(output,
494
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_set_reminder"})
495
- self.cm.user.set_reminder({"reminder": reminder, "timestamp": timestamp, 'recurrence': recurrence, 'action': action})
496
- tool_outputs.append({
497
- "tool_call_id": tool.id,
498
- "output": f"** {output} **"
499
- })
500
- elif tool.function.name == "get_user_info":
501
- category = json.loads(tool.function.arguments)['category']
502
- # one of [
503
- # "personal",
504
- # "challenges",
505
- # "recommended_actions",
506
- # "micro_actions",
507
- # "other_focusses",
508
- # "reminders",
509
- # "goal",
510
- # "growth_guide_session",
511
- # "life_score",
512
- # "recent_wins",
513
- # "subscription_info"
514
- # ]
515
- logger.info(f"Getting user information: {category}",
516
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
517
- user_info = '** If the user asks for their progress, also include a link to their Revelation Dashboard: {OURCOACH_DASHBOARD_URL} so that they can find out more **\n\n'
518
- if category == "personal":
519
- user_info += f"** Personal Information **\n\n{self.cm.user.user_info}"
520
- user_info += f"\n\n** User's Mantra This Week:**\n\n{self.cm.user.mantra or 'Mantra not available.'}"
521
- elif category == "challenges":
522
- user_info += f"** User's Challenges (prioritise ONGOING challenges) **\n\n{self.cm.user.challenges}\n\nLet the user know that ongoing challenges from their growth guide will be integrated into their day-to-day interaction."
523
- elif category == "recommended_actions":
524
- 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."
525
- elif category == "micro_actions":
526
- user_info += f"** User's Micro Actions (already introduced microactions) **\n\n{self.cm.user.micro_actions}"
527
- elif category == "other_focusses":
528
- 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."
529
- elif category == "reminders":
530
- user_info += f"** User's Reminders **\n\n{self.cm.user.reminders}"
531
- elif category == "goal":
532
- 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}"
533
- elif category == "growth_guide_session":
534
- growth_guide = get_growth_guide(self.cm.user.user_id)
535
- user_info += f"** Users' Growth Guide (always refer to the Growth Guide as 'Growth Guide <Name>') **\n{growth_guide}"
536
- logger.info(f"User's growth guide: {growth_guide}",
537
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
538
-
539
- booked_sessions = get_booked_gg_sessions(self.cm.user.user_id)
540
-
541
- # for each booking, if the booking has completed, fetch the zoom_ai_summary and gg_report from
542
- for booking in booked_sessions:
543
- if booking['status'] == "completed":
544
- summary_data = get_growth_guide_summary(self.cm.user.user_id, booking['booking_id'])
545
- logger.info(f"Summary data for booking: {booking['booking_id']} - {summary_data}",
546
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
547
- if summary_data:
548
- booking['zoom_ai_summary'] = summary_data['zoom_ai_summary']
549
- booking['gg_report'] = summary_data['gg_report']
550
- else:
551
- booking['zoom_ai_summary'] = "Growth Guide has not uploaded the report yet"
552
- booking['gg_report'] = "Growth Guide has not uploaded the report yet"
553
-
554
- logger.info(f"User's booked sessions: {booked_sessions}",
555
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
556
-
557
- if len(booked_sessions):
558
- # booked session is an array of jsons
559
- # convers it to have i) json where i = 1...N where N is the len of booked_sessions
560
- # join the entire array into 1 string with each item seperated by a newline
561
- formatted_sessions = "\n".join([f"** Session {i+1} **\n{json.dumps(session, indent=4)}" for i, session in enumerate(booked_sessions)])
562
- logger.info(f"Formatted booked sessions: {formatted_sessions}",
563
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
564
-
565
- 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}"
566
- else:
567
- 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://...\")"
568
- 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"
569
- elif category == "life_score":
570
- 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}"
571
- elif category == "recent_wins":
572
- user_info += f"** User's Recent Wins / Achievements **\n\n {self.cm.user.recent_wins}"
573
- elif category == "subscription_info":
574
- subscription_history = get_user_subscriptions(self.cm.user.user_id)
575
- logger.info(f"User's subscription history: {subscription_history}",
576
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
577
- 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."
578
- logger.info(f"Finish Getting user information: {user_info}",
579
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_user_info"})
580
- tool_outputs.append({
581
- "tool_call_id": tool.id,
582
- "output": f"** User Info:\n\n{user_info} **"
583
- })
584
- elif tool.function.name == "extend_two_weeks":
585
- logger.info(f"Changing plan from 1 week to 2 weeks...", extra={"user_id": self.cm.user.user_id, "endpoint": "extend_two_weeks"})
586
- goal = self.cm.user.extend_growth_plan()
587
- tool_outputs.append({
588
- "tool_call_id": tool.id,
589
- "output": f"Changed plan from 1 week to 2 weeks."
590
- })
591
 
592
- # Submit all tool outputs at once after collecting them in a list
593
- if tool_outputs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  run = self.cm.client.beta.threads.runs.submit_tool_outputs_and_poll(
595
  thread_id=thread.id,
596
  run_id=run.id,
@@ -598,25 +569,17 @@ class Assistant:
598
  )
599
  logger.info("Tool outputs submitted successfully",
600
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
 
 
 
 
601
  return run, just_finish_intro
602
- else:
603
- logger.warning("No tool outputs to submit",
604
- extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
605
- run = PseudoRun(status="completed", metadata={"message": "No tool outputs to submit"})
606
- return run, just_finish_intro
607
- except Exception as e:
608
- # Cancel the run
609
- logger.error(f"Yeap Error in call_tool: {e}", extra={"user_id": self.cm.user.user_id, "endpoint": 'assistant_call_tool'})
610
- logger.error(f"Cancelling run {run.id} for thread {thread.id}", extra={"user_id": self.cm.user.user_id, "endpoint": 'cancel_erroneous_run'})
611
- self.cancel_run(run, thread)
612
- logger.error(f"Run {run.id} cancelled for thread {thread.id}", extra={"user_id": self.cm.user.user_id, "endpoint": 'cancel_erroneous_run'})
613
- raise e
614
-
615
- class PseudoRun:
616
- def __init__(self, status, id='pseudo_run', metadata=None):
617
- self.id = id
618
- self.status = status
619
- self.metadata = metadata or {}
620
 
621
 
622
  class GeneralAssistant(Assistant):
 
1
  import json
2
  import io
3
  import os
4
+ from datetime import datetime
5
  import json
6
  import random
7
  from time import sleep
 
8
  import pandas as pd
9
  from dotenv import load_dotenv
10
  import logging
 
 
 
11
 
12
+ from app.utils import get_users_mementos, print_log
 
13
  from app.flows import FOLLOW_UP_STATE, GENERAL_COACHING_STATE, MICRO_ACTION_STATE, REFLECTION_STATE
 
14
 
15
  load_dotenv()
16
 
17
  logger = logging.getLogger(__name__)
 
18
 
19
+ import requests
20
 
21
+ class SearchEngine:
22
+ BING_API_KEY = os.getenv("BING_API_KEY")
23
+ BING_ENDPOINT = 'https://api.bing.microsoft.com/v7.0'
24
+
25
+ @staticmethod
26
+ def search(feedback_type_name, search_term):
27
+ """
28
+ Public method to perform a search based on the feedback type.
29
+ """
30
+ search_methods = {
31
+ "Resource Links": SearchEngine._search_relevant_links,
32
+ "Book/Podcast Recommendations": SearchEngine._search_books_or_podcasts,
33
+ # "Success Stories/Testimonials": SearchEngine._search_success_stories,
34
+ "Inspirational Stories or Case Studies": SearchEngine._search_inspirational_stories,
35
+ "Fun Facts": SearchEngine._search_fun_facts,
36
+ # "Visual Content (Images, Infographics)": SearchEngine._search_visual_content,
37
+ "Personalised Recommendations": SearchEngine._search_personalized_recommendations
38
+ }
39
+
40
+ search_method = search_methods.get(feedback_type_name)
41
+
42
+ if search_method:
43
+ return search_method(search_term)
44
+ else:
45
+ return (feedback_type_name, search_term)
46
+
47
+ @staticmethod
48
+ def _search_relevant_links(search_term):
49
+ """
50
+ Uses Bing Web Search API to search for relevant links.
51
+ """
52
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
53
+ params = {'q': search_term, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
54
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
55
+ if response.status_code == 200:
56
+ data = response.json()
57
+ links = []
58
+ if 'webPages' in data and 'value' in data['webPages']:
59
+ for result in data['webPages']['value']:
60
+ links.append(result)
61
+ return links
62
+ return ["No relevant links found."]
63
+
64
+ @staticmethod
65
+ def _search_books_or_podcasts(search_term):
66
+ """
67
+ Uses Bing Web Search API to search for books or podcasts.
68
+ """
69
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
70
+ query = f"{search_term} book OR podcast"
71
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
72
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
73
+ if response.status_code == 200:
74
+ data = response.json()
75
+ recommendations = []
76
+ if 'webPages' in data and 'value' in data['webPages']:
77
+ for result in data['webPages']['value']:
78
+ title = result.get('name', 'Unknown Title')
79
+ url = result.get('url', '')
80
+ recommendations.append(f"{title}: {url}")
81
+ return recommendations
82
+ return ["No book or podcast recommendations found."]
83
+
84
+ @staticmethod
85
+ def _search_success_stories(search_term):
86
+ """
87
+ Uses Bing Web Search API to search for success stories.
88
+ """
89
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
90
+ query = f"{search_term} success stories"
91
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
92
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
93
+ if response.status_code == 200:
94
+ data = response.json()
95
+ stories = []
96
+ if 'webPages' in data and 'value' in data['webPages']:
97
+ for result in data['webPages']['value']:
98
+ title = result.get('name', 'Unknown Title')
99
+ url = result.get('url', '')
100
+ stories.append(f"{title}: {url}")
101
+ return stories
102
+ return ["No success stories found."]
103
+
104
+ @staticmethod
105
+ def _search_inspirational_stories(search_term):
106
+ """
107
+ Uses Bing Web Search API to search for inspirational stories or case studies.
108
+ """
109
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
110
+ query = f"{search_term} inspirational stories OR case studies"
111
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
112
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
113
+ if response.status_code == 200:
114
+ data = response.json()
115
+ stories = []
116
+ if 'webPages' in data and 'value' in data['webPages']:
117
+ for result in data['webPages']['value']:
118
+ title = result.get('name', 'Unknown Title')
119
+ url = result.get('url', '')
120
+ stories.append(f"{title}: {url}")
121
+ return stories
122
+ return ["No inspirational stories found."]
123
+
124
+ @staticmethod
125
+ def _search_fun_facts(search_term):
126
+ """
127
+ Uses Bing Web Search API to search for fun facts related to personal growth.
128
+ """
129
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
130
+ query = f"{search_term} fun facts"
131
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
132
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
133
+ if response.status_code == 200:
134
+ data = response.json()
135
+ facts = []
136
+ if 'webPages' in data and 'value' in data['webPages']:
137
+ for result in data['webPages']['value']:
138
+ snippet = result.get('snippet', '')
139
+ facts.append(snippet)
140
+ return facts
141
+ return ["No fun facts found."]
142
+
143
+ @staticmethod
144
+ def _search_visual_content(search_term):
145
+ """
146
+ Uses Bing Image Search API to search for images or infographics.
147
+ """
148
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
149
+ params = {'q': search_term, 'count': 3}
150
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/images/search", headers=headers, params=params)
151
+ if response.status_code == 200:
152
+ data = response.json()
153
+ images = []
154
+ if 'value' in data:
155
+ for result in data['value']:
156
+ image_url = result.get('contentUrl', '')
157
+ images.append(image_url)
158
+ return images
159
+ return ["No visual content found."]
160
+
161
+ @staticmethod
162
+ def _search_personalized_recommendations(search_term):
163
+ """
164
+ Uses Bing Web Search API to provide personalized recommendations.
165
+ """
166
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
167
+ query = f"tips for {search_term}"
168
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
169
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
170
+ if response.status_code == 200:
171
+ data = response.json()
172
+ recommendations = []
173
+ if 'webPages' in data and 'value' in data['webPages']:
174
+ for result in data['webPages']['value']:
175
+ title = result.get('name', 'Unknown Title')
176
+ url = result.get('url', '')
177
+ recommendations.append(f"{title}: {url}")
178
+ return recommendations
179
+ return ["No personalized recommendations found."]
180
 
181
  class FeedbackContent:
182
  def __init__(self, content, role):
 
278
  # return result
279
  return selected
280
 
281
+ def get_current_datetime():
282
+ return datetime.now()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
  class Assistant:
 
 
 
 
 
 
 
 
 
 
 
 
285
  def __init__(self, id, cm):
286
  self.id = id
287
  self.cm = cm
288
  self.recent_run = None
289
 
290
  def cancel_run(self, run, thread):
291
+ if run.status != 'completed':
292
+ cancel = self.cm.client.beta.threads.runs.cancel(thread_id=thread.id, run_id=run.id)
293
+ while cancel.status != 'cancelled':
294
+ sleep(0.05)
295
+ cancel = self.cm.client.beta.threads.runs.retrieve(
296
+ thread_id=thread.id,
297
+ run_id=cancel.id
298
  )
299
+ logger.info(f"Cancelled run: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
300
+ return True
301
+ else:
302
+ logger.info(f"Run already completed: {run.id}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_cancel_run"})
303
+ return False
304
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  def process(self, thread, text):
306
+ # 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:")
307
+ message = self.cm.add_message_to_thread(thread.id, "user", text)
308
+
309
+ run = self.cm.client.beta.threads.runs.create_and_poll(
310
+ thread_id=thread.id,
311
+ assistant_id=self.id,
312
+ model="gpt-4o-mini",
313
+ )
314
+ just_finished_intro = False
315
  try:
 
 
 
 
 
 
 
 
 
 
316
  if run.status == 'completed':
317
+ logger.info(f"Run Completed: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
 
318
  return run, just_finished_intro, message
319
 
320
+ reccursion = 0
321
+ logger.info(f"[Run Pending] Status: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
322
+ while run.status == 'requires_action':
323
+ logger.info(f"Run Calling tool [{reccursion}]: {run.status}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
324
+ run, just_finished_intro = self.call_tool(run, thread)
325
+ reccursion += 1
326
+ if run == "cancelled" or run == "change_goal":
327
+ break
328
+
329
+ if run in ['cancelled', 'change_goal'] or run.status != 'completed':
330
+ logger.error(f"RUN NOT COMPLETED: {run}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_process"})
331
+ finally:
332
+ self.recent_run = run
333
+ return run, just_finished_intro, message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
+ def call_tool(self, run, thread):
336
+ tool_outputs = []
337
+ logger.info(f"Required actions: {list(map(lambda x: f'{x.function.name}({x.function.arguments})', run.required_action.submit_tool_outputs.tool_calls))}",
338
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_call_tool"})
339
+ just_finish_intro = False
340
+ for tool in run.required_action.submit_tool_outputs.tool_calls:
341
+ if tool.function.name == "transition":
342
+ transitions = json.loads(tool.function.arguments)
343
+ logger.info(f"Transition: {transitions['from']} -> {transitions['to']}",
344
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
345
+
346
+ if transitions['from'] == "PLANNING STATE":
347
  tool_outputs.append({
348
  "tool_call_id": tool.id,
349
+ "output": f"help the user set a new goal"
350
  })
351
+ # logger.info(f"Exiting the introduction state",
352
+ # extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
353
+ # just_finish_intro = True
354
+ # # run = self.cancel_run(run, thread)
355
+ # # logger.info(f"Successfully cancelled run",
356
+ # # extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_transition"})
357
+ # tool_outputs.append({
358
+ # "tool_call_id": tool.id,
359
+ # "output": "true"
360
+ # })
361
+ else:
362
+ flow_instructions = ""
363
+ if transitions['to'] == "REFLECTION STATE":
364
+ flow_instructions = REFLECTION_STATE
365
+
366
+ next = self.cm.matters_most.next()
367
+ logger.info(f"Successfully moved to bext matters most: {next}")
368
+ question_format = random.choice(['[Option 1] Likert-Scale Objective Question','[Option 2] Multiple-Choice Question','[Option 3] Yes-No Question'])
369
+
370
+ 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
371
+ 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
372
 
373
+ """ + flow_instructions
 
374
 
375
+ elif transitions['to'] == "FOLLOW UP STATE":
376
+ flow_instructions = FOLLOW_UP_STATE
 
377
 
378
+ elif transitions['to'] == "GENERAL COACHING STATE":
379
+ flow_instructions = GENERAL_COACHING_STATE
 
 
 
 
 
 
380
 
381
  tool_outputs.append({
382
  "tool_call_id": tool.id,
383
+ "output": f"{flow_instructions}\n\n" + f"** Follow the above flow template to respond to the user **"
384
  })
385
+ elif tool.function.name == "get_date":
386
+ # print(f"[DATETIME]: {get_current_datetime()}")
387
+ # self.cm.state['date'] = 'date': pd.Timestamp.now().strftime("%Y-%m-%d %a %H:%M:%S")
388
+ # get and update the current time to self.cm.state['date'] but keep the date component
389
+ current_time = get_current_datetime()
390
+ # replace time component of self.cm.state['date'] with the current time
391
+ 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))
392
+ logger.info(f"Current datetime: {self.cm.state['date']}",
393
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_date"})
394
+
395
+ tool_outputs.append({
396
+ "tool_call_id": tool.id,
397
+ "output": f"{self.cm.state['date']}"
398
+ })
399
+ elif tool.function.name == "create_goals" or tool.function.name == "create_memento":
400
+ json_string = json.loads(tool.function.arguments)
401
+ json_string['created'] = str(self.cm.state['date'])
402
+ json_string['updated'] = None
403
+ logger.info(f"New event: {json_string}",
404
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_create_event"})
405
+
406
+ # Create a folder for the user's mementos if it doesn't exist
407
+ user_mementos_folder = os.path.join("mementos", "to_upload", self.cm.user.user_id)
408
+
409
+ # Ensure the directory exists
410
+ os.makedirs(user_mementos_folder, exist_ok=True)
411
+
412
+ # Construct the full file path for the JSON file
413
+ file_path = os.path.join(user_mementos_folder, f"{json_string['title']}.json")
414
+
415
+ # Save the JSON string as a file
416
+ with open(file_path, "w") as json_file:
417
+ json.dump(json_string, json_file)
418
+
419
+ tool_outputs.append({
420
+ "tool_call_id": tool.id,
421
+ "output": f"** [Success]: Added event to the user's vector store**"
422
+ })
423
+ elif tool.function.name == "msearch":
424
+ queries = json.loads(tool.function.arguments)['queries']
425
+ logger.info(f"Searching for: {queries}",
426
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_msearch"})
427
+
428
+ tool_outputs.append({
429
+ "tool_call_id": tool.id,
430
+ "output": f"** retrieve any files related to: {queries} **"
431
+ })
432
+ elif tool.function.name == "get_mementos":
433
+ logger.info(f"Getting mementos", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
434
+ args = json.loads(tool.function.arguments)
435
+ logger.info(f"ARGS: {args}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
436
+ queries = args['queries']
437
+
438
+ if 'on' not in args:
439
+ on = pd.to_datetime(self.cm.state['date']).date()
440
+ else:
441
+ on = args['on']
442
+ if on == '':
443
  on = pd.to_datetime(self.cm.state['date']).date()
444
  else:
445
+ on = pd.to_datetime(args['on']).date()
446
+
447
+
448
+ logger.info(f"Query date: {on}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
449
+ # query the user memento db for all mementos where follow_up_on is equal to the query date
450
+ mementos = get_users_mementos(self.cm.user.user_id, on)
451
+
452
+ # if on == "":
453
+ # instruction = f"** Fetch all files (mementos) from this thread's Memento vector_store ([id={self.cm.user_personal_memory.id}]) **"
454
+ # else:
455
+ # 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)**"
456
+ # # 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} **"
457
+
458
+ logger.info(f"Finish Getting mementos: {mementos}", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_get_mementos"})
459
+ tool_outputs.append({
460
+ "tool_call_id": tool.id,
461
+ "output": f"Today's mementos: {mementos}" if len(mementos) else "No mementos to follow up today."
462
+ })
463
+ elif tool.function.name == "get_feedback_types":
464
+ print_log("WARNING","Calling get_feedback_types", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
465
+ logger.warning("Calling get_feedback_types", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
466
+ feedbacks = [
467
+ ("Inspirational Quotes", "📜", "Short, impactful quotes from the user's chosen Legendary Persona"),
468
+ ("Tips & Advice", "💡", "Practical suggestions or strategies for personal growth (no need to say \"Tips:\" in the beggining of your tips)"),
469
+ ("Encouragement", "🌈", "Positive affirmations and supportive statements"),
470
+ ("Personalized Recommendations", "🧩", "Tailored suggestions based on user progress"),
471
+ ("Affirmations", "✨", "Positive statements for self-belief and confidence"),
472
+ ("Mindfulness/Meditation", "🧘‍♀️", "Guided prompts for mindfulness practice"),
473
+ ("Book/Podcast", "📚🎧", "Suggestions aligned with user interests"),
474
+ ("Habit Tip", "🔄", "Tips for building and maintaining habits"),
475
+ ("Seasonal Content", "🌸", "Time and theme-relevant interactions"),
476
+ ("Fun Fact", "🎉", "Interesting and inspiring facts (no need to say \"Fun Fact:\" in the beggining of your tips)"),
477
+ ("Time Management", "⏳", "Tips for effective time management")
478
+ ]
479
+ sample_feedbacks = random.sample(feedbacks, 3)
480
+ print_log("INFO",f"Feedback types: {sample_feedbacks}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
481
+ logger.info(f"Feedback types: {sample_feedbacks}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_feedback_types"})
482
+ tool_outputs.append({
483
  "tool_call_id": tool.id,
484
+ "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)
485
+ })
486
+ elif tool.function.name == "search_resource":
487
+ type = json.loads(tool.function.arguments)['resource_type']
488
+ query = json.loads(tool.function.arguments)['query']
489
+ logger.info(f"Getting microaction theme: {type} - {query}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
490
+ relevant_context = SearchEngine.search(type, query)
491
+ logger.info(f"Finish Getting microaction theme: {relevant_context}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
492
+ tool_outputs.append({
493
+ "tool_call_id": tool.id,
494
+ "output": f"** Relevant context: {relevant_context} **"
495
+ })
496
+ elif tool.function.name == "end_conversation":
497
+ day_n = json.loads(tool.function.arguments)['day_n']
498
+ completed_micro_action = json.loads(tool.function.arguments)['completed_micro_action']
499
+ area_of_deep_reflection = json.loads(tool.function.arguments)['area_of_deep_reflection']
500
+
501
+ self.cm.user.update_micro_action_status(completed_micro_action)
502
+ self.cm.user.trigger_deep_reflection_point(area_of_deep_reflection)
503
+
504
+ # NOTE: we will record whether the user has completed the theme for the day
505
+ # NOTE: if no, we will include a short followup message to encourage the user the next day
506
+ 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"})
507
+ tool_outputs.append({
508
+ "tool_call_id": tool.id,
509
+ "output": "true"
510
+ })
511
+ elif tool.function.name == "create_smart_goal":
512
+ print_log("WARNING", f"Creating a SMART goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
513
+ logger.warning(f"Creating a SMART goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
 
515
+ user_goal = json.loads(tool.function.arguments)['goal']
516
+ user_goal_area = json.loads(tool.function.arguments)['area']
517
+ user_goal_status = 'PENDING'
518
 
519
+ self.cm.user.set_goal(user_goal, user_goal_area, user_goal_status)
 
520
 
521
+ print_log("INFO", f"SMART goal approved: {user_goal}", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
522
+ logger.info(f"SMART goal approved: {user_goal}", extra={"user_id": self.cm.user.user_id, "endpoint": "create_smart_goal"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
 
524
+ tool_outputs.append({
525
+ "tool_call_id": tool.id,
526
+ "output": "true"
527
+ })
528
+ elif tool.function.name == "start_now":
529
+ logger.info(f"Starting Growth Plan on Day 0",
530
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_start_now"})
531
+ # set intro finish to true
532
+ just_finish_intro = True
533
+
534
+ # cancel current run
535
+ run = self.cancel_run(run, thread)
536
+ logger.info(f"Successfully cancelled run",
537
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_start_now"})
538
+ return "cancelled", just_finish_intro
539
+ elif tool.function.name == "change_goal":
540
+ logger.info(f"Changing user goal...", extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_change_goal"})
541
+
542
+ # switch back to the intro assistant, so we set just_finish_intro to False again
543
+ just_finish_intro = False
544
+
545
+ # cancel current run
546
+ run = self.cancel_run(run, thread)
547
+ logger.info(f"Successfully cancelled run",
548
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_change_goal"})
549
+ return "change_goal", just_finish_intro
550
+
551
+ elif tool.function.name == "change_goal":
552
+ pass
553
+
554
+ elif tool.function.name == "complete_goal":
555
+ self.cm.user.update_goal_status()
556
+ logger.info(f"Updated recent goal status as COMPLETED.", extra={"user_id": self.cm.user.user_id, "endpoint": "complete_goal"})
557
+ tool_outputs.append({
558
+ "tool_call_id": tool.id,
559
+ "output": "true"
560
+ })
561
+
562
+ # Submit all tool outputs at once after collecting them in a list
563
+ if tool_outputs:
564
+ try:
565
  run = self.cm.client.beta.threads.runs.submit_tool_outputs_and_poll(
566
  thread_id=thread.id,
567
  run_id=run.id,
 
569
  )
570
  logger.info("Tool outputs submitted successfully",
571
  extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
572
+ except Exception as e:
573
+ logger.error(f"Failed to submit tool outputs: {str(e)}",
574
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
575
+ finally:
576
  return run, just_finish_intro
577
+ else:
578
+ logger.warning("No tool outputs to submit",
579
+ extra={"user_id": self.cm.user.user_id, "endpoint": "assistant_submit_tools"})
580
+ return {"status": "No tool outputs to submit"}
581
+
582
+
 
 
 
 
 
 
 
 
 
 
 
 
583
 
584
 
585
  class GeneralAssistant(Assistant):
app/cache.py DELETED
@@ -1,205 +0,0 @@
1
- import time
2
- import threading
3
- import logging
4
- import boto3
5
- from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError
6
- import os
7
- import re
8
-
9
- from dotenv import load_dotenv
10
-
11
- from app.exceptions import DBError
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- load_dotenv()
16
-
17
- AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY')
18
- AWS_SECRET_KEY = os.getenv('AWS_SECRET_KEY')
19
- REGION = os.getenv('AWS_REGION')
20
-
21
- def upload_file_to_s3(filename):
22
- # Extract user_id from the filename
23
- user_id = os.path.basename(filename).split('.')[0]
24
- # user_id = filename.split('.')[0]
25
- print(user_id)
26
- function_name = upload_file_to_s3.__name__
27
- logger.info(f"Uploading file {filename} to S3", extra={'user_id': user_id, 'endpoint': function_name})
28
- bucket = 'core-ai-assets'
29
- try:
30
- if (AWS_ACCESS_KEY and AWS_SECRET_KEY):
31
- session = boto3.session.Session(aws_access_key_id=AWS_ACCESS_KEY, aws_secret_access_key=AWS_SECRET_KEY, region_name=REGION)
32
- else:
33
- session = boto3.session.Session()
34
- s3_client = session.client('s3')
35
- with open(filename, "rb") as f:
36
- ## Upload to Production Folder
37
- s3_client.upload_fileobj(f, bucket, f'dev/users/{user_id}.pkl')
38
- logger.info(f"File {filename} uploaded successfully to S3", extra={'user_id': user_id, 'endpoint': function_name})
39
-
40
- os.remove(filename)
41
-
42
- # force_file_move(os.path.join('users', 'to_upload', filename), os.path.join('users', 'data', filename))
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):
50
- """
51
- ttl: Time to live (in seconds) for each item
52
- cleanup_interval: Interval at which background thread checks for expired items
53
- """
54
- self.ttl = ttl
55
- self.data = {} # key -> (value, expiration_time)
56
- self.lock = threading.Lock()
57
- self.cleanup_interval = cleanup_interval
58
- self.running = True
59
- self.cleanup_thread = threading.Thread(target=self._cleanup_task, daemon=True) # for periodic cleanup
60
- self.cleanup_thread.start()
61
-
62
- def __setitem__(self, key, value):
63
- """
64
- For item assignment i.e. cache[key] = value
65
- """
66
- self.set(key, value)
67
-
68
- def set(self, key, value):
69
- # set expiration time as current time + ttl
70
- expire_at = time.time() + self.ttl
71
- with self.lock:
72
- self.data[key] = (value, expire_at)
73
-
74
- def get(self, key, default=None):
75
- """
76
- Get a value from the cache. Returns default if key not found or expired.
77
- Resets the TTL if the item is successfully accessed.
78
- """
79
- with self.lock:
80
- entry = self.data.get(key)
81
- if not entry:
82
- return default
83
- value, expire_at = entry
84
- now = time.time()
85
- if now > expire_at:
86
- # item expired, evict and upload
87
- self._evict_item(key, value)
88
- return default
89
- # refresh this data's TTL since it's accessed
90
- self.data[key] = (value, now + self.ttl)
91
- return value
92
-
93
- def __contains__(self, key):
94
- """
95
- Chheck if a key is in the cache and not expired.
96
- If not expired, reset the TTL.
97
- """
98
- with self.lock:
99
- entry = self.data.get(key)
100
- if not entry:
101
- return False
102
- value, expire_at = entry
103
- now = time.time()
104
- if now > expire_at:
105
- self._evict_item(key, value)
106
- return False
107
- # refresh TTL
108
- self.data[key] = (value, now + self.ttl)
109
- return True
110
-
111
- def __getitem__(self, key):
112
- """
113
- Retrieve an item i.e. user_cache[key].
114
- Raises KeyError if not found or expired.
115
- Resets the TTL if the item is successfully accessed.
116
- """
117
- with self.lock:
118
- if key not in self.data:
119
- raise KeyError(key)
120
- value, expire_at = self.data[key]
121
- now = time.time()
122
- if now > expire_at:
123
- self._evict_item(key, value)
124
- raise KeyError(key)
125
- self.data[key] = (value, now + self.ttl)
126
- return value
127
-
128
- def reset_cache(self):
129
- """
130
- Reset the cache by removing all items.
131
- """
132
- with self.lock:
133
- for key, value in self.data.items():
134
- self._evict_item(key, value)
135
- self.data = {}
136
-
137
- def pop(self, key, default=None):
138
- """
139
- Remove an item from the cache and return its value.
140
- Upload to S3 if it hasn't expired yet.
141
- """
142
- with self.lock:
143
- entry = self.data.pop(key, None)
144
- if entry:
145
- value, expire_at = entry
146
- # upload before removal
147
- self._upload_value(key, value)
148
- return value
149
- return default
150
-
151
- def close(self):
152
- """
153
- Stop the background cleanup thread.
154
- Technically we should call this when done using the cache but I guess we are never done using it lol.
155
- """
156
- self.running = False
157
- self.cleanup_thread.join()
158
-
159
- def _cleanup_task(self):
160
- """
161
- Background task that periodically checks for expired items and removes them.
162
- """
163
- while self.running:
164
- now = time.time()
165
- keys_to_remove = []
166
- with self.lock:
167
- for k, (v, exp) in list(self.data.items()):
168
- if now > exp:
169
- keys_to_remove.append(k)
170
-
171
- # Evict expired items outside the lock to handle uploading
172
- for k in keys_to_remove:
173
- with self.lock:
174
- if k in self.data:
175
- value, _ = self.data.pop(k)
176
- self._evict_item(k, value)
177
-
178
- time.sleep(self.cleanup_interval)
179
-
180
- def _evict_item(self, key, value):
181
- """
182
- Called internally when an item expires. upload to S3 before removal.
183
- """
184
- self._upload_value(key, value)
185
-
186
- def _upload_value(self, key, value):
187
- """
188
- Saves the user's data and uploads the resulting file to S3.
189
- """
190
- try:
191
- filename = value.save_user()
192
- logger.info(
193
- f"FILENAME = {filename}",
194
- extra={'user_id': key, 'endpoint': 'cache_eviction'}
195
- )
196
- upload_file_to_s3(filename)
197
- logger.info(
198
- f"User {key} saved to S3 during cache eviction",
199
- extra={'user_id': key, 'endpoint': 'cache_eviction'}
200
- )
201
- except Exception as e:
202
- logger.error(
203
- f"Failed to save user {key} to S3 during cache eviction: {e}",
204
- extra={'user_id': key, 'endpoint': 'cache_eviction'}
205
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/conversation_manager.py DELETED
@@ -1,351 +0,0 @@
1
- import base64
2
- import os
3
- import openai
4
- import pandas as pd
5
- from datetime import datetime, timezone
6
- from app.assistants import Assistant
7
- import random
8
- import logging
9
- from app.exceptions import BaseOurcoachException, ConversationManagerError, OpenAIRequestError
10
- from datetime import datetime
11
-
12
- import dotenv
13
-
14
- from app.utils import get_user_local_timezone, id_to_persona
15
- from PIL import Image
16
- import io
17
- dotenv.load_dotenv()
18
-
19
- OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
- def get_current_datetime():
24
- return datetime.now(timezone.utc)
25
-
26
- class ConversationManager:
27
- def __init__(self, client, user, asst_id, intro_done=False):
28
- self.user = user
29
- self.intro_done = intro_done
30
- self.assistants = {'general': Assistant(asst_id, self), 'intro': Assistant('asst_baczEK65KKvPWIUONSzdYH8j', self)}
31
-
32
- self.client = client
33
-
34
- # get users local timezone
35
- timezone = get_user_local_timezone(user.user_id)
36
-
37
- # convert UTC to user's timezone
38
- local_time = pd.Timestamp.now(tz='UTC').tz_convert(timezone)
39
- self.state = {'date': local_time.strftime("%Y-%m-%d %a %H:%M:%S")}
40
-
41
- self.current_thread = self.create_thread()
42
- self.daily_thread = None
43
-
44
- logger.info("Initializing conversation state", extra={"user_id": self.user.user_id, "endpoint": "conversation_init"})
45
-
46
- def __getstate__(self):
47
- state = self.__dict__.copy()
48
- # Remove unpicklable or unnecessary attributes
49
- if 'client' in state:
50
- del state['client']
51
- return state
52
-
53
- def __setstate__(self, state):
54
- self.__dict__.update(state)
55
- # Re-initialize attributes that were not pickled
56
- self.client = None
57
-
58
- def catch_error(func):
59
- def wrapper(self, *args, **kwargs):
60
- try:
61
- return func(self, *args, **kwargs)
62
- except BaseOurcoachException as e:
63
- raise e
64
- except openai.BadRequestError as e:
65
- raise OpenAIRequestError(user_id=self.user.user_id, message="OpenAI Request Error", e=str(e))
66
- except Exception as e:
67
- # Handle other exceptions
68
- logger.error(f"An unexpected error occurred: {e}")
69
- raise ConversationManagerError(user_id=self.user.user_id, message="Unexpected error in ConversationManager", e=str(e))
70
- return wrapper
71
-
72
- @catch_error
73
- def create_thread(self):
74
- user_interaction_guidelines =self.user.user_interaction_guidelines
75
- thread = self.client.beta.threads.create()
76
- self.system_message = self.add_message_to_thread(thread.id, "assistant",
77
- f"""
78
- You are:
79
- {id_to_persona(self.user.asst_id)}, always adhere to your choosen persona by incorporating it conversationally.
80
- You represent a coach at ourcoach. You may refer to you Knowledgebase (ourcoach FAQ) for all information related to ourcoach.
81
- ** Branding ** Always stylize ourcoach as 'ourcoach' instead of 'OurCoach' or 'Ourcoach', regardless of any grammatical errors.
82
- **You truly believe you are your persona**
83
- **You always try to be engaging and personal with your coachee in order to get them to continue interacting with you daily**
84
- -------------------------------------------
85
- You are coaching:
86
- \n\n{user_interaction_guidelines}\n\n\
87
- Be mindful of this information at all times in order to
88
- be as personalised as possible when conversing. Ensure to
89
- follow the conversation guidelines and flow templates. Use the
90
- current state of the conversation to adhere to the flow. Do not let the user know about any transitions.\n\n
91
- Whenever you reference the ourcoach dashboard, refer to it as 'Revelation Dashboard' and include the link: {OURCOACH_DASHBOARD_URL} .
92
- ** Today is {self.state['date']}.\n\n **
93
- ** You are now in the INTRODUCTION STATE. **
94
- """)
95
- return thread
96
-
97
- @catch_error
98
- def _get_current_thread_history(self, remove_system_message=True, _msg=None, thread=None):
99
- if thread is None:
100
- thread = self.current_thread
101
- if not remove_system_message:
102
- return [{"role": msg.role, "content": msg.content[0].text.value} for msg in self.client.beta.threads.messages.list(thread.id, order="asc")]
103
- if _msg:
104
- 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:]
105
- 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
106
-
107
- @catch_error
108
- def add_message_to_thread(self, thread_id, role, content):
109
- message = self.client.beta.threads.messages.create(
110
- thread_id=thread_id,
111
- role=role,
112
- content=content
113
- )
114
- return message
115
-
116
- @catch_error
117
- def _run_current_thread(self, text, thread=None, hidden=False, media=None):
118
- image_context = ""
119
- if media:
120
- logger.info(f"Describing Media", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
121
- # assert media is a base64 encoded string
122
- try:
123
- # scale down media to 512x512
124
- img_data = base64.b64decode(media)
125
- img = Image.open(io.BytesIO(img_data))
126
- img = img.resize((512, 512), Image.Resampling.LANCZOS) # Better quality resizing
127
- buffered = io.BytesIO()
128
- img.save(buffered, format="JPEG", quality=85) # Specify format and quality
129
- media = base64.b64encode(buffered.getvalue()).decode()
130
-
131
- except Exception as e:
132
- raise ConversationManagerError(user_id=self.user.user_id, message="Invalid base64 image data", e=str(e))
133
- logger.info(f"Media is valid", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
134
- response = self.client.chat.completions.create(
135
- model="gpt-4o",
136
- messages=[
137
- {
138
- "role": "user",
139
- "content": [
140
- {
141
- "type": "text",
142
- "text": f"""Describe this image to an LLM for it to have rich and complete context.
143
- {'Lock in on things related to the users message:'+text if text else text}""",
144
- },
145
- {
146
- "type": "image_url",
147
- "image_url": {"url": f"data:image/jpeg;base64,{media}", "detail": "low"},
148
- },
149
- ],
150
- }
151
- ])
152
-
153
- image_context = response.choices[0].message.content
154
- logger.info(f"Image context: {image_context}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
155
-
156
- if thread is None:
157
- thread = self.current_thread
158
- logger.warning(f"{self}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
159
-
160
- if image_context:
161
- text = f"{text}\n\n[MEDIA ATTACHMENT] User attached an image about: {image_context}"
162
-
163
- # need to select assistant
164
- if self.intro_done:
165
- logger.info(f"Running general assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
166
- run, just_finished_intro, message = self.assistants['general'].process(thread, text)
167
- else:
168
- logger.info(f"Running intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
169
- run, just_finished_intro, message = self.assistants['intro'].process(thread, text)
170
-
171
- logger.info(f"Run done, status={run.status} just finished intro: {just_finished_intro}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
172
-
173
- if 'message' in run.metadata:
174
- info = run.metadata['message']
175
-
176
- if info == 'start_now':
177
- self.intro_done = True
178
- logger.info(f"Start now", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
179
- elif info == 'change_goal':
180
- self.intro_done = False
181
- logger.info(f"Changing goal, reset to intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
182
- # Actually dont need this
183
- elif info == 'error':
184
- logger.error(f"Run was cancelled due to error", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
185
- # self.add_message_to_thread(thread.id, "assistant", run.metadata['content'])
186
- # return self._get_current_thread_history(remove_system_message=False)[-1], run
187
-
188
- if hidden:
189
- self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
190
- logger.info(f"Deleted hidden message", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
191
-
192
- return self._get_current_thread_history(remove_system_message=False)[-1], run
193
-
194
- @catch_error
195
- def _send_and_replace_message(self, text, replacement_msg=None):
196
- logger.info(f"Sending hidden message", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
197
- response, _ = self._run_current_thread(text, hidden=True)
198
-
199
- # check if there is a replacement message
200
- if replacement_msg:
201
- logger.info(f"Adding replacement message", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
202
- # get the last message
203
- last_msg = list(self.client.beta.threads.messages.list(self.current_thread.id, order="asc"))[-1]
204
- # logger.info(f"Last message: {last_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
205
- response = last_msg.content[0].text.value
206
-
207
- # delete the last message
208
- self.client.beta.threads.messages.delete(message_id=last_msg.id, thread_id=self.current_thread.id)
209
- self.add_message_to_thread(self.current_thread.id, "user", replacement_msg)
210
- self.add_message_to_thread(self.current_thread.id, "assistant", response)
211
-
212
- logger.info(f"Hidden message response: {response}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
213
- # NOTE: this is a hack, should get the response straight from the run
214
- return {'content': response, 'role': 'assistant'}
215
-
216
- @catch_error
217
- def _add_ai_message(self, text):
218
- return self.add_message_to_thread(self.current_thread.id, "assistant", text)
219
-
220
- @catch_error
221
- def get_daily_thread(self):
222
- if self.daily_thread is None:
223
- messages = self._get_current_thread_history(remove_system_message=False)
224
-
225
- self.daily_thread = self.client.beta.threads.create(
226
- messages=messages[:30]
227
- )
228
-
229
- # Add remaining messages one by one if there are more than 30
230
- for msg in messages[30:]:
231
- self.add_message_to_thread(
232
- self.daily_thread.id,
233
- msg['role'],
234
- msg['content']
235
- )
236
- self.last_daily_message = list(self.client.beta.threads.messages.list(self.daily_thread.id, order="asc"))[-1]
237
- else:
238
- messages = self._get_current_thread_history(remove_system_message=False, _msg=self.last_daily_message)
239
- self.client.beta.threads.delete(self.daily_thread.id)
240
- self.daily_thread = self.client.beta.threads.create(messages=messages)
241
- self.last_daily_message = list(self.client.beta.threads.messages.list(self.daily_thread.id, order="asc"))[-1]
242
- 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"})
243
- logger.info(f"Last Daily Message: {self.last_daily_message}", extra={"user_id": self.user.user_id, "endpoint": "send_morning_message"})
244
- return self._get_current_thread_history(thread=self.daily_thread)
245
- # [{"role":, "content":}, ....]
246
-
247
- @catch_error
248
- def _send_morning_message(self, text, add_to_main=False):
249
- # create a new thread
250
- # OPENAI LIMITATION: Can only attach a maximum of 32 messages when creating a new thread
251
- messages = self._get_current_thread_history(remove_system_message=False)
252
- if len(messages) >= 29:
253
- messages = [{"content": """ Remember who you are and who you are coaching.
254
- Be mindful of this information at all times in order to
255
- be as personalised as possible when conversing. Ensure to
256
- follow the conversation guidelines and flow provided.""", "role":"assistant"}] + messages[-29:]
257
- # logger.info(f"Current Thread Messages: {messages}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
258
-
259
- temp_thread = self.client.beta.threads.create(messages=messages)
260
- # logger.info(f"Created Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
261
-
262
- if add_to_main:
263
- logger.info(f"Adding message to main thread", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
264
- self.add_message_to_thread(self.current_thread.id, "assistant", text)
265
-
266
- self.add_message_to_thread(temp_thread.id, "user", text)
267
-
268
- self._run_current_thread(text, thread=temp_thread)
269
- response = self._get_current_thread_history(thread=temp_thread)[-1]
270
- logger.info(f"Hidden Response: {response}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
271
-
272
- # delete temp thread
273
- self.client.beta.threads.delete(temp_thread.id)
274
- logger.info(f"Deleted Temp Thread", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
275
-
276
- return response
277
-
278
- @catch_error
279
- def delete_hidden_messages(self, old_thread=None):
280
- if old_thread is None:
281
- old_thread = self.current_thread
282
-
283
- # find all hidden messages to delete
284
- to_delete = [{'id': msg.id,'content': msg.content[0].text.value, 'role': msg.role} for msg in self.client.beta.threads.messages.list(self.current_thread.id, order="desc") if msg.content[0].text.value.startswith("[hidden]")]
285
- logger.info(f"Marked for deletion= {to_delete}", extra={"user_id": self.user.user_id, "endpoint": "delete_hidden_messaged"})
286
-
287
- for msg in to_delete:
288
- # delete msg
289
- deleted_message = self.client.beta.threads.messages.delete(
290
- message_id=msg['id'],
291
- thread_id=self.current_thread.id,
292
- )
293
- logger.info(f"Deleted Message = {deleted_message}", extra={"user_id": self.user.user_id, "endpoint": "delete_hidden_messaged"})
294
- return True
295
-
296
- @catch_error
297
- def cancel_run(self, run, thread = None):
298
- # Cancels and recreates a thread
299
- logger.info(f"(CM) Cancelling run {run} for thread {thread}", extra={"user_id": self.user.user_id, "endpoint": "cancel_run"})
300
- if thread is None:
301
- thread = self.current_thread.id
302
-
303
- if self.intro_done:
304
- self.assistants['general'].cancel_run(run, thread)
305
- else:
306
- self.assistants['intro'].cancel_run(run, thread)
307
-
308
- logger.info(f"Run cancelled", extra={"user_id": self.user.user_id, "endpoint": "cancel_run"})
309
- return True
310
-
311
- @catch_error
312
- def clone(self, client):
313
- """Creates a new ConversationManager with copied thread messages."""
314
- # Create new instance with same init parameters
315
- new_cm = ConversationManager(
316
- client,
317
- self.user,
318
- self.assistants['general'].id,
319
- intro_done=True
320
- )
321
-
322
- # Get all messages from current thread
323
- messages = self._get_current_thread_history(remove_system_message=False)
324
-
325
- # Delete the automatically created thread from constructor
326
- new_cm.client.beta.threads.delete(new_cm.current_thread.id)
327
-
328
- # Create new thread with first 30 messages
329
- new_cm.current_thread = new_cm.client.beta.threads.create(
330
- messages=messages[:30]
331
- )
332
-
333
- # Add remaining messages one by one if there are more than 30
334
- for msg in messages[30:]:
335
- new_cm.add_message_to_thread(
336
- new_cm.current_thread.id,
337
- msg['role'],
338
- msg['content']
339
- )
340
-
341
- # Copy other relevant state
342
- new_cm.state = self.state
343
-
344
- return new_cm
345
-
346
- def __str__(self):
347
- return f"ConversationManager(intro_done={self.intro_done}, assistants={self.assistants}, current_thread={self.current_thread})"
348
-
349
- def __repr__(self):
350
- return (f"ConversationManager("
351
- f"intro_done={self.intro_done}, current_thread={self.current_thread})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/exceptions.py DELETED
@@ -1,113 +0,0 @@
1
- from enum import Enum
2
- from typing import Optional, Dict, Any
3
- import traceback
4
- import logging
5
- import json
6
- import re
7
- from datetime import datetime
8
-
9
- logger = logging.getLogger(__name__)
10
-
11
- import re
12
-
13
- class BaseOurcoachException(Exception):
14
- def __init__(self, **kwargs):
15
- self.user_id = kwargs.get('user_id', 'no-user')
16
- self.code = kwargs.get('code', 'UnknownError')
17
- self.message = kwargs.get('message', 'An unknown error occurred')
18
- self.e = kwargs.get('e', None)
19
-
20
- # Initialize the parent Exception with the message
21
- Exception.__init__(self, self.message)
22
-
23
- # Capture the full traceback
24
- self.stack_trace = traceback.format_exc()
25
- self.timestamp = datetime.utcnow()
26
-
27
- logger.exception(f"Error raised with code={self.code}, message={self.message}, user_id={self.user_id}", exc_info=self.e)
28
-
29
- def get_formatted_details(self) -> dict:
30
- def format_traceback(traceback_str: str) -> str:
31
- # Extract error type and message
32
- error_pattern = r"(\w+Error): (.+?)(?=\n|$)"
33
- error_match = re.search(error_pattern, traceback_str)
34
- error_type = error_match.group(1) if error_match else "Unknown Error"
35
- error_msg = error_match.group(2) if error_match else ""
36
-
37
- # Extract file paths and line numbers
38
- file_pattern = r"File \"(.+?)\", line (\d+), in (\w+)"
39
- matches = re.findall(file_pattern, traceback_str)
40
-
41
- # Build formatted output
42
- formatted_lines = [f"Error: {error_type} - {error_msg}\n"]
43
-
44
- for filepath, line_num, func_name in matches:
45
- if func_name == "wrapper":
46
- continue
47
- # Convert to relative path
48
- rel_path = filepath.split('app/')[-1] if 'app/' in filepath else filepath.split('\\')[-1]
49
- formatted_lines.append(f"at {rel_path}:{func_name} (line {line_num})")
50
-
51
- return "\n".join(formatted_lines)
52
- """Returns pinpointed error details."""
53
- return {
54
- "type": f"{self.__class__.__name__}{'.' + self.code if self.code != self.__class__.__name__ else ''}",
55
- "message": self.message,
56
- "stack_trace": format_traceback(self.stack_trace),
57
- "user_id": self.user_id,
58
- "at": self.timestamp.isoformat(),
59
- }
60
-
61
- def to_json(self) -> Dict[str, Any]:
62
- """Convert exception to JSON-serializable dictionary"""
63
- return self.get_formatted_details()
64
-
65
- def __str__(self) -> str:
66
- return json.dumps(self.to_json(), indent=2)
67
-
68
- class DBError(BaseOurcoachException):
69
- ALLOWED_CODES = ['S3Error', 'SQLError', 'NoOnboardingError', 'NoPickleError', 'NoBookingError']
70
- def __init__(self, **kwargs):
71
- if kwargs.get('code') not in self.ALLOWED_CODES:
72
- raise ValueError(f"Invalid code for DBError: {kwargs.get('code')}")
73
- super().__init__(**kwargs)
74
-
75
- def to_json(self) -> Dict[str, Any]:
76
- base_json = super().to_json()
77
- base_json["allowed_codes"] = self.ALLOWED_CODES
78
- return base_json
79
-
80
- class OpenAIRequestError(BaseOurcoachException):
81
- def __init__(self, **kwargs):
82
- super().__init__(**kwargs)
83
- self.run_id = kwargs.get('run_id', None)
84
-
85
- def to_json(self) -> Dict[str, Any]:
86
- base_json = super().to_json()
87
- base_json["run_id"] = self.run_id
88
- return base_json
89
-
90
- class AssistantError(BaseOurcoachException):
91
- def __init__(self, **kwargs):
92
- kwargs['code'] = "AssistantError"
93
- super().__init__(**kwargs)
94
-
95
- class UserError(BaseOurcoachException):
96
- def __init__(self, **kwargs):
97
- kwargs['code'] = "UserError"
98
- super().__init__(**kwargs)
99
-
100
- class ConversationManagerError(BaseOurcoachException):
101
- def __init__(self, **kwargs):
102
- kwargs['code'] = "ConversationManagerError"
103
- super().__init__(**kwargs)
104
-
105
- class FastAPIError(BaseOurcoachException):
106
- def __init__(self, **kwargs):
107
- kwargs['code'] = "FastAPIError"
108
- super().__init__(**kwargs)
109
-
110
- class UtilsError(BaseOurcoachException):
111
- def __init__(self, **kwargs):
112
- kwargs['code'] = "UtilsError"
113
- super().__init__(**kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/flows.py CHANGED
@@ -1,411 +1,295 @@
1
- POST_GG_STATE = f"""
2
- * Post-Growth Guide Message *
3
- Objective: Provide a space for reflection on the user's growth guide session while providing encouragement and inspiration to the user to help them stay motivated on their growth journey.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- The user:
6
- {{}}
 
 
 
 
 
 
 
 
 
 
7
 
8
- Summary of the Growth Guide Session:
9
- {{}}
10
 
11
- Growth Guide Session Notes:
12
- {{}}
13
 
14
- Guidelines:
15
  ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
16
 
17
  ** IMPORTANT **: When you feel that the user is replying with short messages and not in the mood to chat, end the conversation immediately. Always try to make the conversation quick & not asking too many questions
18
 
19
- ** IMPORTANT **: Only send a maximum of 3 questions on this day. Do **not** overwhelm the user with too many questions! And there should only be one question mark in one message! (max one question in a single message)
 
 
 
 
20
 
21
- ** IMPORTANT **: Don't forget the message formatting rule where it applies: Use a single line break right before asking a question and encapsulate the question with asterisk, like this: *question*!
22
 
23
  ** IMPORTANT **: Do not explicitly state the function name that you are calling in the response
24
 
25
- ** IMPORTANT **: You must end the interaction gracefully. End with a valuable statement to encourage the user.
 
 
26
 
27
  ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow.
28
 
29
- ** IMPORTANT **: Don't ask the user for ideas on what to do. Remember that you are the coach and you should suggest ideas/actions to the user. You will be punished if you don't adhere to this.
30
 
31
- ** IMPORTANT **: If the user asks anything that results into a list, you only give a maximum 3 items !
32
 
33
- ** IMPORTANT **: Keep the conversation creative, but make sure your responses are short, conversational, human-like and succinct like you are interacting via text messages as a modern and adept texter.
34
 
35
- Keep the message short (between 1-3 lines maximum), please.
36
 
37
- Task:
38
- The user has just completed their growth guide session. Generate an appropriate and encouraging message that reflects on the user's Growth Guide session and provides inspiration for their continued growth journey. Use the user's profile information and the summary of the Growth Guide session to craft a personalized and motivational message. Remember to keep the message concise and engaging to encourage the user to continue their growth journey with enthusiasm.
39
 
 
 
40
  """
41
 
42
- MICRO_ACTION_STATE = f"""
43
- **Micro-Action** *(PLEASE READ CAREFULLY)*
44
- **Objective:** Build momentum toward the user’s goal with small, actionable tasks that feel immediately achievable. Avoid saying “Today’s micro-action is:”—state the action naturally.
45
-
46
- **User’s Context:**
47
- - **Goal:** {{}}
48
- - **Day:** {{}}/{{}} of their journey.
49
- - **User's Upcoming (Postponed) Micro-Actions:** {{}}
50
-
51
- *(If the user has postponed micro-actions from the list above, remind them of the postponed micro-actions and ask if they are ready to do it today. If the user says yes, proceed with the postponed micro-action. If the user says no, propose a new micro-action.)*
52
- ---
53
- **The Order of Your Conversation Flow (Follow these step-by-step instructions! You are sending 3 messages in a day):**
54
 
55
- Step 0: Check whether the user has answered your yesterday's question (if any)
56
 
57
- Step 1:
58
 
59
- **First Message: Micro-Action Suggestion**
60
 
61
- - Propose one clear, actionable task for the day.
62
- - Ensure it is relevant, timely, easy to begin, and framed positively.
63
- - You can call search_web(...) to get better context or timely recommendations to incorporate into your response.
64
- - Avoid repeating previous actions.
65
- - Your message must be concise! (use Whatsapp texting length)
66
- - Wait for the user's response
67
 
68
- **Second Message: User Response Handling**
69
 
70
- - **If the user says they can only do it on a later time:**
71
- - Send an understanding yet encouraging message (Your message must be concise!)
72
- - **Ask** what time to remind them (use this question only if necessary).
73
- - Immediately call the `process_reminder()` function:
74
- ```
75
- process_reminder(
76
- 'content': '⏰ **Reminder**: Hey <user's name>! 🌟 Just a friendly reminder to do <postponed micro-action>',
77
- 'timestamp': '<relevant timestamp based on user’s message>',
78
- 'recurrence': 'postponed',
79
- 'action': 'set'
80
- )
81
- ```
82
- - Ensure that 'content' is framed from the perspective of the current day. For example, if the reminder is for an event happening tomorrow, the message should say "<reminder> ... today!" instead of "<reminder> ... tomorrow!". Ensure that it is timely and immediately relevant.
83
- - Ensure that you set the `'recurrence'` to `'postponed'`.
84
- - Then, end the conversation by:
85
- - Providing two short, specific, technical, and practical pieces of advice in bullet points
86
- - Immediately call the `end_conversation()` function without waiting for user's response
87
- - *(Do **not** explicitly mention the content of the reminder to the user.)*
88
 
89
- - **If the user has decided to try out the micro-action:**
90
- - Encourage them to proceed with the task (Your message must be concise!)
91
- - Ok coach, if you feel like its appropriate i.e, the quality of the microaction can be enhanced via a picture or the microaction accountability tracking can be enhaced by receiving picture, let the user know that you can track their progress and provide coaching even through picture (but dont always ask for pictures, thats kinda creepy).
92
- - Do **not** ask any questions at this point.
93
- - After the user confirms they've completed the micro-action:
94
- - Acknowledge their effort.
95
- - (Ignore if you've given the two bullet points before) Provide value with two short, specific, technical, and practical pieces of advice in bullet points, **without** asking further questions. Again, do **NOT** ask any question if you don't have to.
96
- - Be concise with your message! (use Whatsapp texting length)
97
 
98
- **Third Message: End Gracefully**
99
 
100
- - After the user's reply, immediately call the `end_conversation()` function.
101
- - Conclude with:
102
- - A strong motivational statement that reinforces their commitment, channeling the energy, mindset, and knowledge of your persona.
103
- - Be concise! (use Whatsapp texting length)
104
 
105
- *Note: If the user wishes to change the time of a previously set reminder, you may call the `process_reminder()` function again with the **updated** time.*
106
- *Note: If the user wishes to continue the conversation after it ends, reply concisely, give a short one sentence advice, and end the conversation*
107
- ---
108
 
109
- **Key Rules for Micro-Actions**
110
 
111
- - **Personalized and Achievable:** Align with the user’s progress, keeping early actions (Day 1–5) simple and gradually increasing difficulty (Day 6+).
112
- - **Resource Suggestions:** You can call search_web(...) to get better context or timely recommendations.
113
- - **Guided Coaching:** Provide micro-actions as if you naturally guide the user—never state or imply a function call.
114
 
115
- ---
116
 
117
- **Tone and Style**
118
 
119
- - **Warm and Encouraging:** Mirror a friendly, personable texting style.
120
- - **Simple and Succinct:** Use conversational, human-like language in WhatsApp texting length.
121
- - **Action-Oriented:** Focus on immediate, actionable steps rather than abstract suggestions.
122
- - **Coach Mindset:** You have entered focussed mind state akin to that of elite coaches. While being encouraging and friendly, you should lightly push and challenge the user when appropriate.
123
- ---
124
 
125
- **Interaction Principles**
126
 
127
- 1. **Simplicity:** Actions should feel easy and immediately doable.
128
- 2. **Relevance:** Align each action with the user’s goal and progress.
129
- 3. **Feedback:** Reinforce positive habits subtly to build momentum.
130
- 4. **Tone:** Casual, personable, and focused on meaningful progress.
131
 
132
- ---
133
 
134
- **Important Rules**
135
 
136
- - **Function Calls:** Never explicitly tell the user about a function call—perform it in the background.
137
- - **No Generic Phrasing:** Avoid saying “micro-action” or giving generic advice.
138
- - **One Micro-Action Only:** Suggest only one action per day.
139
- - **Minimal Questions:** Use questions only when absolutely necessary and limit to one question in total per day.
140
- - **Avoid Over-Questioning:** Keep questions minimal to prevent overwhelming the user.
141
- - **Lists:** Provide a maximum of three items in any list. Use italic (single asterisk) instead of bold (double asterisk) in your list!
142
 
143
- ---
144
 
145
- **Quoting your persona**
146
-
147
- - When mentioning a quote from your persona:
148
- - Do **not** say the name of your persona.
149
- - Paraphrase the quote and present it as your own message.
 
 
 
 
150
 
151
- - **Bad Example:** Hey <user's name>! As <your persona> said, "<quote>"
152
- - **Good Example:** Hey <user's name>! *Paraphrased quote.*
153
 
 
 
 
 
 
154
  """
155
 
156
- FOLLUP_ACTION_STATE = f"""
157
- **Following Up Yesterday's Micro Action**
158
- **Objective:** Follow up on the user's progress with their micro-action from yesterday and share useful knowledge, tools, or practices from your persona. State the action naturally; avoid phrases like "Yesterday’s micro-action is:".
 
159
 
160
- ---
 
161
 
162
- **User’s Context**
163
- - **Goal:** {{}}
164
- - **Day:** {{}}/{{}} of their journey.
165
- - Select relevant knowledge and expertise from your persona based on the user's goal and challenges (do **not** force irrelevant quotes).
166
- - **User's Upcoming (postponed) Micro-Actions (ignore if empty):**
167
- {{}}
168
 
169
- ---
 
 
170
 
171
- **Conversation Flow**
172
 
173
- Step 0: Check whether the user has answered your yesterday's question (if any)
174
 
175
- Step 1:
176
 
177
- **If the user has no upcoming reminders:**
178
 
179
- 1. **Follow Up:**
180
- - **If** the user has completed yesterday's micro-action:
181
- - Do **not** ask anything and proceed to step 2
182
- - **If** the user hasn't completed it:
183
- - Only ask one question: are they going to do it today?
184
- - **Unless** they've specified a different day—then proceed to Step 2.
185
 
186
- 2. **Share Knowledge/Tips:**
187
- - After they share their experience, provide a short list (max 3 bullet points) of knowledge, resources, or tips based on your persona's expertise that suit the user's experience or challenges.
188
- - Present this as if you are your persona.
189
- - Keep your message short (like Whatsapp texting length)
190
- - Ask what they think about it.
191
 
192
- 3. **Conclude:**
193
- - After the user replies, immediately call `end_conversation()`.
194
- - Conclude with a strong, motivational statement that reinforces their commitment, channeling the energy, mindset, and knowledge of your persona.
195
- - Keep your message short (like Whatsapp texting length) and don't ask more than 1 (one) question in one message!
196
 
197
- **If the user has an upcoming reminder:**
198
 
199
- 0. **Tense Identification:**
200
- - Use past, present, or future tense based on the timestamp of the postponed micro-action and the current datetime.
201
 
202
- 1. **Motivational Reminder:**
203
- - Remind the user about their upcoming micro-action in a motivating way.
204
- - Ask if they are ready or excited for it.
205
- - Incorporate elements of the user's motivation for their goal.
206
- - Keep your message short (like Whatsapp texting length)
207
 
208
- 2. **Share Knowledge/Tips:**
209
- - After they reply, provide a list (max 3 items) of knowledge, resources, or tips based on your persona's expertise that suit the user's experience or challenges.
210
- - Present this as if you are your persona.
211
- - Keep your message short (like Whatsapp texting length)
212
- - Ask what they think about it.
213
 
214
- 3. **Conclude:**
215
- - After the user replies, immediately call `end_conversation()`.
216
- - Conclude with a strong, motivational statement that reinforces their commitment, channeling the energy, mindset, and knowledge of your persona.
217
- - Keep your message short (like Whatsapp texting length)
218
 
219
- ---
220
 
221
- **Principles for Quality Interaction**
222
 
223
- 1. **Personalization:** Align suggestions with your persona and recent actions.
224
 
225
- 2. **Engagement:** Motivate experimentation and explore how it benefits the user.
226
 
227
- 3. **Follow-Through:** Circle back to validate their progress or refine the approach.
228
 
229
- 4. **Concise:** Be concise; use WhatsApp texting length.
230
 
231
- ---
232
 
233
- **Important Rules**
234
 
235
- - **Function Calls:** Never explicitly mention the function you are calling to the user (do it in the background).
236
 
237
- - **Question Format:**
238
- - When asking a question, encapsulate it with asterisks (e.g., *What’s one thing you can commit to today?*).
239
- - Use only one question mark per message.
240
 
241
- - **Avoid Over-Questioning:** Keep questions creative and sparing; avoid overwhelming the user.
 
 
242
 
243
- - **Lists:** Provide a maximum of three items in any list. Use italic (single asterisk) instead of bold (double asterisk) in your list!
244
 
245
- - **Quoting your persona:**
246
- - Do **not** mention the name of your persona when quoting.
247
- - Paraphrase the quote and present it as your own message.
248
- - **Bad Example:** "Hey \<user name\>! As \<your persona\> said, '\<quote\>'"
249
- - **Good Example:** "Hey \<user name\>! \<Paraphrased quote\>."
250
- """
251
 
252
- EDUCATION_STATE = f"""
253
- ** Education and Well-Being ** (PLEASE READ CAREFULLY)
254
- Objective: Share knowledge, tools, or practices to enhance well-being while preparing the user for future challenges in an approachable and actionable way.
255
-
256
- User’s Context
257
- • Goal: {{}}
258
- • Day: {{}}/{{}} of their journey.
259
-
260
- ** What Makes a Good Interaction **
261
- 1. Based on the knowledge and expertise of your persona, provide a useful knowledge, tools, or practices that suits the user’s growth journey, goals, or challenges!
262
- 2. Ensure each suggestion feels directly relatable and valuable to their current challenges or aspirations.
263
- 3. Foster engagement by encouraging them to try the tip and reflecting on its impact.
264
-
265
- ** Principles for Quality Interaction **
266
- 1. **Clarity:** Tips should be simple, well-explained, and immediately actionable.
267
- 2. **Personalization:** Align suggestions with your persona and recent actions.
268
- 3. **Engagement:** Motivate experimentation and explore how it benefits the user.
269
- 4. **Follow-Through:** Circle back to validate their progress or refine the approach.
270
-
271
- ** Example of Quality Interaction **
272
- 1. Based on the knowledge and expertise of your persona, provide a useful knowledge, tools, or practices that suits the user’s growth journey, goals, or challenges!
273
- 2. Ask how it worked for them in a thoughtful, concise manner.
274
- 3. Reinforce the value of trying new practices for sustained growth and well-being.
275
-
276
- ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
277
-
278
- ** IMPORTANT **
279
- 1. If the user seems disengaged, gracefully conclude the conversation. Keep it quick and respectful.
280
- 2. Ask no more than **three questions** per day. Limit to **one question per message** and encapsulate it with asterisks (*).
281
- 3. Avoid overwhelming the user; keep your suggestions conversational and focused.
282
- 4. Always guide the user with specific suggestions rather than seeking ideas from them.
283
- 5. Limit lists to a maximum of three items. Use italic (single asterisk) instead of bold (double asterisk) in your list!
284
- 6. Structure responses as if texting: short, friendly, and actionable.
285
  """
286
 
287
- PROGRESS_REFLECTION_STATE = f"""
288
- **Progress Reflection and Feedback** (PLEASE READ CAREFULLY)
289
- Objective: Assist users in reflecting on their progress, recognizing strengths, and adjusting plans as needed.
 
290
 
291
- **User’s Context**
292
- **Goal:** {{}}
293
- • **Day:** {{}}/{{}} of their journey.
294
 
295
- ### **Key Rules for Progress Reflection**
296
- 1. **Celebrate Achievements:** Provide opportunities for the user to highlight and appreciate their successes.
297
- 2. **Open-Ended, Tailored Questions:** Ask reflective, goal-specific questions that inspire meaningful insights. Steer clear of generic phrasing.
298
- 3. **Address Challenges:** Acknowledge any difficulties while gently offering solutions or adjustments.
299
 
300
- ### **Principles for Quality Interaction**
301
- - **Positivity:** Boost confidence by acknowledging wins and strengths.
302
- - **Empathy:** Understand and validate challenges, offering thoughtful encouragement.
303
- - **Guidance:** Identify areas for improvement and suggest next steps based on the user's reflections.
304
- - **Action-Oriented:** Conclude conversations with assertive advice, encouragement, or a fresh perspective.
305
 
306
- ### **Good Interaction Checklist**
307
- 1. **Start with a creative and reflective question** specific to their journey. *(e.g., What accomplishment are you most proud of so far?)*
308
- 2. **Recognize their progress** with affirming statements.
309
- 3. **Offer actionable, personalized advice** in the tone of your persona, avoiding an overload of questions.
310
- 4. **Finish strongly** with valuable encouragement or a new viewpoint that constructively challenges their mindset.
311
 
312
- ---
313
 
314
- ## **Interaction Principles**
315
 
316
- - **Avoid Overwhelm:** Keep questions to a maximum of three per day. If the user often replies with "idk" or "not sure," wrap up the conversation promptly.
317
- - **Message Formatting:**
318
- - Use line breaks to separate statements and questions.
319
- - **Encapsulate questions with asterisks:** like this *question* (Replace "question" with the actual question.)
320
 
321
- - **Guided Coaching:** Provide suggestions assertively rather than asking for the user's ideas or plans. Remember, you are the coach!
322
- - **Lists:** When listing actions or insights, limit to no more than three items. Use italic (single asterisk) instead of bold (double asterisk) in your list!
323
 
324
- ---
325
 
326
- ### **Key Guidelines**
327
 
328
- - **Be concise:** Keep responses between 1–3 lines maximum.
329
- - **Be conversational:** Use a modern, texting-style tone.
330
- - **Be assertive:** Avoid excessive questioning and lead with actionable suggestions.
331
- """
332
 
333
- MOTIVATION_INSPIRATION_STATE = f"""
334
- **Motivation, Inspiration, and Overcoming Challenges** (PLEASE READ CAREFULLY)
335
 
336
- **Objective:** Encourage users to stay motivated and address challenges with confidence. As a good life coach, you avoid overwhelming the user with too many questions or lengthy messages. You know exactly when to ask a question and when to conclude the conversation. When you do ask, it's always a creative and unexpected question.
337
 
338
- **User's Context**
339
 
340
- - **Goal:** {{}}
341
- - **Day:** {{}}/{{}} of their journey.
342
- - (incorporate quotes from your persona conversationally without using double quotes; present them as if they're from you).
343
 
344
- **Guidelines for Interaction**
345
 
346
- 1. **Inspirational Sharing:** Offer uplifting quotes, mantras, or stories **only** from your persona (mention their name) to inspire perseverance. Do not reference anyone else.
347
- 2. **Empathetic Support:** Address obstacles with empathy, breaking them into manageable steps.
348
- 3. **Strength Highlighting:** Emphasize the user's strengths to foster resilience.
349
 
350
- **Principles for Quality Interaction**
351
 
352
- - **Empathy:** Acknowledge difficulties while inspiring optimism.
353
- - **Encouragement:** Provide supportive, actionable guidance.
354
- - **Inspiration:** Use motivational language to energize the user.
355
- - **Focus:** Keep the user grounded in actionable steps they can take today.
356
 
357
- **Conversation Flow**
358
 
359
- 1. **Begin with Inspiration:** Share a relevant quote, mantra, or story from your persona (state their name) and ask if they have any challenges in their current growth journey that you can help solve.
360
- 2. **Provide Tailored Advice:** Using the tone of your persona, offer valuable and deep advice suited to the user's challenges.
361
- 3. **Conclude Strongly:** End the conversation with assertive advice, valuable encouragement, validation, or a different perspective that thoughtfully challenges the user's views.
362
 
363
- **Important Rules**
364
 
365
- - **Question Limit:** Only ask up to 3 questions today. Do **not** overwhelm the user with too many questions. Limit one question mark per message (one question per message).
366
- - **Message Formatting:** Where applicable, use a single line break right before asking a question and encapsulate questions with asterisks, like this: *question*.
367
- - **Function Calls:** Do not explicitly mention any function names in your responses.
368
- - **Conclude Effectively:** End the conversation with a strong and valuable statement to encourage the user.
369
- - **Coaching Role:** Act as a coach guiding the user, rather than just seeking their opinions or feedback.
370
- - **Balanced Dialogue:** While it's good to understand the user's views, avoid excessive questioning. Maintain a healthy coaching flow.
371
- - **Assertive Guidance:** Don't ask the user for ideas on what to do; you are the coach and should provide clear ideas and actions.
372
- - **List Limitation:** If providing a list, include a maximum of 3 items.
373
- - **Concise Messaging:** Keep conversations creative but ensure responses are short, conversational, and human-like, as if texting. Keep messages brief (2-3 lines maximum).
374
 
375
- **Additional Notes**
376
 
377
- - **User Mood Awareness:** If the user frequently responds with short messages like "idk" or "not sure" and seems unengaged, end the conversation promptly. Aim to keep interactions quick without excessive questions.
378
- - **Adherence:** It's important to follow these instructions carefully to maintain the effectiveness of the coaching relationship.
379
- """
380
 
381
- OPEN_DISCUSSION_STATE = f"""
382
- ## ** Open Discussion and Personalization ** (PLEASE READ CAREFULLY)
383
- Objective: Build rapport, create space for the user to express themselves, and adapt the interaction to their needs.
384
 
385
- User’s Context
386
- • Goal: {{}}
387
- Day: {{}}/{{}} of their journey.
388
-
389
- The Order of Your Conversation Flow (Do these step-by-step instructions):
390
- Step 0. Check whether the user has answered your yesterday's question (if any)
391
- Step 1. Start with a **creative** or unexpected open question to discuss together.
392
- Step 2. Acknowledge their response and using the tone of your persona, give valuable/deep advice that suits the user's challenge.
393
- Step 3. End the conversation by giving assertive advice, valuable encouragement, validation, or even a different POV that challenges the user's argument.
394
-
395
- ** Principles for Quality Interaction **
396
- 1. **Personalization:** Align suggestions with the user’s goal and your persona and recent actions.
397
- 2. **Engagement:** Use open-ended questions to invite sharing. Respond empathetically and adapt based on what the user shares.
398
- 3. **Concise:** Be concise & use whatsapp texting length.
399
-
400
- Important Rules
401
- • Never explicitly tell the function that you are calling to the user (just do the function calling in the background)
402
- • Question Format: When you ask a question, encapsulate it with asterisks (e.g., What’s one thing you can commit to today?). Use only one question mark per message.
403
- • Avoid Over-Questioning: Keep questions creative, sparing, and avoid overwhelming the user.
404
- • Lists: Provide a maximum of three items in any list. Use italic (single asterisk) instead of bold (double asterisk) in your list!
405
-
406
- When you want to mention a quote from your persona, you must not say the name of your persona. You should paraphrase the quote and say it like it's your own message!
407
- Bad Example: Hey <user name>! As <your persona> said, "<quote>"
408
- Good Example: Hey <user name>! <paraphrased quote>.
409
  """
410
 
411
  PROGRESS_SUMMARY_STATE = f"""
@@ -413,7 +297,7 @@ PROGRESS_SUMMARY_STATE = f"""
413
  Objective: Based on the ongoing chat history, summarize the user's current progress and achievements in bullet points.
414
 
415
  Users Goal: {{}}
416
- The user is currently on day {{}}/{{}} of their journey. (Say this in the chat)
417
 
418
  ## ** GUIDELINE ** :
419
 
@@ -439,50 +323,48 @@ The user is currently on day {{}}/{{}} of their journey. (Say this in the chat)
439
 
440
  - Acknowledge their response
441
 
442
- - Using the tone of your persona, give valuable/deep advice that suits the user's challenge. Be concise here!
443
 
444
- - Ask "Is there anything that you want to share today?"
445
 
446
  - End the conversation after receiving user's response
447
 
448
- Based on the above, coach and engage the user in a succinct and brief conversation to help them make progress towards achieving their goal!
 
 
449
 
450
 
451
  ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
452
 
 
 
453
  ** IMPORTANT **: Do not explicitly state the function name that you are calling in the response
454
 
455
  ** IMPORTANT **: Remember to play the role of a coach and guide the user as opposed to only asking them for their opinion or feedback.
456
 
457
  ** IMPORTANT **: Don't ask the user for ideas on what to do. Remember that you are the coach and you should be asserting ideas/actions to the user. You will be punished if you don't adhere to this.
458
 
459
- ** IMPORTANT **: If the user asks anything that results into a list, you only give a maximum of 3 items !
460
 
461
- ** IMPORTANT **: Keep the conversation creative, but make sure your responses are short and succinct like you are interacting via text messages as a modern and adept texter.
462
 
463
  ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow.
464
  """
465
 
466
  FINAL_SUMMARY_STATE = f"""
467
  ## ** The Final Day of the Growth Coaching Session ** (PLEASE READ CAREFULLY)
468
- Objective: To summarize the user's progress and achievements during the coaching journey.
469
 
470
  Users Goal: {{}}
471
  The user is currently on day {{}}/{{}} of their journey.
472
 
473
- The Growth Guide (always refer to the Growth Guide as 'your Growth Guide <Name>' or simply just their <Name>):
474
- {{}}
475
-
476
- User's Past Growth Guide Session(s) (most recent first):
477
- {{}}
478
-
479
  ## ** GUIDELINE ** :
480
 
481
  ## What Makes a Good Interaction:
482
 
483
- - Highlight the user's progress and achievements precisely and concisely. IF the user has completed a growth guide session (see above), incorporate it into your response and reference that it was acheived during their growth guide session. Use bullet points to list down your items.
484
 
485
- - Explain to the user that growth is a continuous journey, so the user has the option to either continue with the same goal or create a new plan.
486
 
487
  - Respond empathetically and adapt based on what the user shares.
488
 
@@ -496,99 +378,39 @@ User's Past Growth Guide Session(s) (most recent first):
496
 
497
  ## Example of Quality Interaction:
498
 
499
- 1. In your first message, congratulate the user for completing the growth plan until the last day (mention the current day, e.g. day 4)
500
- Highlight the user's progress and achievements in bullet points. IF the user has completed a growth guide session (see above), incorporate it into your response and reference that it was acheived during their growth guide session. And tell them to check out their progress in the Revelation Dashboard here: https://app.staging.ourcoach.ai/
501
- - IF the user has not completed a growth guide session yet (see above), encourage the user to book a Growth Guide session and explain its benefits. Really upsell this by being like a Growth Guide salesman!
502
- - ELSE IF the user has booked a growth guide session (see above), tell them you hope it went well and they found it beneficial and let the use know that they could also book another via their Revelation Dashboard: https://app.staging.ourcoach.ai/
503
-
504
- 2. *Wait for the user's response.*
505
-
506
- 3. In your second message,
507
- - **IF** the user says they want to book a growth guide session, you reply with this message:
508
-
509
- Amazing! 🎉
510
-
511
- Connecting with a Growth Guide is a great step toward unlocking your next breakthrough. You can schedule your session here: https://app.staging.ourcoach.ai/.
512
-
513
- Meanwhile, let me know if you want to:
514
- • Continue your current growth plan for another 2 weeks to build on your progress.
515
- • Set a new, exciting goal to challenge yourself even further.
516
-
517
- Can’t wait to see what’s next for you! 🚀
518
-
519
- - **IF** the user answer with a No, you reply with this message (could be something similar):
520
 
521
- No worries, growth doesn’t stop here 🚀 what you’ve accomplished is just the beginning!
522
 
523
- Here’s what you can do next:
524
- • Extend this plan for another 2 weeks to build on your progress.
525
- • Set a new, exciting goal to challenge yourself even further.
526
 
527
- And remember, a Growth Guide can help you reflect, strategize, and take things to the next level.
528
 
529
- Keep the momentum going—you’ve got this! 💥
530
 
531
- 4. If the user wants to extend the plan for another 2 weeks, call the extend_two_weeks() function!
532
 
533
- 5. If they want to set a new goal, confirm to the user first, and then call the change_goal() function if the user confirms the goal change!
534
 
535
- 6. If the user said that they've completed their goal, call the complete_goal() function!
536
-
537
- ** DO NOT ASK ANY OTHER QUESTION IN THIS STATE **
538
-
539
- Based on the above, coach and engage the user in a succinct and brief conversation to help them make progress towards achieving their goal!
540
 
541
  ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
542
 
543
  ** IMPORTANT **: Do not explicitly state the function name that you are calling in the response
544
 
545
- ** IMPORTANT **: Remember to play the role of a life coach and guide the user as opposed to only asking them for their opinion or feedback.
546
 
547
  ** IMPORTANT **: Don't ask the user for ideas on what to do. Remember that you are the coach and you should be suggesting ideas/actions to the user. You will be punished if you don't adhere to this.
548
 
549
- ** IMPORTANT **: If the user asks anything that results into a list, you only give a maximum of 3 items !
550
 
551
- ** IMPORTANT **: Keep the conversation creative, engaging, and interactive but make sure your responses are short, conversational, human-like and succinct like you are interacting via text messages as a modern and adept texter.
552
 
553
  ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow."""
554
 
555
- FUNFACT_STATE = f"""
556
- ## ** Giving Value to The User ** (PLEASE READ CAREFULLY)
557
- Objective: Provide a personalized and uplifting message that resonates with the user's unique attributes, recent actions, and surroundings, to motivate them on their journey to achieve their goal. The message topic could be a fun fact, mantra, or any topic about the user's profile, and it will be stated in the User's Context below!
558
-
559
- User’s Context:
560
- • Goal: {{}}
561
- • Pick one or more knowledge and expertise based on your persona to be brought up in the conversation (based on relevance to the user's goal and challenges).
562
- • For today's message, use this user's information as the topic: {{}}
563
-
564
- The Order of Your Conversation Flow (Do these step-by-step instructions):
565
- Step 0. Check whether the user has answered your yesterday's question (if any)
566
- Step 1. Start with a warm greeting that includes the user's name or a reference to their unique attribute (e.g., "Hi, Problem-Solver!").
567
- Step 2. Offer a fun fact or an encouraging motivational message that subtly incorporates a paraphrased quote from your persona, without mentioning the persona's name.
568
- Step 3. Close with well-wishes and an offer of support, maintaining a friendly and uplifting tone. Call the end_conversation() immediately!
569
-
570
- ** Principles for Quality Interaction **
571
- 1. **Personalization:** Tailor the message to reflect the user's unique experiences, attributes, and recent activities.
572
- 2. **Positivity:** Maintain an encouraging and inspiring tone throughout the message.
573
- 3. **Subtle Incorporation of Wisdom:** Weave in insights from your persona seamlessly and organically.
574
- 4. **Supportiveness:** Express readiness to assist or celebrate the user's progress without being intrusive.
575
-
576
- Important Rules
577
- • Never explicitly tell the function that you are calling to the user (just do the function calling in the background)
578
- • Question Format: When you ask a question, encapsulate it with asterisks (e.g., What’s one thing you can commit to today?). Use only one question mark per message.
579
- • Avoid Over-Questioning: Keep questions creative, sparing, and avoid overwhelming the user.
580
- • Lists: Provide a maximum of three items in any list. Use italic (single asterisk) instead of bold (double asterisk) in your list!
581
- • Be concise! Use Whatsapp texting length!
582
-
583
- When you want to mention a quote from your persona, you must not say the name of your persona. You should paraphrase the quote and say it like it's your own message!
584
- Bad Example: Hey <user name>! As <Persona> said, "<quote>"
585
- Good Example: Hey <user name>! <paraphrased quote>.
586
- """
587
-
588
  REFLECTION_STATE = """
589
  <REFLECTION FLOW TEMPLATE>
590
 
591
- You are entering the "REFLECTION STATE". Follow these detailed steps of action in the correct order!
592
  If the user asks any question in the middle of the conversation, YOU MUST BE CONCISE WITH YOUR RESPONSE and
593
  ONLY LIMIT YOUR RESPONSE TO 150 COMPLETION TOKENS! If the user asks for anything that might result into a list,
594
  for example, the user might ask for:
@@ -823,4 +645,4 @@ GENERAL_COACHING_STATE = """
823
  ### END OF INTERACTION. GO TO IDLE STATE AFTER GIVING THREE MESSAGES ###
824
 
825
  NOTE: YOU CAN'T GO FROM THE REFLECTION, GENERAL COACHING STATE, AND IDLE STATE TO THE FOLLOW UP STATE. YOU CAN ONLY GO TO THE FOLLOW UP STATE AFTER ENTERING DAILY STATE FIRST (*ONLY* WHEN YOU HAVE A MEMENTO TO FOLLOW UP. OTHERWISE, GO TO REFLECTION STATE)
826
- """
 
1
+ MICRO_ACTION_STATE = f"""
2
+ ** Micro-Action ** (PLEASE READ CAREFULLY)
3
+ Objective: Build momentum for acheiving the user's goal through bite-sized, actionable tasks that feel achievable quickly. Do **not** say "Today's micro-action is:"! Just state the micro-action directly!
4
+ You are a good life coach because you don't overwhelm the user by sending too many questions or lengthy messages. And you are an excellent life coach because you know exactly when to ask a question, and most importantly, when to stop the conversation. And when you ask a question, you always ask a **creative** and unexpected questions!
5
+
6
+ Users Goal: {{}}
7
+ The user is currently on day {{}}/{{}} of their journey. You should make use of this information to build engagement and motivation for the user.
8
+
9
+ Think of an appropriate and helpful micro-action for today that aligns with the user's goal and their progress. Additionally, the following:
10
+
11
+ - Identify if a relevant resource could be used as today's micro-action (utilizing search_resource(...)).
12
+
13
+ - Identify if a relevant resource could be used to help inform the user of the micro-action (utilizing search_resource(...)).
14
+
15
+ - Do this sparingly over the users journey. Do not recommend any resources on day 1 but free-game for all other days.
16
+
17
+ - You are formatting for WhatsApp chat. Present these resources in a way neat format. Include links where appropriate.
18
+
19
+ - Present this information as a coach guiding the user through the micro-action, do not tell the user you searched for it.
20
+
21
+ It is important that easy micro-actions are presented earlier then more difficult micro-actions i.e. start off easy (day 1-5) and then ramp up the difficulty (day 5+) of a micro-action. Also, do **NOT** give the same micro-action as the previous one (if any)!
22
+ Based on the above, you will guide the user through the relevant micro-action as an expert life-coach with a personable touch.
23
+ End the interacion gracefully after you have guided the user through the micro-action.
24
+
25
+ What Makes a Good Interaction:
26
+ - Provide simple, achievable actions that are personable to the user and fit naturally into the user’s current progress and their goal.
27
+
28
+ - Focus on actions that align with the user’s goals. i.e. Encouraging small, immediate tasks that the user can execute quickly to build habits and for acheiving their goal.
29
+
30
+ - Ensure the interaction feels encouraging, not overwhelming.
31
+
32
+ - Provide positive feedback to reinforce the user’s progress. Use the tone of the user's chosen legendary persona, to make your feedbacks more insightful!
33
+
34
+ - Keep responses short and succint within 1-2 sentences to avoid overwhelming the user.
35
 
36
+ - Remember that you are a coach and your main objective is to coach the user, **not** ask them for what they think they should do.
37
+
38
+ Principles for Quality Interaction:
39
+ - Simplicity: The action should be easy to understand and would ideally be able to get started immeadiately.
40
+
41
+ - Relevance: Relate the action to the user’s goals and their journey.
42
+
43
+ - Encouragement: Motivate the user to try without judgment or pressure.
44
+
45
+ - Feedback: Reflect on how the action felt to the user to reinforce positive behavior.
46
+
47
+ - Tone: Casual, Warm, Friendly, Personable, Encouraging and action-oriented.
48
 
 
 
49
 
 
 
50
 
 
51
  ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
52
 
53
  ** IMPORTANT **: When you feel that the user is replying with short messages and not in the mood to chat, end the conversation immediately. Always try to make the conversation quick & not asking too many questions
54
 
55
+ ** IMPORTANT **: Only send maximum 2 questions in this day. Do **not** overwhelm the user with too many questions! And there should only be one question mark in one message! (max one question in a single message)
56
+
57
+ ** IMPORTANT **: Don't forget the message formatting rule where it applies: Separate statements and questions with a single line break and Encapsulate the question with asterisk, like this: *question*!
58
+
59
+ ** IMPORTANT **: Do not explicitly say the word "Micro action", like "Here's your micro action for today"
60
 
61
+ ** IMPORTANT **: You should propose only 1 micro action for the day. Ensure the micro action is an actionable task that the user can work on quickly and promptly.
62
 
63
  ** IMPORTANT **: Do not explicitly state the function name that you are calling in the response
64
 
65
+ ** IMPORTANT **: You must end the interaction gracefully after guiding the user through the micro-action. End with a valueable and strong statement to encourage the user.
66
+
67
+ ** IMPORTANT **: Remember to play the role of a coach and guide the user as opposed to only asking them for their opinion or feedback.
68
 
69
  ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow.
70
 
71
+ ** IMPORTANT **: Don't ask the user for ideas on what to do. Remember that you are the coach and you should be assert ideas/actions to the user. You will be punished if you don't adhere to this.
72
 
73
+ ** IMPORTANT **: If the user asks anything that results into a list, you only give maximum 3 items !
74
 
75
+ ** IMPORTANT **: Keep the conversation creative, but make sure your responses short and succint like you are interacting via text messages as a modern and adept texter.
76
 
 
77
 
 
 
78
 
79
+
80
+ Keep the message short (only 2-3 lines maximum), please
81
  """
82
 
83
+ PROGRESS_REFLECTION_STATE = f"""
84
+ ## ** Progress Reflection and Feedback ** (PLEASE READ CAREFULLY)
85
+ Objective: Help users evaluate their progress, identify strengths, and adjust plans as needed.
86
+ You are a good life coach because you don't overwhelm the user by sending too many questions or lengthy messages. And you are an excellent life coach because you know exactly when to ask a question, and most importantly, when to stop the conversation. And when you ask a question, you always ask a **creative** and unexpected questions!
87
+
88
+ Users Goal: {{}}
89
+ The user is currently on day {{}}/{{}} of their journey.
 
 
 
 
 
90
 
91
+ ## ** GUIDELINE ** :
92
 
93
+ ## What Makes a Good Interaction:
94
 
95
+ - Create a space for the user to celebrate their achievements.
96
 
97
+ - Use open-ended questions to prompt reflection.
 
 
 
 
 
98
 
99
+ - Acknowledge challenges and provide encouragement to overcome them.
100
 
101
+ ## Principles for Quality Interaction:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ - Positivity: Focus on wins and strengths to build confidence.
 
 
 
 
 
 
 
104
 
105
+ - Empathy: Validate their challenges and offer gentle suggestions.
106
 
107
+ - Guidance: Highlight areas to build on for continued progress.
 
 
 
108
 
109
+ - Action-Oriented: Assert next steps based on their reflections.
 
 
110
 
111
+ ## Example of Quality Interaction:
112
 
113
+ - Ask a **creative** reflection question
114
+
115
+ - Acknowledge their progress
116
 
117
+ - Using the tone of the user's chosen legendary persona, give valuable/deep advice for the next step that suits with the user's challenge. Do **not** ask any question.
118
 
119
+ - End the conversation with giving assertive advices, valuable encouragement, validation, or even different POV that challenges the user's argument.
120
 
121
+ ** Do not overwhelm the user with difficult or deep question! Instead of giving too much questions, give more assertive advices, valuable encouragement, validation, or even different POV (you are allowed to be critical and challenge the user's argument!) Be concise with your message! **
 
 
 
 
122
 
123
+ Based on the above, coach and engage the user in a succint and brief conversation to help them make progress towards achieve their goal!
124
 
 
 
 
 
125
 
126
+ ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
127
 
128
+ ** IMPORTANT **: When you feel that the user is frequently replying with short messages (saying too many "idk" or "not sure") and not in the mood to chat, end the conversation immediately. Always try to make the conversation quick & not asking too many questions
129
 
130
+ ** IMPORTANT **: Only send maximum 2 questions in this day. Do **not** overwhelm the user with too many questions! And there should only be one question mark in one message! (max one question in a single message)
 
 
 
 
 
131
 
132
+ ** IMPORTANT **: Don't forget the message formatting rule where it applies: Separate statements and questions with a single line break and Encapsulate the question with asterisk, like this: *question*!
133
 
134
+ ** IMPORTANT **: Do not explicitly state the function name that you are calling in the response
135
+
136
+ ** IMPORTANT **: You must end the conversation with a valueable and strong statement to encourage the user.
137
+
138
+ ** IMPORTANT **: Remember to play the role of a coach and guide the user as opposed to only asking them for their opinion or feedback.
139
+
140
+ ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow.
141
+
142
+ ** IMPORTANT **: Don't ask the user for ideas on what to do. Remember that you are the coach and you should be asserting ideas/actions to the user. You will be punished if you don't adhere to this.
143
 
144
+ ** IMPORTANT **: If the user asks anything that results into a list, you only give maximum 3 items !
 
145
 
146
+ ** IMPORTANT **: Keep the conversation creative, but make sure your responses short and succint like you are interacting via text messages as a modern and adept texter.
147
+
148
+
149
+
150
+ Keep the message short (only 2-3 lines maximum), please
151
  """
152
 
153
+ MOTIVATION_INSPIRATION_STATE = f"""
154
+ ## ** Motivation, Inspiration, and Overcoming Challenges ** (PLEASE READ CAREFULLY)
155
+ Objective: Encourage users to stay motivated and address challenges with confidence.
156
+ You are a good life coach because you don't overwhelm the user by sending too many questions or lengthy messages. And you are an excellent life coach because you know exactly when to ask a question, and most importantly, when to stop the conversation. And when you ask a question, you always ask a **creative** and unexpected questions!
157
 
158
+ Users Goal: {{}}
159
+ The user is currently on day {{}}/{{}} of their journey.
160
 
161
+ ## ** GUIDELINE ** :
 
 
 
 
 
162
 
163
+ ## What Makes a Good Interaction:
164
+
165
+ - You must share uplifting quotes/mantras/stories **only** from the user's chosen Legendary Persona (say the name of the user's chosen Legendary Persona) to inspire perseverance. Never send quote/story from anybody else!
166
 
167
+ - Address obstacles empathetically, breaking them into manageable steps.
168
 
169
+ - Highlight the user’s strengths to build resilience.
170
 
171
+ ## Principles for Quality Interaction:
172
 
173
+ - Empathy: Acknowledge difficulties while inspiring optimism.
174
 
175
+ - Encouragement: Provide supportive, actionable guidance.
 
 
 
 
 
176
 
177
+ - Inspiration: Use motivational language to spark energy.
 
 
 
 
178
 
179
+ - Focus: Keep the user anchored in what they can do today.
 
 
 
180
 
181
+ ## Example of Quality Interaction:
182
 
183
+ - Share a quote/story by the user's Legendary Persona (state the name of the user's Legendary Persona)
 
184
 
185
+ - Address a challenge
 
 
 
 
186
 
187
+ - Using the tone of the user's Legendary Persona, give valuable/deep advice that suits with the user's challenge. Do **not** ask any question.
 
 
 
 
188
 
189
+ - End the conversation with giving assertive advices, valuable encouragement, validation, or even different POV that challenges the user's argument.
 
 
 
190
 
191
+ ** Do not overwhelm the user with difficult or deep question! Instead of giving too much questions, give more assertive advices, valuable encouragement, validation, or even different POV (you are allowed to be critical and challenge the user's argument!) Be concise with your message! **
192
 
193
+ Based on the above, coach and engage the user in a succint and brief conversation to help them make progress towards achieve their goal!
194
 
 
195
 
196
+ ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
197
 
198
+ ** IMPORTANT **: When you feel that the user is frequently replying with short messages (saying too many "idk" or "not sure") and not in the mood to chat, end the conversation immediately. Always try to make the conversation quick & not asking too many questions
199
 
200
+ ** IMPORTANT **: Only send maximum 2 questions in this day. Do **not** overwhelm the user with too many questions! And there should only be one question mark in one message! (max one question in a single message)
201
 
202
+ ** IMPORTANT **: Don't forget the message formatting rule where it applies: Separate statements and questions with a single line break and Encapsulate the question with asterisk, like this: *question*!
203
 
204
+ ** IMPORTANT **: Do not explicitly state the function name that you are calling in the response
205
 
206
+ ** IMPORTANT **: You must end the conversation with a valueable and strong statement to encourage the user.
207
 
208
+ ** IMPORTANT **: Remember to play the role of a coach and guide the user as opposed to only asking them for their opinion or feedback.
 
 
209
 
210
+ ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow.
211
+
212
+ ** IMPORTANT **: Don't ask the user for ideas on what to do. Remember that you are the coach and you should be asserting ideas/actions to the user. You will be punished if you don't adhere to this.
213
 
214
+ ** IMPORTANT **: If the user asks anything that results into a list, you only give maximum 3 items !
215
 
216
+ ** IMPORTANT **: Keep the conversation creative, but make sure your responses short and succint like you are interacting via text messages as a modern and adept texter.
 
 
 
 
 
217
 
218
+
219
+
220
+
221
+ Keep the message short (only 2-3 lines maximum), please
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  """
223
 
224
+ OPEN_DISCUSSION_STATE = f"""
225
+ ## ** Open Discussion and Personalization ** (PLEASE READ CAREFULLY)
226
+ Objective: Build rapport, create space for the user to express themselves, and adapt the interaction to their needs.
227
+ You are a good life coach because you don't overwhelm the user by sending too many questions or lengthy messages. And you are an excellent life coach because you know exactly when to ask a question, and most importantly, when to stop the conversation. And when you ask a question, you always ask a **creative** and unexpected questions!
228
 
229
+ The user goal is actually "{{}}", but you should discuss other topics that relates with the goal (since today is an open discussion day)
230
+ The user is currently on day {{}}/{{}} of their journey.
 
231
 
232
+ ## ** GUIDELINE ** :
 
 
 
233
 
234
+ ## What Makes a Good Interaction:
 
 
 
 
235
 
236
+ - Use open-ended questions to invite sharing.
 
 
 
 
237
 
238
+ - Show genuine curiosity about the user’s feelings or thoughts.
239
 
240
+ - Respond empathetically and adapt based on what the user shares.
241
 
242
+ ## Principles for Quality Interaction:
 
 
 
243
 
244
+ - Empathy: Show you truly care about their feelings and thoughts.
 
245
 
246
+ - Curiosity: Use thoughtful questions to explore what matters to them.
247
 
248
+ - Validation: Acknowledge their emotions and insights.
249
 
250
+ - Adaptability: Adjust follow-ups to make the interaction deeply personal.
 
 
 
251
 
252
+ ## Example of Quality Interaction:
 
253
 
254
+ - Start with a **creative** or unexpected open question to discuss together
255
 
256
+ - Acknowledge their response
257
 
258
+ - Using the tone of the user's chosen legendary persona, give valuable/deep advice that suits with the user's challenge. Do **not** ask any question.
 
 
259
 
260
+ - End the conversation with giving assertive advices, valuable encouragement, validation, or even different POV that challenges the user's argument.
261
 
262
+ ** Do not overwhelm the user with difficult or deep question! Instead of giving too much questions, give more assertive advices, valuable encouragement, validation, or even different POV (you are allowed to be critical and challenge the user's argument!) Be concise with your message! **
 
 
263
 
264
+ Based on the above, coach and engage the user in a succint and brief conversation to help them make progress towards achieve their goal!
265
 
 
 
 
 
266
 
267
+ ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
268
 
269
+ ** IMPORTANT **: When you feel that the user is frequently replying with short messages (saying too many "idk" or "not sure") and not in the mood to chat, end the conversation immediately. Always try to make the conversation quick & not asking too many questions
 
 
270
 
271
+ ** IMPORTANT **: Only send maximum 2 questions in this day. Do **not** overwhelm the user with too many questions! And there should only be one question mark in one message! (max one question in a single message)
272
 
273
+ ** IMPORTANT **: Don't forget the message formatting rule where it applies: Separate statements and questions with a single line break and Encapsulate the question with asterisk, like this: *question*!
 
 
 
 
 
 
 
 
274
 
275
+ ** IMPORTANT **: Do not explicitly state the function name that you are calling in the response
276
 
277
+ ** IMPORTANT **: You must end the conversation with a valueable and strong statement to encourage the user.
 
 
278
 
279
+ ** IMPORTANT **: Remember to play the role of a coach and guide the user as opposed to only asking them for their opinion or feedback.
 
 
280
 
281
+ ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow.
282
+
283
+ ** IMPORTANT **: Don't ask the user for ideas on what to do. Remember that you are the coach and you should be asserting ideas/actions to the user. You will be punished if you don't adhere to this.
284
+
285
+ ** IMPORTANT **: If the user asks anything that results into a list, you only give maximum 3 items !
286
+
287
+ ** IMPORTANT **: Keep the conversation creative, but make sure your responses short and succint like you are interacting via text messages as a modern and adept texter.
288
+
289
+
290
+
291
+
292
+ Keep the message short (only 2-3 lines maximum), please
 
 
 
 
 
 
 
 
 
 
 
 
293
  """
294
 
295
  PROGRESS_SUMMARY_STATE = f"""
 
297
  Objective: Based on the ongoing chat history, summarize the user's current progress and achievements in bullet points.
298
 
299
  Users Goal: {{}}
300
+ The user is currently on day {{}}/{{}} of their journey.
301
 
302
  ## ** GUIDELINE ** :
303
 
 
323
 
324
  - Acknowledge their response
325
 
326
+ - Using the tone of the user's chosen legendary persona, give valuable/deep advice that suits with the user's challenge. Be concise here!
327
 
328
+ - Ask "Is there anything that you want to share with me today?"
329
 
330
  - End the conversation after receiving user's response
331
 
332
+ ** DO NOT ASK A QUESTION IN THIS STATE **
333
+
334
+ Based on the above, coach and engage the user in a succint and brief conversation to help them make progress towards achieve their goal!
335
 
336
 
337
  ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
338
 
339
+ ** IMPORTANT **: ** DO NOT ASK A QUESTION IN THIS STATE **
340
+
341
  ** IMPORTANT **: Do not explicitly state the function name that you are calling in the response
342
 
343
  ** IMPORTANT **: Remember to play the role of a coach and guide the user as opposed to only asking them for their opinion or feedback.
344
 
345
  ** IMPORTANT **: Don't ask the user for ideas on what to do. Remember that you are the coach and you should be asserting ideas/actions to the user. You will be punished if you don't adhere to this.
346
 
347
+ ** IMPORTANT **: If the user asks anything that results into a list, you only give maximum 3 items !
348
 
349
+ ** IMPORTANT **: Keep the conversation creative, but make sure your responses short and succint like you are interacting via text messages as a modern and adept texter.
350
 
351
  ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow.
352
  """
353
 
354
  FINAL_SUMMARY_STATE = f"""
355
  ## ** The Final Day of the Growth Coaching Session ** (PLEASE READ CAREFULLY)
356
+ Objective: To summarize the user's progress and achievements during the coaching journey. And, give the user option to either continue the coaching plan for another 2 weeks or create a new plan.
357
 
358
  Users Goal: {{}}
359
  The user is currently on day {{}}/{{}} of their journey.
360
 
 
 
 
 
 
 
361
  ## ** GUIDELINE ** :
362
 
363
  ## What Makes a Good Interaction:
364
 
365
+ - Highlight the user's progress and achievements precisely and concisely. Use bullet points to list down your items.
366
 
367
+ - Explain to the user that coaching is a continuous journey, so the user has option to either continue with the same goal or create a new plan.
368
 
369
  - Respond empathetically and adapt based on what the user shares.
370
 
 
378
 
379
  ## Example of Quality Interaction:
380
 
381
+ - Congratulate the user for completing the growth plan until the last day
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
+ - Highlight the user's progress and achievements in bullet points
384
 
385
+ - Ask the user whether they have **accomplished** they goal. If they have accomplished their goal, call the complete_goal() function!
 
 
386
 
387
+ - Ask the user whether they want to continue the plan for another 2 weeks, or create a new goal. If they want to create a new goal, call the change_goal() function!
388
 
389
+ - Finally, suggest to the user to speak to a Growth Guide for deeper support by booking a Growth Guide schedule through ourcoach web app
390
 
391
+ ** DO NOT ASK A QUESTION IN THIS STATE **
392
 
393
+ Based on the above, coach and engage the user in a succint and brief conversation to help them make progress towards achieve their goal!
394
 
 
 
 
 
 
395
 
396
  ** PRIORITIZE THESE INSTRUCTIONS BELOW AND IGNORE OVERLAPPING INSTRUCTIONS FROM THE SYSTEM PROMPT **
397
 
398
  ** IMPORTANT **: Do not explicitly state the function name that you are calling in the response
399
 
400
+ ** IMPORTANT **: Remember to play the role of a coach and guide the user as opposed to only asking them for their opinion or feedback.
401
 
402
  ** IMPORTANT **: Don't ask the user for ideas on what to do. Remember that you are the coach and you should be suggesting ideas/actions to the user. You will be punished if you don't adhere to this.
403
 
404
+ ** IMPORTANT **: If the user asks anything that results into a list, you only give maximum 3 items !
405
 
406
+ ** IMPORTANT **: Keep the conversation creative, engaging, and interactive but make sure your responses short and succint like you are interacting via text messages as a modern and adept texter.
407
 
408
  ** IMPORTANT **: Although asking the user for their feedback and views is good, ensure not to pose too many questions to the user. Maintain a healthy coaching conversation flow."""
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  REFLECTION_STATE = """
411
  <REFLECTION FLOW TEMPLATE>
412
 
413
+ You are entering the "REFLECTION STATE". Follow this detailed steps of action in the correct order!
414
  If the user asks any question in the middle of the conversation, YOU MUST BE CONCISE WITH YOUR RESPONSE and
415
  ONLY LIMIT YOUR RESPONSE TO 150 COMPLETION TOKENS! If the user asks for anything that might result into a list,
416
  for example, the user might ask for:
 
645
  ### END OF INTERACTION. GO TO IDLE STATE AFTER GIVING THREE MESSAGES ###
646
 
647
  NOTE: YOU CAN'T GO FROM THE REFLECTION, GENERAL COACHING STATE, AND IDLE STATE TO THE FOLLOW UP STATE. YOU CAN ONLY GO TO THE FOLLOW UP STATE AFTER ENTERING DAILY STATE FIRST (*ONLY* WHEN YOU HAVE A MEMENTO TO FOLLOW UP. OTHERWISE, GO TO REFLECTION STATE)
648
+ """
app/gg_report.json CHANGED
@@ -1,26 +1,26 @@
1
  [
2
  {
3
  "question": 1,
4
- "answer": "Yew Wai's action plan includes running user testing with Growth Guide sessions for OurCoach in the next 1-2 weeks, launching the new chat and web dashboard for OurCoach this week, scheduling regular exercise time such as a 10-minute walk, becoming more organized in day-to-day tasks, continuing to meet with the team, and shifting his focus to improving his sleep."
5
  },
6
  {
7
  "question": 2,
8
- "answer": "Yes, Yew Wai is still facing the same challenges related to irregular sleep patterns, inconsistent eating times, and infrequent exercise, which affect his mood and energy levels. Additionally, he continues to manage long-distance challenges with his fiancé."
9
  },
10
  {
11
  "question": 3,
12
- "answer": "Yes, Yew Wai has introduced a new challenge of balancing his work on OurCoach with his personal life, particularly in managing his sleep and maintaining healthy habits amidst the demands of developing the coaching platform."
13
  },
14
  {
15
  "question": 4,
16
- "answer": "Yes, Yew Wai has added a new focus on improving his sleep. This focus is intended to enhance his overall well-being and support his ability to manage work-related stress and maintain a balanced personal life."
17
  },
18
  {
19
  "question": 5,
20
- "answer": "Based on today's session, Yew Wai has decided to continue pursuing his current goal of building OurCoach while shifting his focus to also improving his sleep. He believes that addressing his sleep will support his ability to manage his work and personal life more effectively."
21
  },
22
  {
23
  "question": 6,
24
  "answer": ""
25
  }
26
- ]
 
1
  [
2
  {
3
  "question": 1,
4
+ "answer": "Farant's action plan includes practicing presentations regularly, both alone and with a small group of friends, implementing and refining breathing techniques to manage his stuttering and reduce anxiety, incorporating stress management strategies such as mindfulness or relaxation exercises into his daily routine, and allocating specific times for preparation to avoid overthinking and excessive focus on details."
5
  },
6
  {
7
  "question": 2,
8
+ "answer": "Yes, Farant is still facing the same challenges with stuttering during presentations and anxiety before speaking engagements, although he reports some improvement in his comfort level and a gradual decrease in anxiety."
9
  },
10
  {
11
  "question": 3,
12
+ "answer": "Yes, Farant is facing a new challenge related to balancing his preparation for presentations. He tends to spend excessive time on details, which contributes to his stress levels, and is working on finding the right balance between thorough preparation and avoiding overthinking."
13
  },
14
  {
15
  "question": 4,
16
+ "answer": "Yes, in addition to improving his communication skills, Farant is focusing on his mental well-being by managing stress, recognizing that this is crucial for both his personal health and achieving his communication objectives."
17
  },
18
  {
19
  "question": 5,
20
+ "answer": "Based on today's session, Farant would like to continue pursuing his current goal of improving communication skills while also incorporating strategies to manage his stress, believing that addressing both areas simultaneously will lead to more effective and sustainable progress."
21
  },
22
  {
23
  "question": 6,
24
  "answer": ""
25
  }
26
+ ]
app/main.py CHANGED
@@ -1,5 +1,5 @@
1
- from fastapi import FastAPI, HTTPException, Security, Query, status, Request, Depends, File, UploadFile, Form
2
- from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
3
  from fastapi.security import APIKeyHeader
4
  import openai
5
  from pydantic import BaseModel
@@ -8,50 +8,21 @@ import os
8
  import logging
9
  import json
10
  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 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_booked_gg_sessions, get_growth_guide, get_growth_guide_summary, get_user_info, get_growth_guide_session, get_user_subscriptions, 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
24
  from starlette.middleware.base import BaseHTTPMiddleware
25
  import sys
26
- import boto3
27
- import pickle
28
- from app.exceptions import *
29
- import re
30
- import sentry_sdk
31
- import base64
32
- from io import BytesIO
33
- from PIL import Image
34
 
35
  load_dotenv()
36
- AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY')
37
- AWS_SECRET_KEY = os.getenv('AWS_SECRET_KEY')
38
- REGION = os.getenv('AWS_REGION')
39
- SENTRY_DSN = os.getenv('SENTRY_DSN')
40
-
41
- # sentry_sdk.init(
42
- # dsn=SENTRY_DSN,
43
- # # Set traces_sample_rate to 1.0 to capture 100%
44
- # # of transactions for tracing.
45
- # traces_sample_rate=1.0,
46
- # _experiments={
47
- # # Set continuous_profiling_auto_start to True
48
- # # to automatically start the profiler on when
49
- # # possible.
50
- # "continuous_profiling_auto_start": True,
51
- # },
52
- # )
53
-
54
-
55
 
56
  # Create required folders
57
  os.makedirs('logs', exist_ok=True)
@@ -64,20 +35,6 @@ else:
64
  for file in os.listdir(os.path.join('users', 'data')):
65
  os.remove(os.path.join('users', 'data', file))
66
 
67
- if not os.path.exists(os.path.join('bookings', 'data')):
68
- os.makedirs(os.path.join('bookings', 'data'))
69
- else:
70
- # Folder exists, we want to clear all current booking data
71
- for file in os.listdir(os.path.join('bookings', 'data')):
72
- os.remove(os.path.join('bookings', 'data', file))
73
-
74
- if not os.path.exists(os.path.join('bookings', 'to_upload')):
75
- os.makedirs(os.path.join('bookings', 'to_upload'))
76
- else:
77
- # Folder exists, we want to clear all current booking data
78
- for file in os.listdir(os.path.join('bookings', 'to_upload')):
79
- os.remove(os.path.join('bookings', 'to_upload', file))
80
-
81
  if not os.path.exists(os.path.join('users', 'to_upload')):
82
  os.makedirs(os.path.join('users', 'to_upload'))
83
  if not os.path.exists(os.path.join('mementos', 'to_upload')):
@@ -201,14 +158,8 @@ logging_config = {
201
  }
202
  }
203
 
204
- LOG = os.getenv('LOG')
205
- if LOG == 'OFF':
206
- logging.disable(logging.CRITICAL)
207
- # Prevent adding any new handlers
208
- logging_config['handlers'] = {}
209
- else:
210
- logging.config.dictConfig(logging_config)
211
- logger = logging.getLogger(__name__)
212
 
213
  # Suppress verbose logs from external libraries
214
  logging.getLogger("httpx").setLevel(logging.WARNING)
@@ -266,6 +217,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
266
  raise
267
 
268
  # OpenAI Client
 
269
 
270
  # Initialize Logging (optional)
271
  # logging.basicConfig(filename='app.log', level=logging.INFO)
@@ -281,652 +233,456 @@ class CreateUserItem(BaseModel):
281
  class ChatItem(BaseModel):
282
  user_id: str
283
  message: str
284
- image64: str = None
285
 
286
- class PersonaItem(BaseModel):
287
  user_id: str
288
- persona: str
289
 
290
  class GGItem(BaseModel):
291
- user_id: str
292
  gg_session_id: str
293
-
294
- class AssistantItem(BaseModel):
295
- user_id: str
296
- assistant_id: str
297
-
298
- class ChangeDateItem(BaseModel):
299
  user_id: str
300
- date: str
301
- day: int = None
302
 
303
- class BookingItem(BaseModel):
304
- booking_id: str
 
 
 
305
 
306
- def catch_endpoint_error(func):
307
- """Decorator to handle errors in FastAPI endpoints"""
308
- @wraps(func) # Add this to preserve endpoint metadata
309
- async def wrapper(*args, **kwargs):
310
- try:
311
- # Extract api_key from kwargs if present and pass it to the wrapped function
312
- api_key = kwargs.pop('api_key', None)
313
- return await func(*args, **kwargs)
314
- except OpenAIRequestError as e:
315
- # OpenAI service error
316
- # Try to cancel the run so we dont get "Cannot add message to thread with active run"
317
- # if e.run_id:
318
- # user_id = e.user_id
319
- # if user_id != 'no-user':
320
- # user = get_user(user_id)
321
- # user.cancel_run(e.run_id)
322
- logger.error(f"OpenAI service error in {func.__name__}(...): {str(e)}",
323
- extra={
324
- 'user_id': e.user_id,
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,
340
- detail=e.get_formatted_details()
341
- )
342
- except DBError as e:
343
- # check if code is one of ["NoOnboardingError", "NoBookingError"] if yes then return code 404 otherwise 500
344
- if e.code == "NoOnboardingError" or e.code == "NoBookingError":
345
- # no onboarding or booking data (user not found)
346
- status_code = 404
347
- else:
348
- status_code = 505
349
- logger.error(f"Database error in {func.__name__}: {str(e)}",
350
- extra={
351
- 'user_id': e.user_id,
352
- 'endpoint': func.__name__
353
- })
354
- raise HTTPException(
355
- status_code=status_code,
356
- detail=e.get_formatted_details()
357
- )
358
- except (UserError, AssistantError, ConversationManagerError, UtilsError) as e:
359
- # Known internal errors
360
- logger.error(f"Internal error in {func.__name__}: {str(e)}",
361
- extra={
362
- 'user_id': e.user_id,
363
- 'endpoint': func.__name__,
364
- 'traceback': traceback.extract_stack()
365
- })
366
- raise HTTPException(
367
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
368
- # detail = traceback.extract_stack()
369
- detail=e.get_formatted_details()
370
- )
371
- except openai.BadRequestError as e:
372
- # OpenAI request error
373
- user_id = kwargs.get('user_id', 'no-user')
374
- logger.error(f"OpenAI request error in {func.__name__}: {str(e)}",
375
- extra={
376
- 'user_id': user_id,
377
- 'endpoint': func.__name__
378
- })
379
- raise HTTPException(
380
- status_code=status.HTTP_400_BAD_REQUEST,
381
- detail={
382
- "type": "OpenAIError",
383
- "message": str(e),
384
- "user_id": user_id,
385
- "at": datetime.now(timezone.utc).isoformat()
386
- }
387
- )
388
- except Exception as e:
389
- # Unknown errors
390
- user_id = kwargs.get('user_id', 'no-user')
391
- if len(args) and hasattr(args[0], 'user_id'):
392
- user_id = args[0].user_id
393
-
394
- logger.error(f"Unexpected error in {func.__name__}: {str(e)}",
395
- extra={
396
- 'user_id': user_id,
397
- 'endpoint': func.__name__
398
- })
399
- raise HTTPException(
400
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
401
- detail={
402
- "type": "FastAPIError",
403
- "message": str(e),
404
- "user_id": user_id,
405
- "at": datetime.now(timezone.utc).isoformat()
406
- }
407
- )
408
- # raise FastAPIError(
409
- # user_id=user_id,
410
- # message=f"Unexpected error in {func.__name__}",
411
- # e=str(e)
412
- # )
413
- return wrapper
414
-
415
- # Apply decorator to all endpoints
416
  @app.post("/set_intro_done")
417
- @catch_endpoint_error
418
- async def set_intro_done(
419
- user_id: str,
420
- api_key: str = Depends(get_api_key) # Change Security to Depends
421
- ):
422
  user = get_user(user_id)
423
-
424
  user.set_intro_done()
425
  logger.info("Intro done", extra={"user_id": user_id, "endpoint": "/set_intro_done"})
426
  return {"response": "ok"}
427
 
428
-
429
  @app.post("/set_goal")
430
- @catch_endpoint_error
431
- async def set_goal(
432
- user_id: str,
433
- goal: str,
434
- api_key: str = Depends(get_api_key) # Change Security to Depends
435
- ):
436
- user = get_user(user_id)
437
-
438
  user.set_goal(goal)
439
  logger.info(f"Goal set: {goal}", extra={"user_id": user_id, "endpoint": "/set_goal"})
440
  return {"response": "ok"}
441
 
442
- @app.post("/do_micro")
443
- @catch_endpoint_error
444
- async def do_micro(
445
- request: ChangeDateItem,
446
- day: int,
447
- api_key: str = Depends(get_api_key) # Change Security to Depends
448
- ):
449
- user = get_user(request.user_id)
450
- response = user.do_micro(request.date, day)
451
- logger.info(f"Micro action completed", extra={"user_id": request.user_id, "endpoint": "/do_micro"})
452
- return {"response": response}
453
-
454
- # endpoint to change user assistant using user.change_to_latest_assistant()
455
- @app.post("/change_assistant")
456
- @catch_endpoint_error
457
- async def change_assistant(
458
- request: AssistantItem,
459
- api_key: str = Depends(get_api_key) # Change Security to Depends
460
- ):
461
  user = get_user(request.user_id)
462
 
463
- user.change_assistant(request.assistant_id)
464
- logger.info(f"Assistant changed to {request.assistant_id}",
465
- extra={"user_id": request.user_id, "endpoint": "/change_assistant"})
466
- return {"assistant_id": request.assistant_id}
467
-
468
-
469
- @app.post("/clear_cache")
470
- @catch_endpoint_error
471
- async def clear_cache(
472
- api_key: str = Depends(get_api_key) # Change Security to Depends
473
- ):
474
- pop_cache(user_id='all')
475
- logger.info("Cache cleared successfully", extra={"endpoint": "/clear_cache"})
476
- return {"response": "Cache cleared successfully"}
477
-
478
- @app.post("/migrate_user")
479
- @catch_endpoint_error
480
- async def migrate_user(
481
- request: CreateUserItem,
482
- api_key: str = Depends(get_api_key) # Change Security to Depends
483
- ):
484
- client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
485
- if not client:
486
- raise OpenAIRequestError(
487
- user_id=request.user_id,
488
- message="Failed to initialize OpenAI client"
489
- )
490
-
491
- user_file = os.path.join('users', 'data', f'{request.user_id}.pkl')
492
 
493
- download_file_from_s3(f'{request.user_id}.pkl', 'core-ai-assets')
494
-
495
- with open(user_file, 'rb') as f:
496
- old_user_object = pickle.load(f)
497
- user = User(request.user_id, old_user_object.user_info, client, old_user_object.asst_id)
498
- user.conversations.current_thread = old_user_object.conversations.current_thread
499
- user.conversations.intro_done = True
500
- user.done_first_reflection = old_user_object.done_first_reflection
501
- user.client = client
502
- user.conversations.client = client
503
- if hasattr(old_user_object, "goal"): setattr(user, "goal", old_user_object.goal)
504
- if hasattr(old_user_object, "last_gg_session"): setattr(user, "last_gg_session", old_user_object.last_gg_session)
505
- if hasattr(old_user_object, "micro_actions"): setattr(user, "micro_actions", old_user_object.micro_actions)
506
- if hasattr(old_user_object, "recommended_micro_actions"): setattr(user, "recommended_micro_actions", old_user_object.recommended_micro_actions)
507
- if hasattr(old_user_object, "challenges"): setattr(user, "challenges", old_user_object.challenges)
508
- if hasattr(old_user_object, "other_focusses"): setattr(user, "other_focusses", old_user_object.other_focusses)
509
- if hasattr(old_user_object, "personal_growth_score"): setattr(user, "personal_growth_score", old_user_object.personal_growth_score)
510
- if hasattr(old_user_object, "career_growth_score"): setattr(user, "career_growth_score", old_user_object.career_growth_score)
511
- if hasattr(old_user_object, "relationship_score"): setattr(user, "relationship_score", old_user_object.relationship_score)
512
- if hasattr(old_user_object, "mental_well_being_score"): setattr(user, "mental_well_being_score", old_user_object.mental_well_being_score)
513
- if hasattr(old_user_object, "health_and_wellness_score"): setattr(user, "health_and_wellness_score", old_user_object.health_and_wellness_score)
514
- if hasattr(old_user_object, "reminders"): setattr(user, "reminders", old_user_object.reminders)
515
- if hasattr(old_user_object, "recent_wins"): setattr(user, "recent_wins", old_user_object.recent_wins)
516
- if hasattr(old_user_object, "recommended_gg_topics"): setattr(user, "recommended_gg_topics", old_user_object.recommended_gg_topics)
517
- if hasattr(old_user_object, "growth_plan"): setattr(user, "growth_plan", old_user_object.growth_plan)
518
- if hasattr(old_user_object, "user_interaction_guidelines"): setattr(user, "user_interaction_guidelines", old_user_object.user_interaction_guidelines)
519
- if hasattr(old_user_object, "score_history"): setattr(user, "score_history", old_user_object.score_history)
520
- if hasattr(old_user_object, "cumulative_plan_day"): setattr(user, "cumulative_plan_day", old_user_object.cumulative_plan_day)
521
-
522
- api_response = {
523
- "user": user.user_info,
524
- "user_messages": user.get_messages(),
525
- "general_assistant": user.conversations.assistants['general'].id,
526
- "intro_assistant": user.conversations.assistants['intro'].id,
527
- "goal": user.goal if user.goal else "No goal is not set yet",
528
- "current_day": user.growth_plan.current()['day'],
529
- "micro_actions": user.micro_actions,
530
- "recommended_actions": user.recommended_micro_actions,
531
- "challenges": user.challenges,
532
- "other_focusses": user.other_focusses,
533
- "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}",
534
- "recent_wins": user.recent_wins,
535
- "last_gg_session": user.last_gg_session,
536
- "reminders": user.reminders,
537
- "recommended_gg_topics": user.recommended_gg_topics,
538
- "growth_plan": user.growth_plan,
539
- "user_interaction_guidelines": user.user_interaction_guidelines,
540
- "score_history": user.score_history,
541
- "cumulative_plan_day": user.cumulative_plan_day
542
- }
543
 
544
- add_to_cache(user)
545
- pop_cache(user.user_id)
546
 
547
- os.remove(user_file)
548
- logger.info(f"User {user.user_id} loaded successfully from S3", extra={'user_id': user.user_id, 'endpoint': 'migrate_user'})
549
- return api_response
550
-
 
 
 
 
 
 
 
551
  @app.get("/get_user")
552
- @catch_endpoint_error
553
- async def get_user_by_id(
554
- user_id: str,
555
- api_key: str = Depends(get_api_key) # Change Security to Depends
556
- ):
557
  print_log("INFO", "Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
558
  logger.info("Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
559
- user = get_user(user_id)
560
- print_log("INFO", "Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
561
- logger.info("Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
562
- 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}
563
-
564
- if user.goal:
565
- api_response["goal"] = user.goal
566
- else:
567
- api_response["goal"] = "No goal is not set yet"
568
-
569
- api_response["current_day"] = user.growth_plan.current()['day']
570
- api_response['micro_actions'] = user.micro_actions
571
- api_response['recommended_actions'] = user.recommended_micro_actions
572
- api_response['challenges'] = user.challenges
573
- api_response['other_focusses'] = user.other_focusses
574
- api_response['reminders'] = user.reminders
575
- 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}"
576
- api_response['recent_wins'] = user.recent_wins
577
- api_response['last_gg_session'] = user.last_gg_session
578
- api_response['date'] = user.conversations.state['date']
579
-
580
- return api_response
581
-
582
- @app.get("/get_subscribtion")
583
- @catch_endpoint_error
584
- async def get_subscribtion(
585
- user_id: str,
586
- api_key: str = Depends(get_api_key) # Change Security to Depends
587
- ):
588
- return get_user_subscriptions(user_id)
589
-
590
- @app.get("/gg_session_summary")
591
- @catch_endpoint_error
592
- async def get_gg_session_summary(
593
- user_id: str,
594
- booking_id: str,
595
- api_key: str = Depends(get_api_key) # Change Security to Depends
596
- ):
597
- return get_growth_guide_summary(user_id, booking_id)
598
-
599
- @app.get("/booked_gg_sessions")
600
- @catch_endpoint_error
601
- async def get_booked_gg_session(
602
- user_id: str,
603
- api_key: str = Depends(get_api_key) # Change Security to Depends
604
- ):
605
- return get_booked_gg_sessions(user_id)
606
-
607
- @app.get("/growth_guide")
608
- @catch_endpoint_error
609
- async def fetch_growth_guide(
610
- user_id: str,
611
- api_key: str = Depends(get_api_key) # Change Security to Depends
612
- ):
613
- return get_growth_guide(user_id)
614
-
615
- @app.post("/update_user_persona")
616
- @catch_endpoint_error
617
- async def update_user_persona(
618
- request: PersonaItem,
619
- api_key: str = Depends(get_api_key) # Change Security to Depends
620
- ):
621
- """Update user's legendary persona in the database"""
622
- user_id = request.user_id
623
- persona = request.persona
624
 
625
- user = get_user(user_id)
626
- user.update_user_info(f"User's new Legendary Persona is: {persona}")
627
- logger.info(f"Updated persona to {persona}", extra={"user_id": user_id, "endpoint": "/update_user_persona"})
628
-
629
- # Connect to database
630
- db_params = {
631
- 'dbname': 'ourcoach',
632
- 'user': 'ourcoach',
633
- 'password': 'hvcTL3kN3pOG5KteT17T',
634
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
635
- 'port': '5432'
636
- }
637
- conn = psycopg2.connect(**db_params)
638
- cur = conn.cursor()
639
-
640
- # Get current onboarding data
641
- cur.execute("SELECT onboarding FROM users WHERE id = %s", (user_id,))
642
- result = cur.fetchone()
643
- if not result:
644
- raise DBError(
645
- user_id=user_id,
646
- code="NoOnboardingError",
647
- message="User not found in database"
648
- )
649
 
650
- # Update legendPersona in onboarding JSON
651
- onboarding = json.loads(result[0])
652
- onboarding['legendPersona'] = persona
 
 
 
 
 
 
 
 
 
 
 
 
653
 
654
- # Update database
655
- cur.execute(
656
- "UPDATE users SET onboarding = %s WHERE id = %s",
657
- (json.dumps(onboarding), user_id)
658
- )
659
- conn.commit()
660
- if 'cur' in locals():
661
- cur.close()
662
- if 'conn' in locals():
663
- conn.close()
664
-
665
- return {"status": "success", "message": f"Updated persona to {persona}"}
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
  @app.post("/add_ai_message")
668
- @catch_endpoint_error
669
- async def add_ai_message(
670
- request: ChatItem,
671
- api_key: str = Depends(get_api_key) # Change Security to Depends
672
- ):
673
  user_id = request.user_id
674
  message = request.message
675
- logger.info(f"Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
676
  print_log("INFO", "Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
677
-
678
- user = get_user(user_id)
679
- user.add_ai_message(message)
680
-
681
- add_to_cache(user)
682
- pop_cache(user.user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
 
684
- print_log("INFO", "AI response added", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
685
- return {"response": "ok"}
686
-
687
-
688
- @app.post("/schedule_gg_reminder")
689
- @catch_endpoint_error
690
- async def schedule_gg_reminder(
691
- request: ChangeDateItem,
692
- api_key: str = Depends(get_api_key) # Change Security to Depends
693
- ):
694
- # session_id = request.gg_session_id
695
  user_id = request.user_id
696
- logger.info(f"Scheduling GG session reminder for {request.date}", extra={"user_id": user_id, "endpoint": "/schedule_gg_reminder"})
697
- print_log("INFO", f"Scheduling GG session: reminder for {request.date}", extra={"user_id": user_id, "endpoint": "/schedule_gg_reminder"})
698
 
699
  # get user
700
  user = get_user(user_id)
701
 
702
- # call user.ask_to_schedule_growth_guide_reminder(session_id)
703
- response = user.ask_to_schedule_growth_guide_reminder(request.date)
704
-
705
- logger.info(f"GG session reminder scheduled, response: {response}", extra={"user_id": user_id, "endpoint": "/schedule_gg_reminder"})
706
- return {"response": response}
707
-
708
- @app.post("/process_gg_session")
709
- @catch_endpoint_error
710
- async def process_gg_session(
711
- request: GGItem,
712
- api_key: str = Depends(get_api_key) # Change Security to Depends
713
- ):
714
- logger.info("Processing growth guide session", extra={"user_id": request.user_id, "endpoint": "/process_gg_session"})
715
 
716
- user = get_user(request.user_id)
717
- session_data = get_growth_guide_session(request.user_id, request.gg_session_id)
718
- response = user.process_growth_guide_session(session_data, request.gg_session_id)
719
- add_to_cache(user)
720
- pop_cache(user.user_id)
721
  return {"response": response}
722
 
723
-
724
  @app.get("/user_daily_messages")
725
- @catch_endpoint_error
726
- async def get_daily_message(
727
- user_id: str,
728
- api_key: str = Depends(get_api_key) # Change Security to Depends
729
- ):
730
  logger.info("Getting daily messages", extra={"user_id": user_id, "endpoint": "/user_daily_messages"})
731
  user = get_user(user_id)
732
  daily_messages = user.get_daily_messages()
733
  return {"response": daily_messages}
734
 
735
  @app.post("/batch_refresh_users")
736
- @catch_endpoint_error
737
- async def refresh_multiple_users(
738
- user_ids: List[str],
739
- api_key: str = Depends(get_api_key) # Change Security to Depends
740
- ):
741
  logger.info("Refreshing multiple users", extra={"endpoint": "/batch_refresh_users"})
742
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
743
  failed_users = []
744
 
745
  for i,user_id in enumerate(user_ids):
746
- old_user = get_user(user_id)
747
- user = old_user.refresh(client)
748
- add_to_cache(user)
749
- pop_cache(user.user_id)
750
- logger.info(f"Successfully refreshed user {i+1}/{len(user_ids)}", extra={"user_id": user_id, "endpoint": "/batch_refresh_users"})
 
 
 
 
751
 
752
  if failed_users:
753
  return {"status": "partial", "failed_users": failed_users}
754
  return {"status": "success", "failed_users": []}
755
 
756
  @app.post("/refresh_user")
757
- @catch_endpoint_error
758
- async def refresh_user(
759
- request: CreateUserItem,
760
- api_key: str = Depends(get_api_key) # Change Security to Depends
761
- ):
762
  print_log("INFO","Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
763
  logger.info("Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
764
 
765
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
766
  old_user = get_user(request.user_id)
767
  user = old_user.refresh(client)
768
- add_to_cache(user)
769
- pop_cache(user.user_id)
770
  print_log("INFO","User refreshed", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
771
  logger.info(f"User refreshed -> {user}", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
772
  return {"response": "ok"}
773
 
774
  @app.post("/create_user")
775
- @catch_endpoint_error
776
- async def create_user(
777
- request: CreateUserItem,
778
- api_key: str = Depends(get_api_key) # Change Security to Depends
779
- ):
780
  logger.info("Creating new user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
 
 
781
 
782
- client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
783
- if not client:
784
- raise OpenAIRequestError("client_init", "Failed to initialize OpenAI client")
785
-
786
- if os.path.exists(f'users/data/{request.user_id}.pkl'):
787
- return {"message": f"[OK] User already exists: {request.user_id}"}
788
-
789
- user_info, persona = get_user_info(request.user_id)
790
- # Persona is one of ["Coach Steve", "Coach Aris", "Coach Teresa"]
791
- # we map each of the above to assistant_ids = ["asst_mUm6MBcW544p1iVov9mwIC96", "asst_4WcktKgYdDnXA1QUlWvrNfWV", "asst_4UVkFK6r2pbz6NK6kNzG4sTW"]
792
- logger.info(f"Creating user with persona {persona}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
793
- persona_to_assistant = {
794
- "*Coach Steve*: Innovation & Entrepreneurship": "asst_mUm6MBcW544p1iVov9mwIC96",
795
- "*Coach Aris*: Logic & Decision Making": "asst_4WcktKgYdDnXA1QUlWvrNfWV",
796
- "*Coach Teresa*: Compassion & Service": "asst_4UVkFK6r2pbz6NK6kNzG4sTW"
797
- }
798
-
799
- if persona not in persona_to_assistant:
800
- logger.warning(f"Invalid persona: {persona}, defaulting to: Coach Steve", extra={"user_id": request.user_id, "endpoint": "/create_user"})
801
- # For testing we default to steve
802
- persona = "*Coach Steve*: Innovation & Entrepreneurship"
803
- # raise FastAPIError(
804
- # message="Invalid persona",
805
- # e="Persona must be one of ['Coach Steve', 'Coach Aris', 'Coach Teresa']"
806
- # )
807
-
808
- selected_persona = persona_to_assistant[persona]
809
 
810
- logger.info(f"Creating user with persona {persona}:{selected_persona}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
 
 
811
 
812
- user = User(request.user_id, user_info, client, selected_persona)
813
- folder_path = os.path.join("mementos", "to_upload", request.user_id)
814
- os.makedirs(folder_path, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
815
 
816
- add_to_cache(user)
817
- pop_cache(request.user_id)
 
 
818
 
819
- logger.info(f"Successfully created user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
820
- return {"message": {"info": f"[OK] User created: {user}", "messages": user.get_messages()}}
821
 
822
- @app.post("/fetch_daily_alert")
823
- @catch_endpoint_error
824
- async def fetch_daily_alert(
825
- request: ChangeDateItem,
826
- api_key: str = Depends(get_api_key) # Change Security to Depends
827
- ):
828
- logger.info(f"Upselling GG for day: {request.day}", extra={"user_id": request.user_id, "endpoint": "/upsell_gg"})
829
- user = get_user(request.user_id)
830
- response = user.get_alerts(request.date, request.day)
831
- logger.info(f"Alerts fetched: {response}", extra={"user_id": request.user_id, "endpoint": "/upsell_gg"})
832
- return {"response": response}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
833
 
834
  @app.post("/chat")
835
- @catch_endpoint_error
836
- async def chat(
837
- request: ChatItem,
838
- api_key: str = Depends(get_api_key) # Change Security to Depends
839
- ):
840
  logger.info("Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
841
- user = get_user(request.user_id)
842
-
843
- if request.image64:
844
- logger.info(f"Processing image", extra={"user_id": request.user_id, "endpoint": "/chat"})
845
- response = user.send_message(request.message, request.image64)
846
- logger.info(f"Assistant response generated", extra={"user_id": request.user_id, "endpoint": "/chat"})
847
- return {"response": response}
848
 
849
- @app.get("/reminders")
850
- @catch_endpoint_error
851
- async def get_reminders(
852
- user_id: str,
853
- date:str,
854
- api_key: str = Depends(get_api_key) # Change Security to Depends
855
- ):
856
- print_log("INFO","Getting reminders", extra={"user_id": user_id, "endpoint": "/reminders"})
857
- logger.info("Getting reminders", extra={"user_id": user_id, "endpoint": "/reminders"})
858
 
859
- user = get_user(user_id)
860
- reminders = user.get_reminders(date)
861
- if len(reminders) == 0:
862
- print_log("INFO",f"No reminders for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
863
- logger.info(f"No reminders for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
864
- reminders = None
 
 
 
 
 
 
865
 
866
- print_log("INFO",f"Successfully retrieved reminders: {reminders}", extra={"user_id": user_id, "endpoint": "/reminders"})
867
- logger.info(f"Successfully retrieved reminders: {reminders} for {date}", extra={"user_id": user_id, "endpoint": "/reminders"})
868
- return {"reminders": reminders}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
 
870
  @app.post("/change_date")
871
- @catch_endpoint_error
872
- async def change_date(
873
- request: ChangeDateItem,
874
- api_key: str = Depends(get_api_key) # Change Security to Depends
875
- ):
876
- logger.info(f"Processing date change request", extra={"user_id": request.user_id, "endpoint": "/change_date"})
877
-
878
- user = get_user(request.user_id)
879
-
880
- # Validate date format
881
  try:
882
- datetime.strptime(request.date, "%Y-%m-%d %a %H:%M:%S")
883
- except ValueError as e:
884
- raise FastAPIError(
885
- message="Invalid date format for /change_date expected format: %Y-%m-%d %a %H:%M:%S",
886
- e=str(e)
887
- )
888
- logger.info(f"Changing to {request.date}", extra={"user_id": request.user_id, "endpoint": "/change_date"})
889
- # Upload mementos to DB
890
- upload_mementos_to_db(request.user_id)
891
-
892
- # Change date and get response
893
- response = user.change_date(request.date)
894
- response['user_id'] = request.user_id
895
 
896
- # Update cache
897
- add_to_cache(user)
898
- pop_cache(user.user_id)
899
 
900
- return response
 
901
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
902
  @app.post("/reset_user_messages")
903
- @catch_endpoint_error
904
- async def reset_user_messages(
905
- request: CreateUserItem,
906
- api_key: str = Depends(get_api_key) # Change Security to Depends
907
- ):
908
  print_log("INFO","Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
909
  logger.info("Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
910
-
911
- user = get_user(request.user_id)
912
- user.reset_conversations()
913
- print_log("INFO",f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
914
- logger.info(f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
915
-
916
- add_to_cache(user)
917
- update = pop_cache(user.user_id)
918
-
919
- print_log("INFO",f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
920
- logger.info(f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
921
-
922
- return {"response": "ok"}
923
-
 
 
 
 
 
 
 
 
 
 
 
 
924
 
925
  @app.get("/get_logs")
926
- @catch_endpoint_error
927
- async def get_logs(
928
- user_id: str = Query(default="", description="User ID to fetch logs for")
929
- ):
930
  if (user_id):
931
  log_file_path = os.path.join('logs', 'users', f'{user_id}.log')
932
  if not os.path.exists(log_file_path):
@@ -951,206 +707,84 @@ async def get_logs(
951
  )
952
 
953
  @app.get("/is_user_responsive")
954
- @catch_endpoint_error
955
- async def is_user_responsive(
956
- user_id: str,
957
- api_key: str = Depends(get_api_key) # Change Security to Depends
958
- ):
959
  logger.info("Checking if user is responsive", extra={"user_id": user_id, "endpoint": "/is_user_responsive"})
960
-
961
- user = get_user(user_id)
962
- messages = user.get_messages()
963
- if len(messages) >= 3 and messages[-1]['role'] == 'assistant' and messages[-2]['role'] == 'assistant':
964
- return {"response": False}
965
- else:
966
- return {"response": True}
967
-
 
 
 
 
 
 
 
 
 
 
 
968
 
969
  @app.get("/get_user_summary")
970
- @catch_endpoint_error
971
- async def get_summary_by_id(
972
- user_id: str,
973
- api_key: str = Depends(get_api_key) # Change Security to Depends
974
- ):
975
  print_log("INFO", "Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
976
  logger.info("Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
977
-
978
- # NOTE: This call will also update the users recommended topics to discuss with GG.
979
- # This is so that the AI response will align with the dashboard.
980
- user_summary = get_user_summary(user_id, update_rec_topics=True)
981
- print_log("INFO", "Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
982
- logger.info("Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
983
- return user_summary
984
-
 
 
 
 
 
 
 
 
 
 
 
985
 
986
  @app.get("/get_life_status")
987
- @catch_endpoint_error
988
- async def get_life_status_by_id(
989
- user_id: str,
990
- api_key: str = Depends(get_api_key) # Change Security to Depends
991
- ):
992
  print_log("INFO", "Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
993
  logger.info("Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
994
-
995
- life_status = get_user_life_status(user_id)
996
- print_log("INFO", "Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
997
- logger.info("Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
998
- return life_status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
999
 
1000
  @app.post("/add_booking_point")
1001
- @catch_endpoint_error
1002
- async def add_booking_point_by_user(
1003
- user_id: str,
1004
- api_key: str = Depends(get_api_key) # Change Security to Depends
1005
- ):
1006
  user = get_user(user_id)
1007
  user.add_point_for_booking()
1008
  return {"response": "ok"}
1009
-
1010
 
1011
  @app.post("/add_session_completion_point")
1012
- @catch_endpoint_error
1013
- async def add_session_completion_point_by_user(
1014
- user_id: str,
1015
- api_key: str = Depends(get_api_key) # Change Security to Depends
1016
- ):
1017
  user = get_user(user_id)
1018
  user.add_point_for_completing_session()
1019
- return {"response": "ok"}
1020
-
1021
-
1022
- @app.post("/create_pre_gg_report")
1023
- @catch_endpoint_error
1024
- async def create_pre_gg_by_booking(
1025
- request: BookingItem,
1026
- api_key: str = Depends(get_api_key) # Change Security to Depends
1027
- ):
1028
- create_pre_gg_report(request.booking_id)
1029
- return {"response": "ok"}
1030
-
1031
-
1032
- @app.get("/get_user_persona")
1033
- @catch_endpoint_error
1034
- async def get_user_persona(
1035
- user_id: str,
1036
- api_key: str = Depends(get_api_key) # Change Security to Depends
1037
- ):
1038
- """Get user's legendary persona from the database"""
1039
- logger.info("Getting user's persona", extra={"user_id": user_id, "endpoint": "/get_user_persona"})
1040
-
1041
- # Connect to database
1042
- db_params = {
1043
- 'dbname': 'ourcoach',
1044
- 'user': 'ourcoach',
1045
- 'password': 'hvcTL3kN3pOG5KteT17T',
1046
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
1047
- 'port': '5432'
1048
- }
1049
- conn = psycopg2.connect(**db_params)
1050
- cur = conn.cursor()
1051
-
1052
- # Get onboarding data
1053
- cur.execute("SELECT onboarding FROM users WHERE id = %s", (user_id,))
1054
- result = cur.fetchone()
1055
- if not result:
1056
- raise DBError(
1057
- user_id=user_id,
1058
- code="NoOnboardingError",
1059
- message="User not found in database"
1060
- )
1061
- # Extract persona from onboarding JSON
1062
- onboarding = json.loads(result[0])
1063
- persona = onboarding.get('legendPersona', '')
1064
-
1065
- if 'cur' in locals():
1066
- cur.close()
1067
- if 'conn' in locals():
1068
- conn.close()
1069
-
1070
- return {"persona": persona}
1071
-
1072
-
1073
-
1074
- @app.get("/get_recent_booking")
1075
- @catch_endpoint_error
1076
- async def get_recent_booking(
1077
- user_id: str,
1078
- api_key: str = Depends(get_api_key) # Change Security to Depends
1079
- ):
1080
- """Get the most recent booking ID for a user"""
1081
- logger.info("Getting recent booking", extra={"user_id": user_id, "endpoint": "/get_recent_booking"})
1082
-
1083
- # Connect to database
1084
- db_params = {
1085
- 'dbname': 'ourcoach',
1086
- 'user': 'ourcoach',
1087
- 'password': 'hvcTL3kN3pOG5KteT17T',
1088
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
1089
- 'port': '5432'
1090
- }
1091
- conn = psycopg2.connect(**db_params)
1092
- cur = conn.cursor()
1093
-
1094
- # Get most recent booking where status == 2
1095
- cur.execute("""
1096
- SELECT booking_id
1097
- FROM public.user_notes
1098
- WHERE user_id = %s
1099
- ORDER BY created_at DESC
1100
- LIMIT 1
1101
- """, (user_id,))
1102
- result = cur.fetchone()
1103
-
1104
- if not result:
1105
- raise DBError(
1106
- user_id=user_id,
1107
- code="NoBookingError",
1108
- message="No bookings found for user"
1109
- )
1110
-
1111
- booking_id = result[0]
1112
- logger.info(f"Found recent booking: {booking_id}", extra={"user_id": user_id, "endpoint": "/get_recent_booking"})
1113
- if 'cur' in locals():
1114
- cur.close()
1115
- if 'conn' in locals():
1116
- conn.close()
1117
- return {"booking_id": booking_id}
1118
-
1119
- @app.post("/answer_image_question")
1120
- async def answer_image_question(
1121
- question: str = Form(...),
1122
- file: UploadFile = File(None),
1123
- image_base64: str = Form(None)
1124
- ):
1125
- if file:
1126
- contents = await file.read()
1127
- # convert to base64 string
1128
- image_base64 = base64.b64encode(contents).decode('utf-8')
1129
-
1130
- client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
1131
- response = client.chat.completions.create(
1132
- model="gpt-4o",
1133
- messages=[
1134
- {
1135
- "role": "user",
1136
- "content": [
1137
- {
1138
- "type": "text",
1139
- "text": "What is in this image?",
1140
- },
1141
- {
1142
- "type": "image_url",
1143
- "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"},
1144
- },
1145
- ],
1146
- }
1147
- ])
1148
-
1149
- return {"response": response.choices[0]}
1150
-
1151
- @app.post("/upload_image")
1152
- async def upload_image(file: UploadFile = File(...)):
1153
- # process or save the file
1154
- contents = await file.read()
1155
- image = Image.open(BytesIO(contents))
1156
- return {"filename": file.filename, "info": "Image uploaded successfully"}
 
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
 
8
  import logging
9
  import json
10
  import regex as re
11
+ from datetime import datetime
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 get_api_key, get_user_info, get_growth_guide_session, pop_cache, print_log, update_user, upload_file_to_s3, get_user, upload_mementos_to_db, get_user_summary, get_user_life_status, get_life_score
19
  from dotenv import load_dotenv
20
  import logging.config
21
  import time
22
  from starlette.middleware.base import BaseHTTPMiddleware
23
  import sys
 
 
 
 
 
 
 
 
24
 
25
  load_dotenv()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  # Create required folders
28
  os.makedirs('logs', exist_ok=True)
 
35
  for file in os.listdir(os.path.join('users', 'data')):
36
  os.remove(os.path.join('users', 'data', file))
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  if not os.path.exists(os.path.join('users', 'to_upload')):
39
  os.makedirs(os.path.join('users', 'to_upload'))
40
  if not os.path.exists(os.path.join('mementos', 'to_upload')):
 
158
  }
159
  }
160
 
161
+ logging.config.dictConfig(logging_config)
162
+ logger = logging.getLogger(__name__)
 
 
 
 
 
 
163
 
164
  # Suppress verbose logs from external libraries
165
  logging.getLogger("httpx").setLevel(logging.WARNING)
 
217
  raise
218
 
219
  # OpenAI Client
220
+ GENERAL_ASSISTANT = os.getenv('OPENAI_GENERAL_ASSISTANT')
221
 
222
  # Initialize Logging (optional)
223
  # logging.basicConfig(filename='app.log', level=logging.INFO)
 
233
  class ChatItem(BaseModel):
234
  user_id: str
235
  message: str
 
236
 
237
+ class ChangeDateItem(BaseModel):
238
  user_id: str
239
+ date: str
240
 
241
  class GGItem(BaseModel):
 
242
  gg_session_id: str
 
 
 
 
 
 
243
  user_id: str
 
 
244
 
245
+ class ErrorResponse(BaseModel):
246
+ status: str = "error"
247
+ code: int
248
+ message: str
249
+ timestamp: datetime = datetime.now()
250
 
251
+ @app.get("/ok")
252
+ def ok_endpoint():
253
+ print_log("INFO", "health check endpoint")
254
+ logger.info("Health check endpoint called", extra={"endpoint": "/ok"})
255
+ return {"message": "ok"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  @app.post("/set_intro_done")
258
+ def set_intro_done(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
259
  user = get_user(user_id)
 
260
  user.set_intro_done()
261
  logger.info("Intro done", extra={"user_id": user_id, "endpoint": "/set_intro_done"})
262
  return {"response": "ok"}
263
 
 
264
  @app.post("/set_goal")
265
+ def set_goal(user_id: str, goal: str, api_key: str = Security(get_api_key)):
266
+ user = get_user(user_id)
 
 
 
 
 
 
267
  user.set_goal(goal)
268
  logger.info(f"Goal set: {goal}", extra={"user_id": user_id, "endpoint": "/set_goal"})
269
  return {"response": "ok"}
270
 
271
+ @app.post("/do_micro")
272
+ def do_micro(request: ChangeDateItem, day: int, api_key: str = Security(get_api_key)):
273
+ print_log("INFO", "do_micro endpoint")
274
+ logger.info("do_micro endpoint called", extra={"endpoint": "/do_micro"})
275
+
276
+ # get user
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  user = get_user(request.user_id)
278
 
279
+ try:
280
+ response = user.do_micro(request.date, day)
281
+ except openai.BadRequestError:
282
+ # Check if there is an active run for the thread id
283
+ recent_run = user.get_recent_run()
284
+ print_log("INFO",f"Recent run: {recent_run}", extra={"user_id": request.user_id, "endpoint": "/chat"})
285
+ logger.info(f"Recent run: {recent_run}", extra={"user_id": request.user_id, "endpoint": "/chat"})
286
+ # If there is an active run, cancel it and resubmit the previous message
287
+ if recent_run:
288
+ user.cancel_run(recent_run)
289
+ response = user.send_message(user.get_recent_message())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ print_log("INFO",f"Assistant: {response['content']}", extra={"user_id": request.user_id, "endpoint": "/chat"})
292
+ logger.info(f"Assistant: {response['content']}", extra={"user_id": request.user_id, "endpoint": "/chat"})
293
+ return {"response": response}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
 
 
295
 
296
+ # endpoint to change user assistant using user.change_to_latest_assistant()
297
+ @app.get("/change_assistant")
298
+ def change_assistant(user_id: str, api_key: str = Security(get_api_key)):
299
+ print_log("INFO", "Changing assistant", extra={"user_id": user_id, "endpoint": "/change_assistant"})
300
+ logger.info("Changing assistant", extra={"user_id": user_id, "endpoint": "/change_assistant"})
301
+ user = get_user(user_id)
302
+ assistant_id = user.change_to_latest_assistant()
303
+ logger.info(f"Assistant changed to {assistant_id}", extra={"user_id": user_id, "endpoint": "/change_assistant"})
304
+ return {"assistant_id": assistant_id}
305
+
306
+
307
  @app.get("/get_user")
308
+ def get_user_by_id(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
309
  print_log("INFO", "Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
310
  logger.info("Getting user", extra={"user_id": user_id, "endpoint": "/get_user"})
311
+ try:
312
+ user = get_user(user_id)
313
+ print_log("INFO", "Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
314
+ logger.info("Successfully retrieved user", extra={"user_id": user_id, "endpoint": "/get_user"})
315
+ api_response = {"user": str(user), "user_messages": user.get_messages(), "general_assistant": user.conversations.assistants['general'].id, "intro_assistant": user.conversations.assistants['intro'].id}
316
+
317
+ if user.goal:
318
+ api_response["goal"] = user.goal
319
+ else:
320
+ api_response["goal"] = ["Goal is not set yet"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
+ api_response["current_day"] = user.growth_plan.current()['day']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
+ return api_response
325
+ except LookupError:
326
+ print_log("ERROR", "User not found", extra={"user_id": user_id, "endpoint": "/get_user"})
327
+ logger.error("User not found", extra={"user_id": user_id, "endpoint": "/get_user"})
328
+ raise HTTPException(
329
+ status_code=status.HTTP_404_NOT_FOUND,
330
+ detail=f"User with ID {user_id} not found"
331
+ )
332
+ except Exception as e:
333
+ print_log("ERROR",f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user"}, exc_info=True)
334
+ logger.error(f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user"}, exc_info=True)
335
+ raise HTTPException(
336
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
337
+ detail=str(e)
338
+ )
339
 
340
+ @app.get("/get_user_life_score")
341
+ def life_score_by_id(user_id: str, api_key: str = Security(get_api_key)):
342
+ print_log("INFO", "Getting user life score", extra={"user_id": user_id, "endpoint": "/get_user_life_score"})
343
+ logger.info("Getting user life score", extra={"user_id": user_id, "endpoint": "/get_user_life_score"})
344
+ try:
345
+ life_score = get_life_score(user_id)
346
+ print_log("INFO", "Successfully retrieved user life score", extra={"user_id": user_id, "endpoint": "/get_user_life_score"})
347
+ logger.info("Successfully retrieved user life score", extra={"user_id": user_id, "endpoint": "/get_user_life_score"})
348
+
349
+ return life_score
350
+ except LookupError:
351
+ print_log("ERROR", "User not found", extra={"user_id": user_id, "endpoint": "/get_user_life_score"})
352
+ logger.error("User not found", extra={"user_id": user_id, "endpoint": "/get_user_life_score"})
353
+ raise HTTPException(
354
+ status_code=status.HTTP_404_NOT_FOUND,
355
+ detail=f"User with ID {user_id} not found"
356
+ )
357
+ except Exception as e:
358
+ print_log("ERROR",f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user_life_score"}, exc_info=True)
359
+ logger.error(f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user_life_score"}, exc_info=True)
360
+ raise HTTPException(
361
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
362
+ detail=str(e)
363
+ )
364
 
365
  @app.post("/add_ai_message")
366
+ def add_ai_message(request: ChatItem, api_key: str = Security(get_api_key)):
 
 
 
 
367
  user_id = request.user_id
368
  message = request.message
369
+ logger.info("Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
370
  print_log("INFO", "Adding AI response", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
371
+ try:
372
+ user = get_user(user_id)
373
+ user.add_ai_message(message)
374
+ user.save_user()
375
+ update_user(user)
376
+ print_log("INFO", "AI response added", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
377
+ return {"response": "ok"}
378
+ except LookupError:
379
+ print_log("ERROR", "User not found", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
380
+ logger.error("User not found", extra={"user_id": user_id, "endpoint": "/add_ai_message"})
381
+ raise HTTPException(
382
+ status_code=status.HTTP_404_NOT_FOUND,
383
+ detail=f"User with ID {user_id} not found"
384
+ )
385
+ except Exception as e:
386
+ print_log("ERROR",f"Error adding AI response: {str(e)}", extra={"user_id": user_id, "endpoint": "/add_ai_message"}, exc_info=True)
387
+ logger.error(f"Error adding AI response: {str(e)}", extra={"user_id": user_id, "endpoint": "/add_ai_message"}, exc_info=True)
388
+ raise HTTPException(
389
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
390
+ detail=str(e)
391
+ )
392
 
393
+ @app.post("/process_gg_session")
394
+ def process_gg_session(request: GGItem, api_key: str = Security(get_api_key)):
395
+ session_id = request.gg_session_id
 
 
 
 
 
 
 
 
396
  user_id = request.user_id
397
+ logger.info(f"Processing GG session: {session_id}", extra={"user_id": user_id, "endpoint": "/process_gg_session"})
398
+ print_log("INFO", f"Processing GG session: {session_id}", extra={"user_id": user_id, "endpoint": "/process_gg_session"})
399
 
400
  # get user
401
  user = get_user(user_id)
402
 
403
+ # get the session_data
404
+ session_data = get_growth_guide_session(user_id, session_id)
 
 
 
 
 
 
 
 
 
 
 
405
 
406
+ # update user
407
+ response = user.process_growth_guide_session(session_data)
408
+ logger.info(f"GG session processed: {session_id}, response: {response}", extra={"user_id": user_id, "endpoint": "/process_gg_session"})
 
 
409
  return {"response": response}
410
 
 
411
  @app.get("/user_daily_messages")
412
+ def get_daily_message(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
413
  logger.info("Getting daily messages", extra={"user_id": user_id, "endpoint": "/user_daily_messages"})
414
  user = get_user(user_id)
415
  daily_messages = user.get_daily_messages()
416
  return {"response": daily_messages}
417
 
418
  @app.post("/batch_refresh_users")
419
+ def refresh_multiple_users(user_ids: List[str], api_key: str = Security(get_api_key)):
 
 
 
 
420
  logger.info("Refreshing multiple users", extra={"endpoint": "/batch_refresh_users"})
421
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
422
  failed_users = []
423
 
424
  for i,user_id in enumerate(user_ids):
425
+ try:
426
+ old_user = get_user(user_id)
427
+ user = old_user.refresh(client)
428
+ user.save_user()
429
+ update_user(user)
430
+ logger.info(f"Successfully refreshed user {i+1}/{len(user_ids)}", extra={"user_id": user_id, "endpoint": "/batch_refresh_users"})
431
+ except Exception as e:
432
+ logger.error(f"Failed to refresh user: {str(e)}", extra={"user_id": user_id, "endpoint": "/batch_refresh_users"})
433
+ failed_users.append(user_id)
434
 
435
  if failed_users:
436
  return {"status": "partial", "failed_users": failed_users}
437
  return {"status": "success", "failed_users": []}
438
 
439
  @app.post("/refresh_user")
440
+ def refresh_user(request: CreateUserItem, api_key: str = Security(get_api_key)):
 
 
 
 
441
  print_log("INFO","Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
442
  logger.info("Refreshing user", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
443
 
444
  client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
445
  old_user = get_user(request.user_id)
446
  user = old_user.refresh(client)
447
+ user.save_user()
448
+ update_user(user)
449
  print_log("INFO","User refreshed", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
450
  logger.info(f"User refreshed -> {user}", extra={"user_id": request.user_id, "endpoint": "/refresh_user"})
451
  return {"response": "ok"}
452
 
453
  @app.post("/create_user")
454
+ def create_user(request: CreateUserItem, api_key: str = Security(get_api_key)):
455
+ print_log("INFO","Creating new user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
 
 
 
456
  logger.info("Creating new user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
457
+ try:
458
+ client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
459
 
460
+ # check if user exists by looking for pickle file in users/data
461
+ if os.path.exists(f'users/data/{request.user_id}.pkl'):
462
+ print_log("INFO",f"User already exists: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
463
+ logger.info(f"User already exists: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
464
+ return {"message": f"[OK] User already exists: {request.user_id}"}
465
+
466
+ user_info, _ = get_user_info(request.user_id)
467
+ if not user_info:
468
+ print_log("ERROR",f"Could not fetch user information from DB {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
469
+ logger.error(f"Could not fetch user information from DB {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/create_user"})
470
+ raise HTTPException(
471
+ status_code=status.HTTP_400_BAD_REQUEST,
472
+ detail="Could not fetch user information from DB"
473
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
474
 
475
+ user = User(request.user_id, user_info, client, GENERAL_ASSISTANT)
476
+ save = user.save_user()
477
+
478
 
479
+ if save:
480
+ print_log("INFO",f"Created pickle file for user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
481
+ logger.info(f"Created pickle file for user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
482
+ else:
483
+ print_log("ERROR",f"Failed to create (user.save_user()) pickle file", extra={"user_id": request.user_id, "endpoint": "/create_user"})
484
+ logger.error(f"Failed to create (user.save_user()) pickle file", extra={"user_id": request.user_id, "endpoint": "/create_user"})
485
+ raise HTTPException(
486
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
487
+ detail="Failed to create user pickle file"
488
+ )
489
+
490
+ # create memento folder for user
491
+ folder_path = os.path.join("mementos", "to_upload", request.user_id)
492
 
493
+ # create folder if not exists
494
+ os.makedirs(folder_path, exist_ok=True)
495
+ print_log("INFO",f"Created temp memento folder for user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
496
+ logger.info(f"Created temp memento folder for user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
497
 
 
 
498
 
499
+ # upload user pickle file to s3 bucket
500
+ try:
501
+ # value.save_user()
502
+ pop_cache(request.user_id)
503
+ upload = True
504
+ except:
505
+ upload = False
506
+
507
+ if upload == True:
508
+ print_log("INFO",f"Successfully created user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
509
+ logger.info(f"Successfully created user", extra={"user_id": request.user_id, "endpoint": "/create_user"})
510
+ return {"message": f"[OK] User created: {user.user_id}"}
511
+ else:
512
+ print_log("ERROR",f"Failed to upload user pickle to S3", extra={"user_id": request.user_id, "endpoint": "/create_user"})
513
+ logger.error(f"Failed to upload user pickle to S3", extra={"user_id": request.user_id, "endpoint": "/create_user"})
514
+ raise HTTPException(
515
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
516
+ detail="Failed to upload user pickle to S3"
517
+ )
518
+
519
+ except Exception as e:
520
+ print_log("ERROR",f"Failed to create user: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/create_user"}, exc_info=True)
521
+ logger.error(f"Failed to create user: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/create_user"}, exc_info=True)
522
+ raise HTTPException(
523
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
524
+ detail=str(e)
525
+ )
526
 
527
  @app.post("/chat")
528
+ def chat(request: ChatItem, api_key: str = Security(get_api_key)):
529
+ print_log("INFO","Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
 
 
 
530
  logger.info("Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
 
 
 
 
 
 
 
531
 
532
+ try:
533
+ # get user
534
+ user = get_user(request.user_id)
 
 
 
 
 
 
535
 
536
+ try:
537
+ response = user.send_message(request.message)
538
+ except openai.BadRequestError as e:
539
+ print(e)
540
+ # Check if there is an active run for the thread id
541
+ recent_run = user.get_recent_run()
542
+ print_log("INFO",f"Recent run: {recent_run}", extra={"user_id": request.user_id, "endpoint": "/chat"})
543
+ logger.info(f"Recent run: {recent_run}", extra={"user_id": request.user_id, "endpoint": "/chat"})
544
+ # If there is an active run, cancel it and resubmit the previous message
545
+ if recent_run:
546
+ user.cancel_run(recent_run)
547
+ response = user.send_message(user.get_recent_message())
548
 
549
+ print_log("INFO",f"Assistant: {response['content']}", extra={"user_id": request.user_id, "endpoint": "/chat"})
550
+ logger.info(f"Assistant: {response['content']}", extra={"user_id": request.user_id, "endpoint": "/chat"})
551
+ return {"response": response}
552
+ except LookupError:
553
+ print_log("ERROR",f"User not found for chat: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/chat"})
554
+ logger.error(f"User not found for chat: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/chat"})
555
+ raise HTTPException(
556
+ status_code=status.HTTP_404_NOT_FOUND,
557
+ detail=f"User with ID {request.user_id} not found"
558
+ )
559
+ except ReferenceError:
560
+ logger.warning(f"User pickle creation still ongoing for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/chat"})
561
+ print_log("WARNING",f"User pickle creation still ongoing for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/chat"})
562
+ raise HTTPException(
563
+ status_code=status.HTTP_400_BAD_REQUEST,
564
+ detail="User pickle creation still ongoing"
565
+ )
566
+ except Exception as e:
567
+ print_log("ERROR",f"Chat error for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/chat"}, exc_info=True)
568
+ logger.error(f"Chat error for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/chat"}, exc_info=True)
569
+ raise HTTPException(
570
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
571
+ detail=str(e)
572
+ )
573
 
574
  @app.post("/change_date")
575
+ def change_date(request: ChangeDateItem, api_key: str = Security(get_api_key)):
576
+ print_log("INFO",f"Processing date change request, new date: {request.date}",
577
+ extra={"user_id": request.user_id, "endpoint": "/change_date"})
578
+ logger.info(f"Processing date change request, new date: {request.date}",
579
+ extra={"user_id": request.user_id, "endpoint": "/change_date"})
 
 
 
 
 
580
  try:
581
+ user_id = request.user_id
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
+ user = get_user(user_id)
584
+ logger.info(f"User: {user}", extra={"user_id": user_id, "endpoint": "/change_date"})
 
585
 
586
+ # infer follow_up dates
587
+ user.infer_memento_follow_ups()
588
 
589
+ # Push users mementos to DB
590
+ try:
591
+ upload = upload_mementos_to_db(user_id)
592
+ if upload:
593
+ print_log("INFO",f"Uploaded mementos to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
594
+ logger.info(f"Uploaded mementos to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
595
+ else:
596
+ print_log("ERROR",f"Failed to upload mementos to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
597
+ logger.error(f"Failed to upload mementos to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
598
+ raise HTTPException(
599
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
600
+ detail=f"Failed to upload mementos to DB for user: {user_id}"
601
+ )
602
+ except ConnectionError as e:
603
+ print_log("ERROR",f"Failed to connect to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
604
+ logger.error(f"Failed to connect to DB for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
605
+ raise HTTPException(
606
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
607
+ detail=f"Failed to connect to DB for user: {user_id}"
608
+ )
609
+
610
+ response = user.change_date(request.date)
611
+ response['user_id'] = user_id
612
+
613
+ print_log("INFO",f"Date changed successfully for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
614
+ logger.info(f"Date changed successfully for user: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
615
+ print_log("DEBUG",f"Change date response: {response}", extra={"user_id": user_id, "endpoint": "/change_date"})
616
+ logger.debug(f"Change date response: {response}", extra={"user_id": user_id, "endpoint": "/change_date"})
617
+
618
+ # Update user
619
+ user.save_user()
620
+ update = update_user(user)
621
+ if not update:
622
+ print_log("ERROR",f"Failed to update user pickle in S3: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
623
+ logger.error(f"Failed to update user pickle in S3: {user_id}", extra={"user_id": user_id, "endpoint": "/change_date"})
624
+ raise HTTPException(
625
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
626
+ detail=f"Failed to update user pickle in S3 {user_id}"
627
+ )
628
+
629
+ return {"response": response}
630
+
631
+ except ValueError as e:
632
+ print_log("ERROR",f"Invalid date format for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/change_date"})
633
+ logger.error(f"Invalid date format for user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/change_date"})
634
+ raise HTTPException(
635
+ status_code=status.HTTP_400_BAD_REQUEST,
636
+ detail=str(e)
637
+ )
638
+ except LookupError:
639
+ print_log("ERROR",f"User not found for date change: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/change_date"})
640
+ logger.error(f"User not found for date change: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/change_date"})
641
+ raise HTTPException(
642
+ status_code=status.HTTP_404_NOT_FOUND,
643
+ detail=f"User with ID {request.user_id} not found"
644
+ )
645
+ except Exception as e:
646
+ 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)
647
+ 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)
648
+ raise HTTPException(
649
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
650
+ detail=str(e)
651
+ )
652
+
653
  @app.post("/reset_user_messages")
654
+ def reset_user_messages(request: CreateUserItem, api_key: str = Security(get_api_key)):
 
 
 
 
655
  print_log("INFO","Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
656
  logger.info("Resetting messages", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
657
+ try:
658
+ user = get_user(request.user_id)
659
+ user.reset_conversations()
660
+ print_log("INFO",f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
661
+ logger.info(f"Successfully reset messages for user: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
662
+
663
+ user.save_user()
664
+ update_user(user)
665
+ print_log("INFO",f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
666
+ logger.info(f"Successfully updated user pickle: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
667
+
668
+ return {"response": "ok"}
669
+ except LookupError:
670
+ print_log("ERROR",f"User not found for reset: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
671
+ logger.error(f"User not found for reset: {request.user_id}", extra={"user_id": request.user_id, "endpoint": "/reset_user"})
672
+ raise HTTPException(
673
+ status_code=status.HTTP_404_NOT_FOUND,
674
+ detail=f"User with ID {request.user_id} not found"
675
+ )
676
+ except Exception as e:
677
+ print_log("ERROR",f"Error resetting user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/reset_user"}, exc_info=True)
678
+ logger.error(f"Error resetting user {request.user_id}: {str(e)}", extra={"user_id": request.user_id, "endpoint": "/reset_user"}, exc_info=True)
679
+ raise HTTPException(
680
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
681
+ detail=str(e)
682
+ )
683
 
684
  @app.get("/get_logs")
685
+ def get_logs(user_id: str = Query(default="", description="User ID to fetch logs for")):
 
 
 
686
  if (user_id):
687
  log_file_path = os.path.join('logs', 'users', f'{user_id}.log')
688
  if not os.path.exists(log_file_path):
 
707
  )
708
 
709
  @app.get("/is_user_responsive")
710
+ def is_user_responsive(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
711
  logger.info("Checking if user is responsive", extra={"user_id": user_id, "endpoint": "/is_user_responsive"})
712
+ try:
713
+ user = get_user(user_id)
714
+ messages = user.get_messages()
715
+ if len(messages) >= 3 and messages[-1]['role'] == 'assistant' and messages[-2]['role'] == 'assistant':
716
+ return {"response": False}
717
+ else:
718
+ return {"response": True}
719
+ except LookupError:
720
+ logger.error(f"User not found: {user_id}", extra={"user_id": user_id, "endpoint": "/is_user_responsive"})
721
+ raise HTTPException(
722
+ status_code=status.HTTP_404_NOT_FOUND,
723
+ detail=f"User with ID {user_id} not found"
724
+ )
725
+ except Exception as e:
726
+ logger.error(f"Error checking user responsiveness: {str(e)}", extra={"user_id": user_id, "endpoint": "/is_user_responsive"}, exc_info=True)
727
+ raise HTTPException(
728
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
729
+ detail=str(e)
730
+ )
731
 
732
  @app.get("/get_user_summary")
733
+ def get_summary_by_id(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
734
  print_log("INFO", "Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
735
  logger.info("Getting user's summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
736
+ try:
737
+ user_summary = get_user_summary(user_id)
738
+ print_log("INFO", "Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
739
+ logger.info("Successfully generated summary", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
740
+ return user_summary
741
+ except LookupError:
742
+ print_log("ERROR", "User not found", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
743
+ logger.error("User not found", extra={"user_id": user_id, "endpoint": "/get_user_summary"})
744
+ raise HTTPException(
745
+ status_code=status.HTTP_404_NOT_FOUND,
746
+ detail=f"User with ID {user_id} not found"
747
+ )
748
+ except Exception as e:
749
+ print_log("ERROR",f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user_summary"}, exc_info=True)
750
+ logger.error(f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_user_summary"}, exc_info=True)
751
+ raise HTTPException(
752
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
753
+ detail=str(e)
754
+ )
755
 
756
  @app.get("/get_life_status")
757
+ def get_life_status_by_id(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
758
  print_log("INFO", "Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
759
  logger.info("Getting user's life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
760
+ try:
761
+ life_status = get_user_life_status(user_id)
762
+ print_log("INFO", "Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
763
+ logger.info("Successfully generated life status", extra={"user_id": user_id, "endpoint": "/get_life_status"})
764
+ return life_status
765
+ except LookupError:
766
+ print_log("ERROR", "User not found", extra={"user_id": user_id, "endpoint": "/get_life_status"})
767
+ logger.error("User not found", extra={"user_id": user_id, "endpoint": "/get_life_status"})
768
+ raise HTTPException(
769
+ status_code=status.HTTP_404_NOT_FOUND,
770
+ detail=f"User with ID {user_id} not found"
771
+ )
772
+ except Exception as e:
773
+ print_log("ERROR",f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_life_status"}, exc_info=True)
774
+ logger.error(f"Error getting user: {str(e)}", extra={"user_id": user_id, "endpoint": "/get_life_status"}, exc_info=True)
775
+ raise HTTPException(
776
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
777
+ detail=str(e)
778
+ )
779
 
780
  @app.post("/add_booking_point")
781
+ def add_booking_point_by_user(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
782
  user = get_user(user_id)
783
  user.add_point_for_booking()
784
  return {"response": "ok"}
 
785
 
786
  @app.post("/add_session_completion_point")
787
+ def add_session_completion_point_by_user(user_id: str, api_key: str = Security(get_api_key)):
 
 
 
 
788
  user = get_user(user_id)
789
  user.add_point_for_completing_session()
790
+ return {"response": "ok"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/reminder_example.json DELETED
@@ -1,27 +0,0 @@
1
- {
2
- "role": "assistant",
3
- "content": "Reminders are all set! 📅\n\n- Quality time with Cat at 7 PM daily\n- Audiophile hobby at 8 PM daily\n- Hang out with the Old Mans Club for drinks every Saturday at 7 PM\n- Family time every Sunday at 11 AM\n\nFeel free to reach out anytime! You’re making great strides toward that work-life balance! 💪✨",
4
- "reminders": [
5
- {
6
- "reminder": "⏰ **Reminder**: Hey Shaggy! It's time to spend quality time with Cat ❤️. Enjoy your evening!",
7
- "timestamp": "2024-12-15T19:00:00",
8
- "recurrence": "daily"
9
- },
10
- {
11
- "reminder": "⏰ **Reminder**: Hey Shaggy! Time for your audiophile hobby 🎧. Let the music flow!",
12
- "timestamp": "2024-12-15T20:00:00",
13
- "recurrence": "daily"
14
- },
15
- {
16
- "reminder": "⏰ **Reminder**: Hey Shaggy! Don't forget to hang out with the Old Mans Club for drinks tonight 🍻!",
17
- "timestamp": "2024-12-16T19:00:00",
18
- "recurrence": "weekly"
19
- },
20
- {
21
- "reminder": "⏰ **Reminder**: Hey Shaggy! Family time ❤️ is coming up. Make the most of it!",
22
- "timestamp": "2024-12-16T11:00:00",
23
- "recurrence": "weekly"
24
- }
25
- ]
26
- }
27
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/requirements.txt CHANGED
@@ -13,6 +13,3 @@ python-dotenv==1.0.1
13
  regex==2024.9.11
14
  Requests==2.32.3
15
  cachetools>=5.0.0
16
- pdfkit==1.0.0
17
- PyPDF2==3.0.1
18
- sentry-sdk==2.19.2
 
13
  regex==2024.9.11
14
  Requests==2.32.3
15
  cachetools>=5.0.0
 
 
 
app/user.py CHANGED
@@ -1,34 +1,19 @@
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
8
  from app.assistants import Assistant
9
- from app.exceptions import DBError
10
  import glob
11
  import pickle # Replace dill with pickle
12
  import random
13
  import logging
14
- import psycopg2
15
- from psycopg2 import sql
16
- from app.conversation_manager import ConversationManager
17
- from app.exceptions import BaseOurcoachException, OpenAIRequestError, UserError
18
 
19
- 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
20
  from pydantic import BaseModel
21
  from datetime import datetime
22
 
23
- from app.utils import generate_uuid, get_booked_gg_sessions, get_growth_guide, get_growth_guide_summary, get_user_subscriptions, update_growth_guide_summary
24
-
25
- import dotenv
26
- import re
27
- import math
28
- dotenv.load_dotenv()
29
-
30
- OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
31
-
32
  class UserDataItem(BaseModel):
33
  role: str
34
  content: str
@@ -36,34 +21,248 @@ class UserDataItem(BaseModel):
36
  status: str
37
  created_at: str
38
  updated_at: str
39
- area: str
40
 
41
  class UserDataResponse(BaseModel):
42
  data: list[UserDataItem]
43
 
44
- class Index(BaseModel):
45
- value: int
46
-
47
  logger = logging.getLogger(__name__)
48
 
49
  def get_current_datetime():
50
- return datetime.now(timezone.utc)
51
 
52
- class User:
53
- def catch_error(func):
54
- def wrapper(self, *args, **kwargs):
55
- try:
56
- return func(self, *args, **kwargs)
57
- except (BaseOurcoachException, openai.BadRequestError) as e:
58
- raise e
59
- except openai.BadRequestError as e:
60
- raise OpenAIRequestError(user_id=self.user_id, message="OpenAI Request Error", e=str(e))
61
- except Exception as e:
62
- # Handle other exceptions
63
- logger.error(f"An unexpected error occurred in User: {e}")
64
- raise UserError(user_id=self.user_id, message="Unexpected error in User", e=str(e))
65
- return wrapper
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
 
67
  def __init__(self, user_id, user_info, client, asst_id):
68
  self.user_id = user_id
69
  self.client = client
@@ -71,9 +270,7 @@ class User:
71
  self.user_info = user_info
72
  self.done_first_reflection = None
73
  self.goal = []
74
- self.last_gg_session = None
75
  self.micro_actions = []
76
- self.recommended_micro_actions = []
77
  self.challenges = []
78
  self.other_focusses = []
79
  self.personal_growth_score = 0
@@ -81,14 +278,8 @@ class User:
81
  self.relationship_score = 0
82
  self.mental_well_being_score = 0
83
  self.health_and_wellness_score = 0
84
- self.reminders = None
85
- self.recent_wins = []
86
- self.recommended_gg_topics = []
87
- self.mantra = None
88
 
89
  # Read growth_plan.json and store it
90
-
91
- # TESTING PURPOSE
92
  growth_plan = {"growthPlan": [
93
  {
94
  "day": 1,
@@ -96,11 +287,11 @@ class User:
96
  },
97
  {
98
  "day": 2,
99
- "coachingTheme": "FOLLUP_ACTION_STATE"
100
  },
101
  {
102
  "day": 3,
103
- "coachingTheme": "OPEN_DISCUSSION_STATE"
104
  },
105
  {
106
  "day": 4,
@@ -108,167 +299,56 @@ class User:
108
  },
109
  {
110
  "day": 5,
111
- "coachingTheme": "FOLLUP_ACTION_STATE"
112
  },
113
  {
114
  "day": 6,
115
- "coachingTheme": "FUNFACT_STATE"
116
  },
117
  {
118
  "day": 7,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  "coachingTheme": "FINAL_SUMMARY_STATE"
120
  }
121
- ]}
122
-
123
-
124
  self.growth_plan = CircularQueue(array=growth_plan['growthPlan'], user_id=self.user_id)
125
  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"})
126
 
127
  self.user_interaction_guidelines = self.generate_user_interaction_guidelines(user_info, client)
128
  self.conversations = ConversationManager(client, self, asst_id)
129
 
130
- self.score_history = []
131
- self.cumulative_plan_day = 0
132
-
133
- @catch_error
134
- def extend_growth_plan(self):
135
- # Change current growth plan to 14d growth plan
136
- logger.info(f"Changing plan to 14d...", extra={"user_id": self.user_id, "endpoint": "extend_growth_plan"})
137
- new_growth_plan = {"growthPlan": [
138
- {
139
- "day": 1,
140
- "coachingTheme": "MICRO_ACTION_STATE"
141
- },
142
- {
143
- "day": 2,
144
- "coachingTheme": "FOLLUP_ACTION_STATE"
145
- },
146
- {
147
- "day": 3,
148
- "coachingTheme": "OPEN_DISCUSSION_STATE"
149
- },
150
- {
151
- "day": 4,
152
- "coachingTheme": "MICRO_ACTION_STATE"
153
- },
154
- {
155
- "day": 5,
156
- "coachingTheme": "FOLLUP_ACTION_STATE"
157
- },
158
- {
159
- "day": 6,
160
- "coachingTheme": "FUNFACT_STATE"
161
- },
162
- {
163
- "day": 7,
164
- "coachingTheme": "PROGRESS_REFLECTION_STATE"
165
- },
166
- {
167
- "day": 8,
168
- "coachingTheme": "MICRO_ACTION_STATE"
169
- },
170
- {
171
- "day": 9,
172
- "coachingTheme": "FOLLUP_ACTION_STATE"
173
- },
174
- {
175
- "day": 10,
176
- "coachingTheme": "OPEN_DISCUSSION_STATE"
177
- },
178
- {
179
- "day": 11,
180
- "coachingTheme": "MICRO_ACTION_STATE"
181
- },
182
- {
183
- "day": 12,
184
- "coachingTheme": "FOLLUP_ACTION_STATE"
185
- },
186
- {
187
- "day": 13,
188
- "coachingTheme": "FUNFACT_STATE"
189
- },
190
- {
191
- "day": 14,
192
- "coachingTheme": "FINAL_SUMMARY_STATE"
193
- }
194
- ]
195
- }
196
- self.growth_plan = CircularQueue(array=new_growth_plan['growthPlan'], user_id=self.user_id)
197
- 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"})
198
- logger.info(f"Success.", extra={"user_id": self.user_id, "endpoint": "extend_growth_plan"})
199
- return True
200
-
201
- @catch_error
202
- def reset_cumulative_plan_day(self):
203
- logger.info(f"Reseting cumulative_plan_day", extra={"user_id": self.user_id, "endpoint": "reset_cumulative_plan_day"})
204
- self.cumulative_plan_day = 0
205
-
206
- @catch_error
207
- def add_recent_wins(self, wins, context = None):
208
- prompt = f"""
209
- ## Role
210
- 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:
211
-
212
- ```json
213
- {{
214
- achievement_message: str
215
- }}
216
- ```
217
-
218
- Note: No need to mention the user's name. Make it concise (less than 15 words)
219
-
220
- ## Example
221
- User's achievement: Completing a Task
222
- Achievement context: The user has completed a 10k run
223
-
224
- Output:
225
- ```
226
- {{
227
- achievement_message: You have completed a 10k run!
228
- }}
229
- ```
230
-
231
- ## User Input
232
-
233
- User's achievement: {wins}
234
- Achievement context: {context}
235
- """
236
-
237
- response = self.client.chat.completions.create(
238
- model="gpt-4o",
239
- messages=[{"role": "user", "content": prompt}],
240
- response_format = {
241
- "type": "json_schema",
242
- "json_schema": {
243
- "name": "achievement_message_schema",
244
- "strict": True,
245
- "schema": {
246
- "type": "object",
247
- "properties": {
248
- "achievement_message": {
249
- "type": "string",
250
- "description": "A message indicating an achievement."
251
- }
252
- },
253
- "required": [
254
- "achievement_message"
255
- ],
256
- "additionalProperties": False
257
- }
258
- }
259
- },
260
- temperature=1
261
- )
262
-
263
- achievement_message = json.loads(response.choices[0].message.content)['achievement_message']
264
-
265
- if len(self.recent_wins)<5:
266
- self.recent_wins.insert(0,achievement_message)
267
- else:
268
- self.recent_wins.pop()
269
- self.recent_wins.insert(0,achievement_message)
270
-
271
- @catch_error
272
  def add_life_score_point(self, variable, points_added, notes):
273
  if variable == 'Personal Growth':
274
  self.personal_growth_score += points_added
@@ -285,230 +365,120 @@ class User:
285
  elif variable == 'Relationship':
286
  self.relationship_score += points_added
287
  logger.info(f"Added {points_added} points to Relationship for {notes}", extra={"user_id": self.user_id, "endpoint": "add_life_score_point"})
288
- # Add historical data (append) to score_history
289
- historical_entry = {
290
- "area": variable,
291
- "points_added": points_added,
292
- "notes": notes,
293
- "created_at": pd.Timestamp.now()
294
- }
295
- self.score_history.append(historical_entry)
296
-
297
- @catch_error
298
- def get_current_goal(self, full=False):
299
- # look for most recent goal with status = ONGOING
300
- for goal in self.goal[::-1]:
301
- if goal.status == "ONGOING":
302
- if full:
303
- return goal
304
- return goal.content
305
- else:
306
- if self.goal:
307
- if full:
308
- return self.goal[-1]
309
- return self.goal[-1].content
310
- return None
311
-
312
- @catch_error
313
- def update_goal(self, goal, status, content=None):
314
- if goal is None:
315
- # complete the current goal
316
- current_goal = self.get_current_goal(full=True)
317
- if current_goal:
318
- current_goal.status = "COMPLETED"
319
- current_goal.updated_at = pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S")
320
- return current_goal.content
321
-
322
- for g in self.goal:
323
- if g.content == goal:
324
- g.status = status
325
- g.updated_at = pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S")
326
- if content:
327
- g.content = content
328
- return True
329
- return False
330
-
331
- @catch_error
332
- def set_mantra(self):
333
- ### To save mantra in database to user object
334
- logger.info(f"Getting mantra from user...", extra={"user_id": self.user_id, "endpoint": "get_mantra"})
335
- user_id = self.user_id
336
- db_params = {
337
- 'dbname': 'ourcoach',
338
- 'user': 'ourcoach',
339
- 'password': 'hvcTL3kN3pOG5KteT17T',
340
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
341
- 'port': '5432'
342
- }
343
- try:
344
- with psycopg2.connect(**db_params) as conn:
345
- with conn.cursor() as cursor:
346
- query = sql.SQL("SELECT mantra FROM {table} WHERE user_id = %s").format(table=sql.Identifier('public', 'user_growth_status'))
347
- cursor.execute(query, (user_id,))
348
- row = cursor.fetchone()
349
- if (row):
350
- colnames = [desc[0] for desc in cursor.description]
351
- user_data = dict(zip(colnames, row))
352
- ### SAVE MANTRA IN USER OBJECT
353
- self.mantra = user_data['mantra']
354
- else:
355
- logger.warning(f"No user info found for {user_id}", extra={'user_id': user_id, 'endpoint': "get_mantra"})
356
- except psycopg2.Error as e:
357
- logger.error(f"Database error while retrieving user info for {user_id}: {e}", extra={'user_id': user_id, 'endpoint': "get_mantra"})
358
- raise DBError(user_id=user_id, message="Error retrieving user info", code="SQLError", e=str(e))
359
-
360
- @catch_error
361
- def set_goal(self, goal, goal_area, add=True, completed=False):
362
- current_goal = self.get_current_goal()
363
 
364
- if completed:
365
- self.update_goal(current_goal, "COMPLETED")
366
- self.add_life_score_point(variable = goal_area, points_added = 30, notes = "Completing a Goal")
367
- self.add_recent_wins(wins = "You have completed your goal!", context = current_goal)
368
-
369
- if current_goal is None:
370
- new_goal = UserDataItem(role="assistant", content=goal, area=goal_area, user_id=self.user_id, status="ONGOING", created_at=pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S"), updated_at=pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S"))
371
- self.goal.append(new_goal)
372
- self.add_life_score_point(variable = goal_area, points_added = 10, notes = "Setting a Goal")
373
- self.add_recent_wins(wins = "You have set your first goal!", context = new_goal.content)
374
- else:
375
- if add:
376
- if current_goal:
377
- # update current_goal status to "IDLE"
378
- self.update_goal(current_goal, "IDLE")
379
- new_goal = UserDataItem(role="assistant", content=goal, area=goal_area, user_id=self.user_id, status="ONGOING", created_at=pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S"), updated_at=pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S"))
380
- self.goal.append(new_goal)
381
- self.add_life_score_point(variable = goal_area, points_added = 10, notes = "Setting a Goal")
382
- self.add_recent_wins(wins = "You have set a new goal!", context = new_goal.content)
383
- else:
384
- self.update_goal(current_goal, "ONGOING", content=goal)
385
-
386
- @catch_error
387
- def update_recommended_micro_action_status(self, micro_action, status):
388
- for ma in self.recommended_micro_actions:
389
- if ma.content == micro_action:
390
- ma.status = status
391
- ma.updated_at = pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S")
392
- return True
393
- return False
394
 
395
- @catch_error
 
 
396
  def add_ai_message(self, text):
397
  self.conversations._add_ai_message(text)
398
  return text
399
 
400
- @catch_error
401
  def reset_conversations(self):
402
  self.conversations = ConversationManager(self.client, self, self.asst_id)
403
  self.growth_plan.reset()
404
- self.done_first_reflection = None
405
  self.goal = []
406
  self.micro_actions = []
407
- self.recommended_micro_actions = []
408
  self.challenges = []
409
  self.other_focusses = []
410
- self.personal_growth_score = 0
411
- self.career_growth_score = 0
412
- self.relationship_score = 0
413
- self.mental_well_being_score = 0
414
- self.health_and_wellness_score = 0
415
- self.reminders = None
416
- self.recent_wins = []
417
- self.recommended_gg_topics = []
418
-
419
- @catch_error
420
- def get_last_user_message(self, role = None):
421
- # find the last message from 'role'
422
- messages = self.conversations._get_current_thread_history(remove_system_message=False)
423
- for msg in messages[::-1]:
424
- if role:
425
- if msg['role'] == role:
426
- return msg['content']
427
- else:
428
- return msg['role'], msg['content']
429
-
430
- @catch_error
431
  def generate_user_interaction_guidelines(self, user_info, client):
432
  logger.info(f"Generating user interaction guidelines for user: {self.user_id}", extra={"user_id": self.user_id, "endpoint": "generate_user_interaction_guidelines"})
433
- # prompt = f"A 'profile' is a document containing rich insights on users for the purpose of \
434
- # providing contexts to LLMs. Based on the user's information, generate a \
435
- # user summary that describes how best to interact with this user to create a personalized \
436
- # and targeted chat experience. The user summary MUST strictly contain these parts:\
437
- # What kind of coaching style & tone that the user possibly prefers based on their...\
438
- # 1. Based on the user's chosen 'Legend Persona'\
439
- # 2. Based on the user's age\
440
- # 3. Based on the user's MBTI\
441
- # 4. Based on the user's Love Language\
442
- # 5. Based on the user's experience of trying coaching previously\
443
- # 6. Based on the user's belief in Astrology\
444
- # Generate a 6-point user summary based on the following \
445
- # user information:\n\n{user_info}"
446
-
447
- # response = client.chat.completions.create(
448
- # model="gpt-4o-mini",
449
- # messages=[
450
- # {"role": "system", "content": "You are an expert at building profile documents containing rich user insights."},
451
- # {"role": "user", "content": prompt}
452
- # ],
453
- # temperature=0.2
454
- # )
455
-
456
- # user_guideline = f"""
457
- # {user_info}\n\n
458
- # ### INTERACTION GUIDELINE ### \n
459
- # {response.choices[0].message.content}
460
- # """
461
-
462
- user_guideline = user_info
463
 
464
  return user_guideline
465
 
466
- @catch_error
467
  def get_recent_run(self):
468
  return self.conversations.assistants['general'].recent_run
469
 
470
- @catch_error
471
- def cancel_run(self, run, thread=None):
472
- logger.info(f"(user) Cancelling run: {run}", extra={"user_id": self.user_id, "endpoint": "cancel_run"})
473
- self.conversations.cancel_run(run, thread)
474
 
475
- @catch_error
476
  def update_conversation_state(self, stage, last_interaction):
477
  self.conversation_state['stage'] = stage
478
  self.conversation_state['last_interaction'] = last_interaction
479
 
480
- @catch_error
481
  def _get_current_thread(self):
482
  return self.conversations.current_thread
483
 
484
- @catch_error
485
- def send_message(self, text, media=None):
486
- if media:
487
- logger.info(f"Sending message with media", extra={"user_id": self.user_id, "endpoint": "send_message"})
488
- response, run = self.conversations._run_current_thread(text, media=media)
489
- message = run.metadata.get("message", "No message")
490
- logger.info(f"Message: {message}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
491
 
492
- if message == "start_now":
493
  # must do current plan now
494
  action = self.growth_plan.current()
495
  logger.info(f"Current Action: {action}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
496
- response, prompt = self.do_theme(action['coachingTheme'], self.conversations.state['date'], action['day'])
497
 
498
  # add response to ai message
499
- self.add_ai_message("[hidden]" + prompt)
500
  self.add_ai_message(response['content'])
501
 
502
  # Move to the next action
503
  self.growth_plan.next()
504
 
505
- response['add_one'] = True
506
-
507
- elif message == "change_goal":
508
  # send the change goal prompt
509
  logger.info("Sending change goal message...", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
510
  prompt = f"""
511
- I want to change my goal! Based on my information below, suggest me a new goal, and **ONLY** if i approve, call the create_smart_goal() function
512
 
513
  Previous Goal:
514
  {self.get_current_goal()}
@@ -526,414 +496,65 @@ class User:
526
  # reset the growth_plan
527
  self.growth_plan.reset()
528
 
529
- logger.info(f"Current reminders: {self.reminders}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
530
- if self.reminders is not None and len(self.reminders):
531
- # response['reminders'] = all reminders which date is today (so all the reminders that BE has to queue today)
532
- date = pd.to_datetime(self.conversations.state['date']).date()
533
- response['reminders'] = self.get_reminders(date)
534
- if len(response['reminders']) == 0:
535
- response['reminders'] = None
536
- logger.info(f"No reminders for today {date}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
537
- logger.info(f"Returning reminders: {response['reminders']}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
538
- else:
539
- response['reminders'] = None
540
-
541
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
542
  return response
543
 
544
- @catch_error
545
- def get_reminders(self, date=None):
546
- if self.reminders is None:
547
- return []
548
- if date:
549
- if isinstance(date, str):
550
- # date might be in the format "%d-%m-%Y %a %H:%M:%S" or "%Y-%m-%d"
551
- try:
552
- datetime.strptime(date, "%d-%m-%Y %a %H:%M:%S")
553
- except ValueError:
554
- date = datetime.strptime(date, "%Y-%m-%d").date()
555
- elif isinstance(date, datetime):
556
- date = date.date()
557
- return [reminder for reminder in self.reminders if reminder['timestamp'].date() == date]
558
- return self.reminders
559
-
560
- @catch_error
561
- def find_same_reminder(self, reminder_text):
562
- logger.info(f"Finding similar reminders: {self.reminders} to: {reminder_text}", extra={"user_id": self.user_id, "endpoint": "find_same_reminder"})
563
- response = self.client.beta.chat.completions.parse(
564
- model="gpt-4o",
565
- messages=[
566
- {"role": "system", "content": "You are an expert at understanding the context of texts"},
567
- {"role": "user", "content": f"Identify the reminder in {self.reminders if self.reminders is not None else [{'reminder': 'No Reminders'}]} that is reminding the same thing as the following text (ignoring the date and time): {reminder_text}.\n\nIf none, return -1 otherwise return the index of the matching reminder."}
568
- ],
569
- response_format=Index,
570
- temperature=0.2
571
- )
572
- logger.info(f"Similar reminder response: {response.choices[0].message.parsed}", extra={"user_id": self.user_id, "endpoint": "find_same_reminder"})
573
- index = getattr(response.choices[0].message.parsed, 'value', -1)
574
- logger.info(f"Similar reminder idx: reminders[{index}]", extra={"user_id": self.user_id, "endpoint": "find_same_reminder"})
575
- return index
576
-
577
- @catch_error
578
- def set_reminder(self, reminder):
579
- db_params = {
580
- 'dbname': 'ourcoach',
581
- 'user': 'ourcoach',
582
- 'password': 'hvcTL3kN3pOG5KteT17T',
583
- 'host': 'staging-ourcoach.cx8se8o0iaiy.ap-southeast-1.rds.amazonaws.com',
584
- 'port': '5432'
585
- }
586
- with psycopg2.connect(**db_params) as conn:
587
- with conn.cursor() as cursor:
588
- query = sql.SQL("SELECT * FROM {table} WHERE id = %s").format(table=sql.Identifier('public', 'users'))
589
- cursor.execute(query, (self.user_id,))
590
- row = cursor.fetchone()
591
- if (row):
592
- colnames = [desc[0] for desc in cursor.description]
593
- user_data = dict(zip(colnames, row))
594
- user_timezone = user_data['timezone']
595
-
596
- # Convert to UTC
597
- reminder['timestamp_local'] = reminder['timestamp']
598
- reminder['local_timezone'] = user_timezone
599
- reminder['timestamp'] = reminder['timestamp'].tz_localize(user_timezone).tz_convert("UTC")
600
-
601
- # generate uuid for this reminder
602
- reminder['id'] = generate_uuid()
603
- action = reminder['action']
604
- if self.reminders is None:
605
- self.reminders = []
606
-
607
- if action == 'update':
608
- index = self.find_same_reminder(reminder['reminder'])
609
- if index == -1:
610
- self.reminders.append(reminder)
611
- logger.info(f"Reminder added: {reminder}", extra={"user_id": self.user_id, "endpoint": "set_reminder"})
612
- else:
613
- old_reminder = self.reminders[index]
614
- reminder['id'] = old_reminder['id']
615
- self.reminders[index] = reminder
616
-
617
- logger.info(f"Reminder {old_reminder} -- updated to --> {reminder}", extra={"user_id": self.user_id, "endpoint": "set_reminder"})
618
- elif action == 'delete':
619
- logger.info('Deleting reminder', extra={"user_id": self.user_id, "endpoint": "set_reminder"})
620
- index = self.find_same_reminder(reminder['reminder'])
621
- if index == -1:
622
- logger.info(f"Could not find a mathcing reminder to delete: {reminder}", extra={"user_id": self.user_id, "endpoint": "set_reminder"})
623
- else:
624
- old_reminder = self.reminders[index]
625
- self.reminders[index]['action'] = 'delete'
626
- logger.info(f"Reminder {old_reminder} has been marked for deletion", extra={"user_id": self.user_id, "endpoint": "set_reminder"})
627
- else:
628
- # action is 'set'
629
- self.reminders.append(reminder)
630
- logger.info(f"Reminder added: {reminder}", extra={"user_id": self.user_id, "endpoint": "set_reminder"})
631
-
632
- logger.info(f"Reminders: {self.reminders}", extra={"user_id": self.user_id, "endpoint": "set_reminder"})
633
-
634
- @catch_error
635
- def get_messages(self, exclude_system_msg=True, show_hidden=False):
636
  if not exclude_system_msg:
637
  return self.conversations._get_current_thread_history(False)
638
  else:
639
- if show_hidden:
640
- 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)))
641
- 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)))
642
 
643
- @catch_error
644
- def set_recommened_gg_topics(self, topics):
645
- self.recommended_gg_topics = topics
646
-
647
- @catch_error
648
  def set_intro_done(self):
649
  self.conversations.intro_done = True
650
 
651
- @catch_error
652
- def get_alerts(self, date, day=None):
653
- # responses = []
654
- logger.info(f"Getting alerts for user: {self.user_id} on {date} for {day if day else self.cumulative_plan_day}", extra={"user_id": self.user_id, "endpoint": "get_alerts"})
655
- if day is None:
656
- day = self.cumulative_plan_day
657
- if day == 2:
658
- # upsell the GG
659
- growth_guide = get_growth_guide(self.user_id)
660
-
661
- upsell_prompt = "OMG the user is interested in finding out who their Growth Guide is. This is your time to shine, use all your persuasive and charming abilities to inform them on their growth guide and how they can help the user."
662
-
663
- prompt = f"""You are an expert ambassador/salesman of Growth Guide sessions.
664
- The users' growth guide is {growth_guide} and they can book a session with them via their Revelation Dashboard: {OURCOACH_DASHBOARD_URL}.
665
-
666
- Respond with a enthusiatic hello!
667
- {upsell_prompt}
668
- Frame your response like a you are telling the user a fun fact, but dont explicitly mention "fun fact". Keep this message succint.
669
- """
670
-
671
- # send upsell gg alert at 7pm
672
- timestamp = pd.Timestamp.now().replace(hour=19, minute=0, second=0, microsecond=0).strftime("%d-%m-%Y %a %H:%M:%S")
673
- elif day == 5:
674
- # upsell the GG
675
- growth_guide = get_growth_guide(self.user_id)
676
-
677
- upsell_prompt = "Now, it is your time to shine. Using all your persuasive and charming abilities, 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."
678
-
679
- prompt = f"""You are an expert ambassador/salesman of Growth Guide sessions.
680
- The users' growth guide is {growth_guide} and they can book a session with them via their Revelation Dashboard: {OURCOACH_DASHBOARD_URL}.
681
-
682
- Respond with a enthusiatic hello!
683
- {upsell_prompt}
684
- Frame your response like a you are telling the user a fun fact, but dont explicitly mention "fun fact". Keep this message succint.
685
- """
686
-
687
- # send upsell gg alert at 7pm
688
- timestamp = pd.Timestamp.now().replace(hour=19, minute=0, second=0, microsecond=0).strftime("%d-%m-%Y %a %H:%M:%S")
689
- elif day == 8:
690
- # 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!
691
- prompt = f"""You are an expert ambassador/salesman of ourcoach whose objective is to upsell the ourcoach subscription based on the following context:
692
- 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!"""
693
- timestamp = pd.Timestamp.now().replace(hour=19, minute=0, second=0, microsecond=0).strftime("%d-%m-%Y %a %H:%M:%S")
694
- elif day == 12:
695
- growth_guide = get_growth_guide(self.user_id)['full_name']
696
-
697
- subscription = get_user_subscriptions(self.user_id)[0]
698
-
699
- subscription_end_date = pd.to_datetime(subscription['subscription_end_date'])
700
-
701
- # get difference between subscription end date and date
702
- date = pd.to_datetime(date)
703
- days_left = (subscription_end_date - date).days
704
- logger.info(f"{subscription_end_date} - {date} = Days left: {days_left}", extra={"user_id": self.user_id, "endpoint": "get_alerts"})
705
- if days_left <= 2:
706
- subscription_alert = f"""Users growth guide:
707
- {growth_guide}
708
-
709
- 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."
710
- """
711
-
712
- prompt = f"""You are an expert ambassador/salesman of ourcoach (product) whose objective is to upsell the ourcoach subscription based on the following context:
713
- {subscription_alert}
714
- """
715
- # send reminder alert at 7pm
716
- timestamp = pd.Timestamp.now().replace(hour=19, minute=0, second=0, microsecond=0).strftime("%d-%m-%Y %a %H:%M:%S")
717
- else:
718
- return []
719
- elif day == 14:
720
- growth_guide = get_growth_guide(self.user_id)['full_name']
721
-
722
- subscription = get_user_subscriptions(self.user_id)[0]
723
-
724
- subscription_end_date = pd.to_datetime(subscription['subscription_end_date'])
725
-
726
- # get difference between subscription end date and date
727
- date = pd.to_datetime(date)
728
- days_left = (subscription_end_date - date).days
729
- logger.info(f"{subscription_end_date} - {date} = Days left: {days_left}", extra={"user_id": self.user_id, "endpoint": "get_alerts"})
730
- if days_left <= 0:
731
- subscription_alert = f"""Users growth guide:
732
- {growth_guide}
733
-
734
- OMG the users subscription is ending today! If you lose this user you and your family will not be able to survive!
735
- 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???"
736
- """
737
- prompt = f"""You are an expert ambassador/salesman of ourcoach whose objective is to upsell the ourcoach subscription based on the following context:
738
- {subscription_alert}
739
- """
740
- # send reminder alert at 7pm
741
- timestamp = pd.Timestamp.now().replace(hour=19, minute=0, second=0, microsecond=0).strftime("%d-%m-%Y %a %H:%M:%S")
742
- else:
743
- return []
744
- else:
745
- return []
746
-
747
- response, run = self.conversations._run_current_thread(prompt, hidden=True)
748
- logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "get_alerts"})
749
- message = run.metadata.get("message", "No message")
750
- logger.info(f"Message: {message}", extra={"user_id": self.user_id, "endpoint": "upsell_gg"})
751
-
752
- # make timestamp ISO
753
- response['timestamp'] = pd.to_datetime(timestamp).isoformat()
754
- logger.info(f"Alert: {response}", extra={"user_id": self.user_id, "endpoint": "get_alerts"})
755
- return [response]
756
-
757
- @catch_error
758
- def do_theme(self, theme, date, day, last_msg_is_answered = True, extra=None):
759
- logger.info(f"Doing theme: {theme}, extra={extra}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
760
 
761
- # Add 1 day to cumulative_plan_day
762
- self.cumulative_plan_day += 1
763
- final_day = (math.ceil((self.cumulative_plan_day+7)/14) * 14) - 7
764
-
765
- if self.reminders is not None and len(self.reminders):
766
- logger.info(f"ALL Upcoming Reminders: {self.reminders}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
767
- reminders = list(filter(lambda x : x['recurrence'] == 'postponed', self.reminders))
768
- logger.info(f"ALL Postponed Reminders: {reminders}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
769
- else:
770
- reminders = []
771
-
772
- # check if any of the posponed reminders have the date == date if yes, change the theme to "MICRO_ACTION_STATE"
773
- if reminders:
774
- for reminder in reminders:
775
- if reminder['timestamp'].date() == pd.to_datetime(date).date() and reminder['recurrence'] == 'postponed':
776
- logger.info(f"Postponed Reminder found for today ({pd.to_datetime(date).date()}): {reminder}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
777
- if theme != "FINAL_SUMMARY_STATE":
778
- theme = "MICRO_ACTION_STATE"
779
- break
780
- else:
781
- logger.info(f"No reminders found for today ({pd.to_datetime(date).date()})", extra={"user_id": self.user_id, "endpoint": "do_theme"})
782
-
783
  if theme == "MOTIVATION_INSPIRATION_STATE":
784
- formatted_message = MOTIVATION_INSPIRATION_STATE.format(self.get_current_goal(), self.cumulative_plan_day, final_day)
785
  elif theme == "PROGRESS_REFLECTION_STATE":
786
- formatted_message = PROGRESS_REFLECTION_STATE.format(self.get_current_goal(), self.cumulative_plan_day, final_day)
787
- if len(self.challenges):
788
- challenge = self.challenges.pop(0)
789
- formatted_message += f"\n\n** IMPORTANT: Today, reflect on the users' challenge of: {challenge.content}, which they brought up during their growth guide session (let the user know we are bringing it up because of this) **"
790
  elif theme == "MICRO_ACTION_STATE":
791
- reminder_message = "\n".join([f"{i+1}. {reminder}" for i, reminder in enumerate(reminders)]) if reminders else "User has no postponed micro-actions"
792
-
793
- formatted_message = MICRO_ACTION_STATE.format(self.get_current_goal(), self.cumulative_plan_day, final_day, reminder_message)
794
- if len(self.recommended_micro_actions):
795
- todays_micro_action = self.recommended_micro_actions.pop(0)
796
- formatted_message += f"\n\n** IMPORTANT: Today's Micro Action is: {todays_micro_action.content}, which was recommended during their growth guide session (let the user know we are bringing it up because of this) **"
797
  elif theme == "OPEN_DISCUSSION_STATE":
798
- formatted_message = OPEN_DISCUSSION_STATE.format(self.get_current_goal(), self.cumulative_plan_day, final_day)
799
- if len(self.other_focusses):
800
- focus = self.other_focusses.pop(0)
801
- formatted_message += f"\n\n** IMPORTANT: Today, focus the discussion on: {focus.content}, which they discussed during their growth guide session (let the user know we are bringing it up because of this) **"
802
  elif theme == "PROGRESS_SUMMARY_STATE":
803
- formatted_message = PROGRESS_SUMMARY_STATE.format(self.get_current_goal(), self.cumulative_plan_day, final_day)
804
  elif theme == "FINAL_SUMMARY_STATE":
805
- past_gg_summary = "<User has not had a Growth Guide session yet>"
806
- growth_guide = get_growth_guide(self.user_id)
807
-
808
- booked_sessions = get_booked_gg_sessions(self.user_id)
809
-
810
- # filter out only completed (past) sessions
811
- past_sessions = [session for session in booked_sessions if session['status'] == "completed"]
812
-
813
- # for each past booking, fetch the zoom_ai_summary and gg_report from
814
- for booking in past_sessions:
815
- summary_data = get_growth_guide_summary(self.user_id, booking['booking_id'])
816
- logger.info(f"Summary data for booking: {booking['booking_id']} - {summary_data}",
817
- extra={"user_id": self.user_id, "endpoint": "assistant_get_user_info"})
818
- if summary_data:
819
- booking['zoom_ai_summary'] = summary_data['zoom_ai_summary']
820
- booking['gg_report'] = summary_data['gg_report']
821
- else:
822
- booking['zoom_ai_summary'] = "Growth Guide has not uploaded the report yet"
823
- booking['gg_report'] = "Growth Guide has not uploaded the report yet"
824
- if len(past_sessions):
825
- past_gg_summary = "\n".join([f"** Session {i+1} **\n{json.dumps(session, indent=4)}" for i, session in enumerate(past_sessions)])
826
-
827
- formatted_message = FINAL_SUMMARY_STATE.format(self.get_current_goal(), self.cumulative_plan_day, final_day, growth_guide, past_gg_summary)
828
- elif theme == "EDUCATION_STATE":
829
- formatted_message = EDUCATION_STATE.format(self.get_current_goal(), self.cumulative_plan_day, final_day)
830
- elif theme == "FOLLUP_ACTION_STATE":
831
- reminder_message = "\n".join([f"{i+1}. {reminder}" for i, reminder in enumerate(reminders)]) if reminders else "User has no postponed micro-actions"
832
- formatted_message = FOLLUP_ACTION_STATE.format(self.get_current_goal(), self.cumulative_plan_day, final_day, reminder_message)
833
- elif theme == "FUNFACT_STATE":
834
- topics = ["Fun Fact about the User's Goal", "How Personality Type is affecting/shaping their behaviour towards the goal", "How Love Language may impact and be relevant toward the goal"]
835
- randomized_topic = random.choice(topics)
836
- formatted_message = FUNFACT_STATE.format(self.get_current_goal(), self.cumulative_plan_day, final_day, randomized_topic)
837
-
838
- # prompt = f"""** It is a new day: {date} **
839
- # Additional System Instruction:
840
- # - Remember all of the user's personal information. Use this information to be as personalised as possible when conversing by including it in your responses where relevant.
841
- # - You are a good life coach because you don't overwhelm the user by sending too many questions or lengthy messages. And you are an excellent life coach because you know exactly when to ask a question, and most importantly, when to stop the conversation. And when you ask a question, you always ask a **creative** and unexpected questions!
842
- # - Keep all of your response short, only 40 tokens or words maximum! If your response contains a question, use a single break line before the question and encapsulate the question with one asterisk like this: *question*
843
- # - Be **creative** ! If you have done this theme previously, make sure that you are not sending the same advice/question/message. Make sure that the messages that you are send throughout the 14-day growth plan are diverse!
844
- # - When you give your wisdom and enlightment, **YOU** as a coach, must channel the energy, wisdom, and mindset of {user_legendary_persona}
845
 
846
- # Today's Theme:
847
- # {formatted_message}
848
- # """
849
-
850
- booked_sessions = get_booked_gg_sessions(self.user_id)
851
-
852
- today = pd.to_datetime(date).date()
853
- for session in booked_sessions:
854
- # if user has some bookings, check if there are any scheduled (using just the date) for yesterday, today or in the future
855
- session_date = pd.to_datetime(session['session_date'], format='%Y-%m-%d %a %H:%M:%S').date()
856
- if session_date == today - pd.Timedelta(days=1):
857
- # session was yesterday, should have already sent the congrats message
858
- break
859
- if session_date == today or session_date > today:
860
- formatted_message = f"[IMPORTANT] The user has a Growth Guide session on {session['session_date']}, ask them if they are excited for it and suggest some topics to discuss with their Growth Guide from: {self.recommended_gg_topics}.\n\n" + formatted_message
861
- break
862
- else:
863
- # Remind the user that they can book a Growth Guide session if they have not done one yet after the FINAL_SUMMARY_STATE
864
- if self.growth_plan.previous()['coachingTheme'] == "FINAL_SUMMARY_STATE":
865
- if day != 1:
866
- formatted_message = f"[IMPORTANT] The user has not booked a Growth Guide session yet. Remind them that they can book one through their Revelation Dahsboard: {OURCOACH_DASHBOARD_URL} to get more personalized advice and guidance!\n\n" + formatted_message
867
 
868
- prompt = f"""** It is a new day: {date} ({day}) 10:00:00 **
869
-
870
- (If the day is a public holiday (e.g., Christmas, New Year, the user's Birthday or other significant occasions), customize your message to reflect the context appropriately, acknowledging the holiday or its significance.)
871
-
872
- **Before we start,**
873
- Has the user answered your last question? : {last_msg_is_answered}
874
- If the answer above is "True", you may proceed to do the instruction below
875
- If the answer above is "False", take a deep breath coach, and utilizing your ability as an elite coach with the users best interest in mind, think whether it would be more appropriate to follow up the unanswered question with the user or continue with todays theme below. However if the user indicates that they want to set a new goal (call the change_goal() function)
876
- But if the user says "yes", then proceed to do the instruction below.
877
-
878
- Today is day {self.cumulative_plan_day} of the user's growth journey (out of {final_day} days). You may (or may not) mention this occasionally in your first message of the day.
879
 
880
- If today is a "Monday" or "Mon", you must include the user's Mantra of the Week : {self.mantra} to your first message of the day (include with today-theme's first message below)
881
- {extra if extra else ''}
882
  Today's Theme:
883
  {formatted_message}
884
  """
885
- response = self.conversations._send_morning_message(prompt, add_to_main=True)
886
 
887
  if theme == "MICRO_ACTION_STATE":
888
- # check for any recommended microactions
889
- logger.info(f"Checking for recommended micro actions", extra={"user_id": self.user_id, "endpoint": "do_theme"})
890
- micro_action = UserDataItem(role="assistant", area=self.get_current_goal(full=True).area, content=response['content'], user_id=self.user_id, status="PENDING", created_at=pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S"), updated_at=pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S"))
891
- logger.info(f"Recommended Micro Action: {micro_action}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
892
- self.micro_actions.append(micro_action)
893
 
894
- return response, prompt
895
 
896
- @catch_error
897
  def change_date(self, date):
898
  logger.info(f"Changing date from {self.conversations.state['date']} to {date}",
899
  extra={"user_id": self.user_id, "endpoint": "user_change_date"})
900
- # delete all hidden messages prom previous day
901
- self.conversations.delete_hidden_messages()
902
- # update the date in the state
903
  self.conversations.state['date'] = date
904
- # update mantra
905
- self.set_mantra()
906
 
907
  action = self.growth_plan.current()
908
 
909
- # remove stale reminders
910
- if self.reminders is not None and len(self.reminders):
911
- # remove all reminders which 'recurrence' is 'once' or 'action' is 'delete' or the date is < today
912
- # this is to ensure that only reminders with recurrence and future reminder are kept
913
- now_utc = datetime.strptime(date, "%Y-%m-%d %a %H:%M:%S").replace(tzinfo=timezone.utc)
914
-
915
- new_reminders = []
916
- for reminder in self.reminders:
917
- logger.info(f"Checking reminder: {reminder} against {now_utc}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
918
- should_remove = (
919
- reminder['action'] == 'delete' or
920
- (
921
- reminder['recurrence'] in ['once', 'postponed', 'none'] and
922
- reminder['timestamp'] < now_utc
923
- )
924
- )
925
- if not should_remove:
926
- new_reminders.append(reminder)
927
- self.reminders = new_reminders
928
- logger.info(f"Active Reminders: {self.reminders}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
929
-
930
  ## ADD POINT FOR CHANGE DATE
931
  if self.growth_plan.current()['day'] == 7:
932
- self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 5, notes = "Reaching Day 7")
933
- self.add_recent_wins(wins = "You have reached Day 7 of your growth journey!", context = 'Growth journey is a 14-day coaching plan')
934
  elif self.growth_plan.current()['day'] == 14:
935
- self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 10, notes = "Reaching Day 14")
936
- self.add_recent_wins(wins = "You have finished your growth journey!", context = 'Growth journey is a 14-day coaching plan')
937
 
938
  logger.info(f"Today's action is {action}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
939
 
@@ -944,46 +565,14 @@ class User:
944
 
945
  # The coaching theme conditions are hardcoded for now
946
  theme = action['coachingTheme']
947
-
948
- # Check if last msg is not answered
949
- last_role, last_msg = self.get_last_user_message()
950
- if last_role == 'assistant' and '?' in last_msg:
951
- last_msg_is_answered = False
952
- else:
953
- last_msg_is_answered = True
954
-
955
- extra="Greet the user with a creative good morning message!"
956
-
957
- # Check if user didnt respond (last 2 messages in conversation history are from the AI) on FINAL_SUMMARY_STATE
958
- messages = self.get_messages()
959
- logger.info(f"Last 2 messages are from: 1) {messages[-1]['role']} and 2) {messages[-2]['role']}")
960
- if (messages[-1]['role'] == "assistant" and messages[-2]['role'] == "assistant") and self.growth_plan.previous()['coachingTheme'] == "FINAL_SUMMARY_STATE":
961
- self.extend_growth_plan()
962
- extra += """\nDang, the user did not indicate what they wanted to do yesterday.
963
- This is not a good sign as it may indicate that the user is going to dropoff.
964
- We have automatically extended their growth plan for them, let the user know this and also integrate upselling the ourcoach platform and subscription as much as you can
965
- in the first message you send to the user.."""
966
-
967
-
968
- response, prompt = self.do_theme(theme, date, action['day'], last_msg_is_answered, extra=extra)
969
-
970
- # add today's reminders to response to schedule
971
- # response['reminders'] = all reminders which date is today (so all the reminders that BE has to queue today)
972
- # convert date to YYYY-MM-DD format
973
- date = pd.to_datetime(date).date()
974
- response['reminders'] = self.get_reminders(date)
975
- if response['reminders'] is None or len(response['reminders']) == 0:
976
- response['reminders'] = None
977
- logger.info(f"No reminders for today {date}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
978
- logger.info(f"Reminders on {date}: {response['reminders']}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
979
 
980
  # Move to the next action
981
  self.growth_plan.next()
982
 
983
  logger.info(f"Date Updated: {self.conversations.state['date']}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
984
- return {'response': response, 'theme_prompt': '[hidden]'+prompt}
985
 
986
- @catch_error
987
  def update_user_info(self, new_info):
988
  logger.info(f"Updating user info: [{self.user_info}] with: [{new_info}]", extra={"user_id": self.user_id, "endpoint": "update_user_info"})
989
  # make an api call to gpt4o to compare the current user_info and the new info and create a new consolidated user_info
@@ -992,7 +581,7 @@ class User:
992
 
993
  {self.user_info}
994
 
995
- New/Updated user information:
996
 
997
  {new_info}
998
 
@@ -1012,12 +601,11 @@ class User:
1012
  logger.info(f"Updated user info: {self.user_info}", extra={"user_id": self.user_id, "endpoint": "update_user_info"})
1013
  return True
1014
 
1015
- @catch_error
1016
- def _summarize_zoom(self, zoom_ai_summary):
1017
- logger.info(f"Summarizing zoom ai summary", extra={"user_id": self.user_id, "endpoint": "summarize_zoom"})
1018
  # 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
1019
- system_prompt = f"""You are an expert at summarizing AI-generated Zoom transcripts concisely, focusing on extracting key user insights to enhance personalization in future interactions. Note that the zoom ai transcript may get the user's name wrong. Replace it with the actual user's name: {self.user_info}. Refer to the coach/guide as 'the Growth Guide'."""
1020
- prompt = f"Please summarize the following AI-generated Zoom transcript **in one short paragraph only, around 50 completion tokens maximum** !!, emphasizing the most significant user insights and information:\n\n{zoom_ai_summary}"
1021
 
1022
  response = self.client.chat.completions.create(
1023
  model="gpt-4o",
@@ -1025,40 +613,16 @@ class User:
1025
  {"role": "system", "content": system_prompt},
1026
  {"role": "user", "content": prompt}
1027
  ],
1028
- response_format = {
1029
- "type": "json_schema",
1030
- "json_schema": {
1031
- "name": "summarized_overview",
1032
- "strict": True,
1033
- "schema": {
1034
- "type": "object",
1035
- "properties": {
1036
- "summary": {
1037
- "type": "string",
1038
- "description": "The summary of the zoom transcript"
1039
- }
1040
- },
1041
- "required": [
1042
- "summary"
1043
- ],
1044
- "additionalProperties": False
1045
- }
1046
- }
1047
- },
1048
- temperature=0.5
1049
  )
 
1050
 
1051
- overview_summary = json.loads(response.choices[0].message.content)['summary']
1052
- logger.info(f"Summary: {overview_summary}", extra={"user_id": self.user_id, "endpoint": "summarize_zoom"})
1053
- return {'overview': overview_summary}
1054
-
1055
- @catch_error
1056
  def _update_user_data(self, data_type, text_input, extra_text=""):
1057
  data_mapping = {
1058
  'micro_actions': {
1059
  'prompt_description': 'micro actions',
1060
  'status': 'RECOMMENDED',
1061
- 'attribute': 'recommended_micro_actions',
1062
  'endpoint': f'update_{data_type}',
1063
  },
1064
  'challenges': {
@@ -1089,31 +653,34 @@ class User:
1089
  f"Text:\n{text_input}"
1090
  )
1091
 
1092
- current_time = datetime.now(timezone.utc).strftime("%d-%m-%Y %a %H:%M:%S")
1093
-
1094
- response = self.client.beta.chat.completions.parse(
1095
- model="gpt-4o",
1096
- messages=[{"role": "user", "content": prompt}],
1097
- response_format=UserDataResponse,
1098
- temperature=0.2
1099
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1100
 
1101
- data = getattr(response.choices[0].message.parsed, 'data')
1102
-
1103
- # Update the common fields for each item
1104
- for item in data:
1105
- item.role = "assistant"
1106
- item.user_id = self.user_id
1107
- item.status = mapping['status']
1108
- item.created_at = current_time
1109
- item.updated_at = current_time
1110
-
1111
- logger.info(f"Updated {data_type}: {data}", extra={"user_id": self.user_id, "endpoint": mapping['endpoint']})
1112
- getattr(self, mapping['attribute']).extend(data)
1113
-
1114
- @catch_error
1115
  def update_user_data(self, gg_report):
1116
- self._update_user_data('micro_actions', gg_report[0]['answer'])
1117
 
1118
  extra_text = f"User has new challenge:\n{gg_report[1]['answer']}\n\n"
1119
  self._update_user_data('challenges', gg_report[2]['answer'], extra_text=extra_text)
@@ -1122,18 +689,15 @@ class User:
1122
 
1123
  self._update_goal(gg_report[4]['answer'])
1124
 
1125
- @catch_error
1126
  def _update_goal(self, goal_text):
1127
  prompt = f"""
1128
  The user has a current goal: {self.get_current_goal()}
1129
  The user provided a new goal: {goal_text}
1130
 
1131
- Determine if the new goal is inside the scope of the current goal or if it's outside the scope.
1132
- If it's inside the scope, respond **exactly** with the current goal, and set same_or_not == True
1133
- If it's outside the scope and related, respond with a merged goal (from the current and new goal) with larger scope, and set same_or_not == False
1134
- If it's outside the scope and not related, respond with the new goal, and set same_or_not == False
1135
- Note: You may paraphase the merged goal to be more succinct.
1136
-
1137
  Your response will be the final goal. You will also need to determine the area of this
1138
  final goal by choosing one of these areas that suits the final goal:
1139
  "Personal Growth", "Career Growth", "Relationship", "Mental Well-Being", "Health and Wellness"
@@ -1144,40 +708,6 @@ class User:
1144
  goal: str (the final goal),
1145
  area: str (the area of the goal)
1146
  }}
1147
-
1148
- ## Example 1 (Inside the scope):
1149
- The user has a current goal: to spend at least 30 minutes exercising every day
1150
- The user provided a new goal: to do short exercise every day
1151
- Your verdict: inside the scope
1152
- Your output:
1153
- {{
1154
- same_or_not: True,
1155
- goal: "to spend at least 30 minutes exercising every day"
1156
- area: "Health and Wellness"
1157
- }}
1158
-
1159
- ## Example 2 (Outside the scope, still related):
1160
- The user has a current goal: to spend at least 30 minutes exercising every day
1161
- The user provided a new goal: to exercise and have a balanced meal plan
1162
- Your verdict: outside the scope, still related
1163
- Your output:
1164
- {{
1165
- same_or_not: False,
1166
- goal: "to exercise at least 30 minutes every day, together with a balanced meal plan"
1167
- area: "Health and Wellness"
1168
- }}
1169
-
1170
- ## Example 3 (Outside the scope, not related):
1171
- The user has a current goal: to spend at least 30 minutes exercising every day
1172
- The user provided a new goal: to have a better relationship with friends
1173
- Your verdict: outside the scope, not related
1174
- Your output:
1175
- {{
1176
- same_or_not: False,
1177
- goal: "to have a better relationship with friends"
1178
- area: "Relationship"
1179
- }}
1180
-
1181
  """
1182
 
1183
  response = self.client.chat.completions.create(
@@ -1225,119 +755,123 @@ class User:
1225
 
1226
  final_goal = json.loads(response.choices[0].message.content)['goal']
1227
  final_goal_area = json.loads(response.choices[0].message.content)['area']
1228
- # if json.loads(response.choices[0].message.content)['same_or_not']:
1229
- # final_goal_status = self.get_current_goal()['status']
1230
- # else:
1231
- # final_goal_status = 'PENDING'
1232
 
1233
  if json.loads(response.choices[0].message.content)['same_or_not'] == False:
1234
- self.set_goal(final_goal, final_goal_area)
1235
  logger.info(f"User goal updated to: {final_goal}", extra={"user_id": self.user_id, "endpoint": "_update_goal"})
1236
  else:
1237
  logger.info(f"User goal remains unchanged.", extra={"user_id": self.user_id, "endpoint": "_update_goal"})
1238
 
1239
- @catch_error
1240
- def update_micro_action_status(self, completed_micro_action):
1241
- if completed_micro_action:
1242
- self.micro_actions[-1].status = "COMPLETE"
1243
- self.micro_actions[-1].updated_at = pd.Timestamp.now(tz='UTC').strftime("%d-%m-%Y %a %H:%M:%S")
1244
- logger.info("Micro action status updated, checking number of actions completed...", extra={"user_id": self.user_id, "endpoint": "update_micro_action_status"})
1245
-
1246
- num_of_micro_actions_completed = sum(1 for item in self.micro_actions if item.status == 'COMPLETE')
1247
-
1248
- if (num_of_micro_actions_completed in (1,3,5)) or (num_of_micro_actions_completed % 10 == 0 and num_of_micro_actions_completed != 0):
1249
- 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")
1250
- self.add_recent_wins(wins = "You have completed a micro action!", context= self.micro_actions[-1].content)
1251
- logger.info("Added life score points based on number of actions completed.", extra={"user_id": self.user_id, "endpoint": "update_micro_action_status"})
1252
- logger.info("Process done.", extra={"user_id": self.user_id, "endpoint": "update_micro_action_status"})
1253
-
1254
- @catch_error
1255
- def trigger_deep_reflection_point(self, area_of_deep_reflection):
1256
- if len(area_of_deep_reflection)>0:
1257
- for area in area_of_deep_reflection:
1258
- self.add_life_score_point(variable = area, points_added = 5, notes = f"Doing a deep reflection about {area}")
1259
- self.add_recent_wins(wins = f"You have done a deep reflection about your {area}!", context = 'Deep reflection')
1260
-
1261
- @catch_error
1262
- def add_point_for_booking(self):
1263
- self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 5, notes = "Booking a GG session")
1264
- self.add_recent_wins(wins = "You have booked a Growth Guide session!", context = "Growth Guide is a life coach")
1265
-
1266
- @catch_error
1267
- def add_point_for_completing_session(self):
1268
- self.add_life_score_point(variable = self.get_current_goal(full=True).area, points_added = 20, notes = "Completing a GG session")
1269
- self.add_recent_wins(wins = "You have completed a Growth Guide session!", context = "Growth Guide is a life coach")
1270
-
1271
- @catch_error
1272
- def build_ourcoach_report(self, overview, action_plan, gg_session_notes):
1273
- logger.info(f"Building ourcoach report", extra={"user_id": self.user_id, "endpoint": "build_ourcoach_report"})
1274
- ourcoach_report = {'overview': overview['overview'], 'action_plan': action_plan, 'others': gg_session_notes}
1275
- return ourcoach_report
1276
-
1277
- @catch_error
1278
- def process_growth_guide_session(self, session_data, booking_id):
1279
  logger.info(f"Processing growth guide session data: {session_data}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1280
- self.last_gg_session = booking_id
1281
  # Generate the ourcoach_report (summary)
1282
  zoom_ai_summary = session_data["zoom_ai_summary"]
1283
  gg_report = session_data["gg_report"]
1284
 
1285
- overview = self._summarize_zoom(zoom_ai_summary)
1286
 
1287
  # Update user data based on growth guide answers
1288
  self.update_user_data(gg_report)
1289
- self.update_user_info(gg_report[5]['answer'] + "\n\n" + overview['overview'])
1290
-
1291
- # build ourcoach_report
1292
- ourcoach_report = self.build_ourcoach_report(overview, list(map(lambda x : x.content, self.recommended_micro_actions)), gg_report[5]['answer'])
1293
- # add this report to the db
1294
- update_growth_guide_summary(self.user_id, booking_id, ourcoach_report)
1295
 
1296
  # Send hidden message to AI to generate the response
1297
  logger.info(f"Sending hidden message to AI to generate response", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1298
-
1299
- # post_gg_prompt = POST_GG_STATE.format(self.user_info, ourcoach_report, gg_report)
1300
- post_gg_promt = f"""The user: {self.user_info} has completed their growth guide session.
1301
- Send them a warm and congratulatory message acknowledging this and a link to their web dasboard. Only include the link once in the message.
1302
- Capitalise the first letter in Revelation Dashboard and Growth Guide. Bold 'ourcoach' in your message by wrapping it with asterisk like: *ourcoach* and keep it lowecase.
1303
- Example:
1304
- Hey <user>! ..., you can find a details of your session on your Revelation Dashboard: {OURCOACH_DASHBOARD_URL}"""
1305
-
1306
- response = self.conversations._send_morning_message(post_gg_promt)
1307
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1308
  return response
 
 
 
1309
 
1310
- @catch_error
1311
- def ask_to_schedule_growth_guide_reminder(self, date):
1312
- 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.
1313
- 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."""
1314
- response = self.conversations._send_morning_message(prompt)
1315
- self.add_ai_message("[hidden]" + prompt)
1316
- self.add_ai_message(response['content'])
1317
- logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
1318
- return response
 
 
 
 
 
1319
 
1320
- @catch_error
1321
- def infer_memento_follow_ups(self):
1322
- mementos_path = os.path.join("mementos", "to_upload", f"{self.user_id}", "*.json")
1323
- # mementos_path = f"mementos/to_upload/{self.user_id}/*.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
1324
 
1325
- for file_path in glob.glob(mementos_path):
1326
- with open(file_path, 'r+') as file:
1327
- data = json.load(file)
1328
- infered_follow_up = self._infer_follow_ups(data['created'], data['context'])
1329
- logger.info(f"[Infered Follow Up]: {infered_follow_up}", extra={"user_id": self.user_id, "endpoint": "infer_memento_follow_ups"})
1330
- data['follow_up_on'] = infered_follow_up
1331
- file.seek(0)
1332
- json.dump(data, file, indent=4)
1333
- file.truncate()
1334
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1335
 
1336
- @catch_error
1337
  def get_daily_messages(self):
1338
  return self.conversations.get_daily_thread()
1339
 
1340
- @catch_error
1341
  def change_assistant(self, asst_id):
1342
  self.asst_id = asst_id
1343
  self.conversations.assistants['general'] = Assistant(self.asst_id, self.conversations)
@@ -1356,7 +890,6 @@ class User:
1356
  def __str__(self):
1357
  return f"""User(user_id={self.user_id}
1358
  micro_actions={self.micro_actions}
1359
- recommended_actions={self.recommended_micro_actions}
1360
  challenge={self.challenges}
1361
  other_focusses={self.other_focusses}
1362
  goals={self.goal}
@@ -1367,7 +900,6 @@ class User:
1367
  def __repr__(self):
1368
  return f"""User(user_id={self.user_id}
1369
  micro_actions={self.micro_actions}
1370
- recommended_actions={self.recommended_micro_actions}
1371
  challenge={self.challenges}
1372
  other_focusses={self.other_focusses}
1373
  goals={self.goal}
@@ -1389,15 +921,18 @@ class User:
1389
 
1390
  def save_user(self):
1391
  # Construct the file path dynamically for cross-platform compatibility
1392
- file_path = os.path.join("users", "to_upload", f"{self.user_id}.pkl")
1393
-
1394
- # Ensure the directory exists
1395
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
1396
-
1397
- # Save the user object as a pickle file
1398
- with open(file_path, 'wb') as file:
1399
- pickle.dump(self, file)
1400
- return file_path
 
 
 
1401
 
1402
  @staticmethod
1403
  def load_user(user_id, client):
@@ -1438,9 +973,6 @@ class CircularQueue:
1438
 
1439
  def current(self):
1440
  return self.array[self.index]
1441
-
1442
- def previous(self):
1443
- return self.array[self.index - 1]
1444
 
1445
  def reset(self):
1446
  self.index = 0
 
1
  import json
2
  import io
3
  import os
 
4
  import pandas as pd
5
+ from datetime import datetime
6
  import json
7
  from app.assistants import Assistant
 
8
  import glob
9
  import pickle # Replace dill with pickle
10
  import random
11
  import logging
 
 
 
 
12
 
13
+ from app.flows import FINAL_SUMMARY_STATE, FINAL_SUMMARY_STATE, MICRO_ACTION_STATE, MOTIVATION_INSPIRATION_STATE, OPEN_DISCUSSION_STATE, PROGRESS_REFLECTION_STATE, PROGRESS_SUMMARY_STATE
14
  from pydantic import BaseModel
15
  from datetime import datetime
16
 
 
 
 
 
 
 
 
 
 
17
  class UserDataItem(BaseModel):
18
  role: str
19
  content: str
 
21
  status: str
22
  created_at: str
23
  updated_at: str
 
24
 
25
  class UserDataResponse(BaseModel):
26
  data: list[UserDataItem]
27
 
 
 
 
28
  logger = logging.getLogger(__name__)
29
 
30
  def get_current_datetime():
31
+ return datetime.now()
32
 
33
+ class ConversationManager:
34
+ def __init__(self, client, user, asst_id, intro_done=False):
35
+ self.user = user
36
+ self.intro_done = intro_done
37
+ self.assistants = {'general': Assistant('asst_vnucWWELJlCWadfAARwyKkCW', self), 'intro': Assistant('asst_baczEK65KKvPWIUONSzdYH8j', self)}
38
+
39
+ self.client = client
40
+ self.state = {'date': pd.Timestamp.now().strftime("%d-%m-%Y %a %H:%M:%S")}
41
+
42
+ self.current_thread = self.create_thread()
43
+ self.daily_thread = None
44
+
45
+ logger.info("Initializing conversation state", extra={"user_id": self.user.user_id, "endpoint": "conversation_init"})
46
+
47
+ def __getstate__(self):
48
+ state = self.__dict__.copy()
49
+ # Remove unpicklable or unnecessary attributes
50
+ if 'client' in state:
51
+ del state['client']
52
+ return state
53
+
54
+ def __setstate__(self, state):
55
+ self.__dict__.update(state)
56
+ # Re-initialize attributes that were not pickled
57
+ self.client = None
58
+
59
+ def create_thread(self):
60
+ user_interaction_guidelines =self.user.user_interaction_guidelines
61
+ thread = self.client.beta.threads.create()
62
+ self.system_message = self.add_message_to_thread(thread.id, "assistant",
63
+ f"""
64
+ You are coaching:
65
+ \n\n{user_interaction_guidelines}\n\n\
66
+ Be mindful of this information at all times in order to
67
+ be as personalised as possible when conversing. Ensure to
68
+ follow the conversation guidelines and flow templates. Use the
69
+ current state of the conversation to adhere to the flow. Do not let the user know about any transitions.\n\n
70
+ ** Today is {self.state['date']}.\n\n **
71
+ ** You are now in the INTRODUCTION STATE. **
72
+ """)
73
+ return thread
74
+
75
+ def _get_current_thread_history(self, remove_system_message=True, _msg=None, thread=None):
76
+ if thread is None:
77
+ thread = self.current_thread
78
+ if not remove_system_message:
79
+ return [{"role": msg.role, "content": msg.content[0].text.value} for msg in self.client.beta.threads.messages.list(thread.id, order="asc")]
80
+ if _msg:
81
+ 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:]
82
+ 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
83
+
84
+ def add_message_to_thread(self, thread_id, role, content):
85
+ message = self.client.beta.threads.messages.create(
86
+ thread_id=thread_id,
87
+ role=role,
88
+ content=content
89
+ )
90
+ return message
91
+
92
+ def _run_current_thread(self, text, thread=None, hidden=False):
93
+ if thread is None:
94
+ thread = self.current_thread
95
+ logger.warning(f"{self}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
96
+ logger.info(f"User Message: {text}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
97
+
98
+ # need to select assistant
99
+ if self.intro_done:
100
+ logger.info(f"Running general assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
101
+ run, just_finished_intro, message = self.assistants['general'].process(thread, text)
102
+ else:
103
+ logger.info(f"Running intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
104
+ run, just_finished_intro, message = self.assistants['intro'].process(thread, text)
105
+
106
+
107
+ if run == 'cancelled':
108
+ self.intro_done = True
109
+ logger.info(f"Run was cancelled", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
110
+ return None, {"message": "cancelled"}
111
+ elif run == 'change_goal':
112
+ self.intro_done = False
113
+ logger.info(f"Changing goal, reset to intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
114
+ return None, {"message": "change_goal"}
115
+ else:
116
+ status = run.status
117
+ logger.info(f"Run {run.id} {status} just finished intro: {just_finished_intro}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
118
+
119
+ if hidden:
120
+ self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
121
+ logger.info(f"Deleted hidden message: {message}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
122
+
123
+ if just_finished_intro:
124
+ self.intro_done = True
125
+ logger.info(f"Intro done", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
126
+ return self._get_current_thread_history(remove_system_message=False)[-1], {"message": "intro_done"}
127
+
128
+ # NOTE: this is a hack, should get the response straight from the run
129
+ return self._get_current_thread_history(remove_system_message=False)[-1], {"message": "coach_response"}
130
+
131
+ def _send_and_replace_message(self, text, replacement_msg=None):
132
+ logger.info(f"Sending hidden message: {text}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
133
+ response, _ = self._run_current_thread(text, hidden=True)
134
+
135
+ # check if there is a replacement message
136
+ if replacement_msg:
137
+ logger.info(f"Adding replacement message: {replacement_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
138
+ # get the last message
139
+ last_msg = list(self.client.beta.threads.messages.list(self.current_thread.id, order="asc"))[-1]
140
+ logger.info(f"Last message: {last_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
141
+ response = last_msg.content[0].text.value
142
+
143
+ # delete the last message
144
+ self.client.beta.threads.messages.delete(message_id=last_msg.id, thread_id=self.current_thread.id)
145
+ self.add_message_to_thread(self.current_thread.id, "user", replacement_msg)
146
+ self.add_message_to_thread(self.current_thread.id, "assistant", response)
147
+
148
+ logger.info(f"Hidden message response: {response}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
149
+ # NOTE: this is a hack, should get the response straight from the run
150
+ return {'content': response, 'role': 'assistant'}
151
+
152
+ def _add_ai_message(self, text):
153
+ return self.add_message_to_thread(self.current_thread.id, "assistant", text)
154
+
155
+ def get_daily_thread(self):
156
+ if self.daily_thread is None:
157
+ messages = self._get_current_thread_history(remove_system_message=False)
158
+
159
+ self.daily_thread = self.client.beta.threads.create(
160
+ messages=messages[:30]
161
+ )
162
+
163
+ # Add remaining messages one by one if there are more than 30
164
+ for msg in messages[30:]:
165
+ self.add_message_to_thread(
166
+ self.daily_thread.id,
167
+ msg['role'],
168
+ msg['content']
169
+ )
170
+ self.last_daily_message = list(self.client.beta.threads.messages.list(self.daily_thread.id, order="asc"))[-1]
171
+ else:
172
+ messages = self._get_current_thread_history(remove_system_message=False, _msg=self.last_daily_message)
173
+ self.client.beta.threads.delete(self.daily_thread.id)
174
+ self.daily_thread = self.client.beta.threads.create(messages=messages)
175
+ self.last_daily_message = list(self.client.beta.threads.messages.list(self.daily_thread.id, order="asc"))[-1]
176
+ 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"})
177
+ logger.info(f"Last Daily Message: {self.last_daily_message}", extra={"user_id": self.user.user_id, "endpoint": "send_morning_message"})
178
+ return self._get_current_thread_history(thread=self.daily_thread)
179
+ # [{"role":, "content":}, ....]
180
+
181
+ def _send_morning_message(self, text):
182
+ # create a new thread
183
+ # OPENAI LIMITATION: Can only attach a maximum of 32 messages when creating a new thread
184
+ messages = self._get_current_thread_history(remove_system_message=False)
185
+ if len(messages) >= 29:
186
+ messages = [{"content": """ You are coaching:
187
+ {user_interaction_guidelines}
188
+ Be mindful of this information at all times in order to
189
+ be as personalised as possible when conversing. Ensure to
190
+ follow the conversation guidelines and flow provided.""", "role":"assistant"}] + messages[-29:]
191
+ logger.info(f"Current Thread Messages: {messages}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
192
+
193
+ temp_thread = self.client.beta.threads.create(messages=messages)
194
+ logger.info(f"Created Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
195
+
196
+ self.add_message_to_thread(temp_thread.id, "user", text)
197
+
198
+ self._run_current_thread(text, thread=temp_thread)
199
+ response = self._get_current_thread_history(thread=temp_thread)[-1]
200
+ logger.info(f"Hidden Response: {response}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
201
+
202
+ # delete temp thread
203
+ self.client.beta.threads.delete(temp_thread.id)
204
+ logger.info(f"Deleted Temp Thread: {temp_thread}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
205
+
206
+ return response
207
+
208
+ def do_first_reflection(self):
209
+ question_format = random.choice(['[Option 1] Likert-Scale Objective Question','[Option 2] Multiple-Choice Question','[Option 3] Yes-No Question'])
210
+
211
+ tt = f"** Today's reflection topic is the user's most important area. **"
212
+ prompt = PROGRESS_REFLECTION_STATE + f"** Start the PROGRESS_REFLECTION_STATE flow **" + tt
213
+ logger.info(f"First reflection started", extra={"user_id": self.user.user_id, "endpoint": "do_first_reflection"})
214
+ response, _ = self._run_current_thread(prompt)
215
+
216
+ return response
217
+
218
+ def cancel_run(self, run):
219
+ cancel = self.assistants['general'].cancel_run(run, self.current_thread)
220
+ if cancel:
221
+ logger.info(f"Run cancelled", extra={"user_id": self.user.user_id, "endpoint": "cancel_run"})
222
+ return True
223
+
224
+ def clone(self, client):
225
+ """Creates a new ConversationManager with copied thread messages."""
226
+ # Create new instance with same init parameters
227
+ new_cm = ConversationManager(
228
+ client,
229
+ self.user,
230
+ self.assistants['general'].id,
231
+ intro_done=True
232
+ )
233
+
234
+ # Get all messages from current thread
235
+ messages = self._get_current_thread_history(remove_system_message=False)
236
+
237
+ # Delete the automatically created thread from constructor
238
+ new_cm.client.beta.threads.delete(new_cm.current_thread.id)
239
+
240
+ # Create new thread with first 30 messages
241
+ new_cm.current_thread = new_cm.client.beta.threads.create(
242
+ messages=messages[:30]
243
+ )
244
+
245
+ # Add remaining messages one by one if there are more than 30
246
+ for msg in messages[30:]:
247
+ new_cm.add_message_to_thread(
248
+ new_cm.current_thread.id,
249
+ msg['role'],
250
+ msg['content']
251
+ )
252
+
253
+ # Copy other relevant state
254
+ new_cm.state = self.state
255
+
256
+ return new_cm
257
+
258
+ def __str__(self):
259
+ return f"ConversationManager(intro_done={self.intro_done}, assistants={self.assistants}, current_thread={self.current_thread})"
260
+
261
+ def __repr__(self):
262
+ return (f"ConversationManager("
263
+ f"intro_done={self.intro_done}, current_thread={self.current_thread})")
264
 
265
+ class User:
266
  def __init__(self, user_id, user_info, client, asst_id):
267
  self.user_id = user_id
268
  self.client = client
 
270
  self.user_info = user_info
271
  self.done_first_reflection = None
272
  self.goal = []
 
273
  self.micro_actions = []
 
274
  self.challenges = []
275
  self.other_focusses = []
276
  self.personal_growth_score = 0
 
278
  self.relationship_score = 0
279
  self.mental_well_being_score = 0
280
  self.health_and_wellness_score = 0
 
 
 
 
281
 
282
  # Read growth_plan.json and store it
 
 
283
  growth_plan = {"growthPlan": [
284
  {
285
  "day": 1,
 
287
  },
288
  {
289
  "day": 2,
290
+ "coachingTheme": "MOTIVATION_INSPIRATION_STATE"
291
  },
292
  {
293
  "day": 3,
294
+ "coachingTheme": "PROGRESS_REFLECTION_STATE"
295
  },
296
  {
297
  "day": 4,
 
299
  },
300
  {
301
  "day": 5,
302
+ "coachingTheme": "MOTIVATION_INSPIRATION_STATE"
303
  },
304
  {
305
  "day": 6,
306
+ "coachingTheme": "OPEN_DISCUSSION_STATE"
307
  },
308
  {
309
  "day": 7,
310
+ "coachingTheme": "PROGRESS_REFLECTION_STATE"
311
+ },
312
+ {
313
+ "day": 8,
314
+ "coachingTheme": "PROGRESS_SUMMARY_STATE"
315
+ },
316
+ {
317
+ "day": 9,
318
+ "coachingTheme": "MICRO_ACTION_STATE"
319
+ },
320
+ {
321
+ "day": 10,
322
+ "coachingTheme": "MOTIVATION_INSPIRATION_STATE"
323
+ },
324
+ {
325
+ "day": 11,
326
+ "coachingTheme": "PROGRESS_REFLECTION_STATE"
327
+ },
328
+ {
329
+ "day": 12,
330
+ "coachingTheme": "MICRO_ACTION_STATE"
331
+ },
332
+ {
333
+ "day": 13,
334
+ "coachingTheme": "MOTIVATION_INSPIRATION_STATE"
335
+ },
336
+ {
337
+ "day": 14,
338
+ "coachingTheme": "OPEN_DISCUSSION_STATE"
339
+ },
340
+ {
341
+ "day": 15,
342
  "coachingTheme": "FINAL_SUMMARY_STATE"
343
  }
344
+ ]
345
+ }
 
346
  self.growth_plan = CircularQueue(array=growth_plan['growthPlan'], user_id=self.user_id)
347
  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"})
348
 
349
  self.user_interaction_guidelines = self.generate_user_interaction_guidelines(user_info, client)
350
  self.conversations = ConversationManager(client, self, asst_id)
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  def add_life_score_point(self, variable, points_added, notes):
353
  if variable == 'Personal Growth':
354
  self.personal_growth_score += points_added
 
365
  elif variable == 'Relationship':
366
  self.relationship_score += points_added
367
  logger.info(f"Added {points_added} points to Relationship for {notes}", extra={"user_id": self.user_id, "endpoint": "add_life_score_point"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
+ def get_current_goal(self):
370
+ return self.goal[-1] if self.goal else None
371
+
372
+ def set_goal(self, goal, area, status):
373
+ self.goal.append({"goal": goal, "area": area, "status": status, "created_at": pd.Timestamp.now().strftime("%d-%m-%Y %a %H:%M:%S"), "updated_at": pd.Timestamp.now().strftime("%d-%m-%Y %a %H:%M:%S")})
374
+ self.add_life_score_point(variable = area, points_added = 10, notes = "Setting a Goal")
375
+
376
+ def update_micro_action_status(self, completed_micro_action):
377
+ if completed_micro_action:
378
+ self.micro_actions[-1]["status"] = "COMPLETE"
379
+ self.micro_actions[-1]["updated_at"] = pd.Timestamp.now().strftime("%d-%m-%Y %a %H:%M:%S")
380
+
381
+ num_of_micro_actions_completed = sum(1 for item in self.micro_actions if item['status'] == 'COMPLETE')
382
+
383
+ if (num_of_micro_actions_completed in (1,3,5)) or (num_of_micro_actions_completed % 10 == 0 and num_of_micro_actions_completed != 0):
384
+ self.add_life_score_point(variable = self.get_current_goal()['area'], points_added = 10, notes = f"Completing the {num_of_micro_actions_completed}-th micro-action")
385
+
386
+ def trigger_deep_reflection_point(self, area_of_deep_reflection):
387
+ if len(area_of_deep_reflection)>0:
388
+ for area in area_of_deep_reflection:
389
+ self.add_life_score_point(variable = area, points_added = 5, notes = f"Doing a deep reflection about {area}")
390
+
391
+ def update_goal_status(self):
392
+ self.goal[-1]["status"] = "COMPLETE"
393
+ self.goal[-1]["updated_at"] = pd.Timestamp.now().strftime("%d-%m-%Y %a %H:%M:%S")
394
+ self.add_life_score_point(variable = self.get_current_goal()['area'], points_added = 30, notes = "Completing a Goal")
395
+
396
+ def add_point_for_booking(self):
397
+ self.add_life_score_point(variable = self.get_current_goal()['area'], points_added = 5, notes = "Booking a GG session")
 
398
 
399
+ def add_point_for_completing_session(self):
400
+ self.add_life_score_point(variable = self.get_current_goal()['area'], points_added = 20, notes = "Completing a GG session")
401
+
402
  def add_ai_message(self, text):
403
  self.conversations._add_ai_message(text)
404
  return text
405
 
 
406
  def reset_conversations(self):
407
  self.conversations = ConversationManager(self.client, self, self.asst_id)
408
  self.growth_plan.reset()
 
409
  self.goal = []
410
  self.micro_actions = []
 
411
  self.challenges = []
412
  self.other_focusses = []
413
+
414
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  def generate_user_interaction_guidelines(self, user_info, client):
416
  logger.info(f"Generating user interaction guidelines for user: {self.user_id}", extra={"user_id": self.user_id, "endpoint": "generate_user_interaction_guidelines"})
417
+ prompt = f"A 'profile' is a document containing rich insights on users for the purpose of \
418
+ providing contexts to LLMs. Based on the user's information, generate a \
419
+ user summary that describes how best to interact with this user to create a personalized \
420
+ and targeted chat experience. The user summary MUST strictly contain these parts:\
421
+ What kind of coaching style & tone that the user possibly prefers based on their...\
422
+ 1. Based on the user's chosen 'Legend Persona'\
423
+ 2. Based on the user's age\
424
+ 3. Based on the user's MBTI\
425
+ 4. Based on the user's Love Language\
426
+ 5. Based on the user's experience of trying coaching previously\
427
+ 6. Based on the user's belief in Astrology\
428
+ Generate a 6-point user summary based on the following \
429
+ user information:\n\n{user_info}"
430
+
431
+ response = client.chat.completions.create(
432
+ model="gpt-4o-mini",
433
+ messages=[
434
+ {"role": "system", "content": "You are an expert at building profile documents containing rich user insights."},
435
+ {"role": "user", "content": prompt}
436
+ ],
437
+ temperature=0.2
438
+ )
439
+
440
+ user_guideline = f"""
441
+ {user_info}\n\n
442
+ ### INTERACTION GUIDELINE ### \n
443
+ {response.choices[0].message.content}
444
+ """
 
 
445
 
446
  return user_guideline
447
 
 
448
  def get_recent_run(self):
449
  return self.conversations.assistants['general'].recent_run
450
 
451
+ def cancel_run(self, run):
452
+ self.conversations.cancel_run(run)
 
 
453
 
 
454
  def update_conversation_state(self, stage, last_interaction):
455
  self.conversation_state['stage'] = stage
456
  self.conversation_state['last_interaction'] = last_interaction
457
 
 
458
  def _get_current_thread(self):
459
  return self.conversations.current_thread
460
 
461
+ def send_message(self, text):
462
+ response, info = self.conversations._run_current_thread(text)
463
+ logger.info(f"Info: {info}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
 
 
 
 
464
 
465
+ if info.get("message") == "cancelled":
466
  # must do current plan now
467
  action = self.growth_plan.current()
468
  logger.info(f"Current Action: {action}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
469
+ response = self.do_theme(action['coachingTheme'], self.conversations.state['date'], action['day'])
470
 
471
  # add response to ai message
 
472
  self.add_ai_message(response['content'])
473
 
474
  # Move to the next action
475
  self.growth_plan.next()
476
 
477
+ elif info.get("message") == "change_goal":
 
 
478
  # send the change goal prompt
479
  logger.info("Sending change goal message...", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
480
  prompt = f"""
481
+ I want to change my goal!
482
 
483
  Previous Goal:
484
  {self.get_current_goal()}
 
496
  # reset the growth_plan
497
  self.growth_plan.reset()
498
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
500
  return response
501
 
502
+ def get_messages(self, exclude_system_msg=True):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  if not exclude_system_msg:
504
  return self.conversations._get_current_thread_history(False)
505
  else:
506
+ 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)))
 
 
507
 
 
 
 
 
 
508
  def set_intro_done(self):
509
  self.conversations.intro_done = True
510
 
511
+ def do_theme(self, theme, date, day):
512
+ logger.info(f"Doing theme: {theme}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  if theme == "MOTIVATION_INSPIRATION_STATE":
515
+ formatted_message = MOTIVATION_INSPIRATION_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
516
  elif theme == "PROGRESS_REFLECTION_STATE":
517
+ formatted_message = PROGRESS_REFLECTION_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
 
 
 
518
  elif theme == "MICRO_ACTION_STATE":
519
+ formatted_message = MICRO_ACTION_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
 
 
 
 
 
520
  elif theme == "OPEN_DISCUSSION_STATE":
521
+ formatted_message = OPEN_DISCUSSION_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
 
 
 
522
  elif theme == "PROGRESS_SUMMARY_STATE":
523
+ formatted_message = PROGRESS_SUMMARY_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
524
  elif theme == "FINAL_SUMMARY_STATE":
525
+ formatted_message = FINAL_SUMMARY_STATE.format(self.get_current_goal(), day, len(self.growth_plan.array))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
+ prompt = f"""** It is a new day: {date} **
528
+ Additional System Instruction:
529
+ - Remember all of the user's personal information. Use this information to be as personalised as possible when conversing by including it in your responses where relevant.
530
+ - You are a good life coach because you don't overwhelm the user by sending too many questions or lengthy messages. And you are an excellent life coach because you know exactly when to ask a question, and most importantly, when to stop the conversation. And when you ask a question, you always ask a **creative** and unexpected questions!
531
+ - Keep all of your response short, only 2-3 lines/sentences maximum! If your response contains a question, use a single break line before the question and encapsulate the question with one asterisk like this: *question*
532
+ - Be **creative** ! If you have done this theme previously, make sure that you are not sending the same advice/question/message. Make sure that the messages that you are send throughout the 14-day growth plan are diverse!
533
+ - At the end of this messasge, there are some instructions that you need to **prioritize**. Read the instructions carefully!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
 
 
 
 
 
 
 
 
 
 
 
535
 
 
 
536
  Today's Theme:
537
  {formatted_message}
538
  """
539
+ response = self.conversations._send_morning_message(prompt)
540
 
541
  if theme == "MICRO_ACTION_STATE":
542
+ self.micro_actions.append({'micro_action': response, 'status': 'PENDING', 'created_at': pd.Timestamp.now().strftime("%d-%m-%Y %a %H:%M:%S"), 'updated_at': pd.Timestamp.now().strftime("%d-%m-%Y %a %H:%M:%S")})
 
 
 
 
543
 
544
+ return response
545
 
 
546
  def change_date(self, date):
547
  logger.info(f"Changing date from {self.conversations.state['date']} to {date}",
548
  extra={"user_id": self.user_id, "endpoint": "user_change_date"})
 
 
 
549
  self.conversations.state['date'] = date
 
 
550
 
551
  action = self.growth_plan.current()
552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  ## ADD POINT FOR CHANGE DATE
554
  if self.growth_plan.current()['day'] == 7:
555
+ self.add_life_score_point(variable = self.get_current_goal()['area'], points_added = 5, notes = "Reaching Day 7")
 
556
  elif self.growth_plan.current()['day'] == 14:
557
+ self.add_life_score_point(variable = self.get_current_goal()['area'], points_added = 10, notes = "Reaching Day 14")
 
558
 
559
  logger.info(f"Today's action is {action}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
560
 
 
565
 
566
  # The coaching theme conditions are hardcoded for now
567
  theme = action['coachingTheme']
568
+ response = self.do_theme(theme, date, action['day'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
  # Move to the next action
571
  self.growth_plan.next()
572
 
573
  logger.info(f"Date Updated: {self.conversations.state['date']}", extra={"user_id": self.user_id, "endpoint": "user_change_date"})
574
+ return response
575
 
 
576
  def update_user_info(self, new_info):
577
  logger.info(f"Updating user info: [{self.user_info}] with: [{new_info}]", extra={"user_id": self.user_id, "endpoint": "update_user_info"})
578
  # make an api call to gpt4o to compare the current user_info and the new info and create a new consolidated user_info
 
581
 
582
  {self.user_info}
583
 
584
+ Pottential new user information:
585
 
586
  {new_info}
587
 
 
601
  logger.info(f"Updated user info: {self.user_info}", extra={"user_id": self.user_id, "endpoint": "update_user_info"})
602
  return True
603
 
604
+ def _summarize_zoom(self, zoom_ai_summary):
605
+ logger.info(f"Summarizing zoom ai summary: {zoom_ai_summary}", extra={"user_id": self.user_id, "endpoint": "summarize_zoom"})
 
606
  # 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
607
+ system_prompt = """You are an expert at summarizing AI-generated Zoom transcripts, focusing on extracting key user insights to enhance personalization in future interactions."""
608
+ prompt = f"Please summarize the following AI-generated Zoom transcript, emphasizing the most significant user insights and information:\n\n{zoom_ai_summary}"
609
 
610
  response = self.client.chat.completions.create(
611
  model="gpt-4o",
 
613
  {"role": "system", "content": system_prompt},
614
  {"role": "user", "content": prompt}
615
  ],
616
+ temperature=0.2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  )
618
+ return response.choices[0].message.content
619
 
 
 
 
 
 
620
  def _update_user_data(self, data_type, text_input, extra_text=""):
621
  data_mapping = {
622
  'micro_actions': {
623
  'prompt_description': 'micro actions',
624
  'status': 'RECOMMENDED',
625
+ 'attribute': 'micro_actions',
626
  'endpoint': f'update_{data_type}',
627
  },
628
  'challenges': {
 
653
  f"Text:\n{text_input}"
654
  )
655
 
656
+ try:
657
+ current_time = datetime.now().strftime("%d-%m-%Y %a %H:%M:%S")
658
+
659
+ response = self.client.beta.chat.completions.parse(
660
+ model="gpt-4o",
661
+ messages=[{"role": "user", "content": prompt}],
662
+ response_format=UserDataResponse,
663
+ temperature=0.2
664
+ )
665
+
666
+ data = getattr(response.choices[0].message.parsed, 'data')
667
+
668
+ # Update the common fields for each item
669
+ for item in data:
670
+ item.role = "assistant"
671
+ item.user_id = self.user_id
672
+ item.status = mapping['status']
673
+ item.created_at = current_time
674
+ item.updated_at = current_time
675
+
676
+ logger.info(f"Updated {data_type}: {data}", extra={"user_id": self.user_id, "endpoint": mapping['endpoint']})
677
+ getattr(self, mapping['attribute']).extend(data)
678
+
679
+ except Exception as e:
680
+ logger.error(f"Failed to update {data_type}: {e}", extra={"user_id": self.user_id})
681
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682
  def update_user_data(self, gg_report):
683
+ self.micro_actions = self._update_user_data('micro_actions', gg_report[0]['answer'])
684
 
685
  extra_text = f"User has new challenge:\n{gg_report[1]['answer']}\n\n"
686
  self._update_user_data('challenges', gg_report[2]['answer'], extra_text=extra_text)
 
689
 
690
  self._update_goal(gg_report[4]['answer'])
691
 
 
692
  def _update_goal(self, goal_text):
693
  prompt = f"""
694
  The user has a current goal: {self.get_current_goal()}
695
  The user provided a new goal: {goal_text}
696
 
697
+ Determine if the new goal is the same as the current goal or if it's a new one.
698
+ If it's the same, respond with the current goal, and set same_or_not == True
699
+ If it's a new goal, respond with the new goal, and set same_or_not == False
700
+
 
 
701
  Your response will be the final goal. You will also need to determine the area of this
702
  final goal by choosing one of these areas that suits the final goal:
703
  "Personal Growth", "Career Growth", "Relationship", "Mental Well-Being", "Health and Wellness"
 
708
  goal: str (the final goal),
709
  area: str (the area of the goal)
710
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  """
712
 
713
  response = self.client.chat.completions.create(
 
755
 
756
  final_goal = json.loads(response.choices[0].message.content)['goal']
757
  final_goal_area = json.loads(response.choices[0].message.content)['area']
758
+ if json.loads(response.choices[0].message.content)['same_or_not']:
759
+ final_goal_status = self.get_current_goal()['status']
760
+ else:
761
+ final_goal_status = 'PENDING'
762
 
763
  if json.loads(response.choices[0].message.content)['same_or_not'] == False:
764
+ self.set_goal(final_goal, final_goal_area, final_goal_status)
765
  logger.info(f"User goal updated to: {final_goal}", extra={"user_id": self.user_id, "endpoint": "_update_goal"})
766
  else:
767
  logger.info(f"User goal remains unchanged.", extra={"user_id": self.user_id, "endpoint": "_update_goal"})
768
 
769
+ def process_growth_guide_session(self, session_data):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  logger.info(f"Processing growth guide session data: {session_data}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
771
+
772
  # Generate the ourcoach_report (summary)
773
  zoom_ai_summary = session_data["zoom_ai_summary"]
774
  gg_report = session_data["gg_report"]
775
 
776
+ ourcoach_report = self._summarize_zoom(zoom_ai_summary)
777
 
778
  # Update user data based on growth guide answers
779
  self.update_user_data(gg_report)
780
+ self.update_user_info(gg_report[5]['answer'] + "\n\n" + ourcoach_report)
 
 
 
 
 
781
 
782
  # Send hidden message to AI to generate the response
783
  logger.info(f"Sending hidden message to AI to generate response", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
784
+ response = self.conversations._send_morning_message("I have completed the growth guide session. Please generate the response.")
 
 
 
 
 
 
 
 
785
  logger.info(f"Response: {response}", extra={"user_id": self.user_id, "endpoint": "process_growth_guide_session"})
786
  return response
787
+
788
+ def __hash__(self) -> int:
789
+ return hash(self.user_id)
790
 
791
+ def _prepare_growth_guide_report(self, zoom_transcript):
792
+ system_prompt = """You are an AI assistant tasked with transforming a raw Zoom transcript of a coaching session into a well-structured report.
793
+ The report should be organized into the following sections:
794
+ 1) Session Details
795
+ 2) Session Objectives
796
+ 3) Summary of Discussion
797
+ 4) Key Takeaways
798
+ 5) Action Items
799
+ 6) Next Steps
800
+ 7) Additional Notes (if any)
801
+ Ensure that each section is clearly labeled and the information is concise and well-organized.
802
+ Use bullet points or numbered lists where appropriate to enhance readability."""
803
+ 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\
804
+ Raw Zoom Transcript:\n\n{zoom_transcript}.\n\nKeep the report personalised to the 'user': {self.user_info}."
805
 
806
+ response = self.client.chat.completions.create(
807
+ model="gpt-4o-mini",
808
+ messages=[
809
+ {"role": "system", "content": system_prompt},
810
+ {"role": "user", "content": prompt}
811
+ ],
812
+ temperature=0.2
813
+ )
814
+ return response.choices[0].message.content
815
+
816
+ def _infer_follow_ups(self, created, context):
817
+ prompt = f"Infer the datetime of the next follow-up for the user based on the created date:{created} and the context:{context}"
818
+
819
+ system_prompt = """
820
+ 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.
821
+ 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.
822
+ # Output Format
823
 
824
+ - Output a single string representing the follow-up date.
825
+ - Format the string as: '%d-%m-%Y %a %H:%M:%S' (e.g., '20-11-2024 Wed 14:30:45').
826
+
827
+ # Notes
828
+
829
+ - The follow-up date must be after the current date.
830
+ - Use the context to infer the time. If a time cannot be inferred, then set it as 10:30:00.
831
+ - Only provide the date string, with no additional text.
832
+ # Example
833
+ 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
834
+ Assistant: '03-01-2024 Wed 10:30:00'
835
+ 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
836
+ Assistant: '03-01-2024 Wed 12:00:00'
837
+ 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
838
+ Assistant: '20-11-2024 Wed 19:30:00'
839
+ 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
840
+ Assistant: '22-11-2024 Fri 23:30:00'
841
+ 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
842
+ Assistant: '24-11-2024 Sun 12:00:00'
843
+ """
844
+ response = self.client.chat.completions.create(
845
+ model="gpt-4o-mini",
846
+ messages=[
847
+ {"role": "system", "content": system_prompt},
848
+ {"role": "user", "content": prompt}
849
+ ],
850
+ top_p=0.1
851
+ )
852
+ return response.choices[0].message.content
853
+
854
+ def infer_memento_follow_ups(self):
855
+ try:
856
+ mementos_path = os.path.join("mementos", "to_upload", f"{self.user_id}", "*.json")
857
+ # mementos_path = f"mementos/to_upload/{self.user_id}/*.json"
858
+
859
+ for file_path in glob.glob(mementos_path):
860
+ with open(file_path, 'r+') as file:
861
+ data = json.load(file)
862
+ infered_follow_up = self._infer_follow_ups(data['created'], data['context'])
863
+ logger.info(f"[Infered Follow Up]: {infered_follow_up}", extra={"user_id": self.user_id, "endpoint": "infer_memento_follow_ups"})
864
+ data['follow_up_on'] = infered_follow_up
865
+ file.seek(0)
866
+ json.dump(data, file, indent=4)
867
+ file.truncate()
868
+ return True
869
+ except Exception as e:
870
+ return False
871
 
 
872
  def get_daily_messages(self):
873
  return self.conversations.get_daily_thread()
874
 
 
875
  def change_assistant(self, asst_id):
876
  self.asst_id = asst_id
877
  self.conversations.assistants['general'] = Assistant(self.asst_id, self.conversations)
 
890
  def __str__(self):
891
  return f"""User(user_id={self.user_id}
892
  micro_actions={self.micro_actions}
 
893
  challenge={self.challenges}
894
  other_focusses={self.other_focusses}
895
  goals={self.goal}
 
900
  def __repr__(self):
901
  return f"""User(user_id={self.user_id}
902
  micro_actions={self.micro_actions}
 
903
  challenge={self.challenges}
904
  other_focusses={self.other_focusses}
905
  goals={self.goal}
 
921
 
922
  def save_user(self):
923
  # Construct the file path dynamically for cross-platform compatibility
924
+ try:
925
+ file_path = os.path.join("users", "to_upload", f"{self.user_id}.pkl")
926
+
927
+ # Ensure the directory exists
928
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
929
+
930
+ # Save the user object as a pickle file
931
+ with open(file_path, 'wb') as file:
932
+ pickle.dump(self, file)
933
+ return True
934
+ except Exception as e:
935
+ return False
936
 
937
  @staticmethod
938
  def load_user(user_id, client):
 
973
 
974
  def current(self):
975
  return self.array[self.index]
 
 
 
976
 
977
  def reset(self):
978
  self.index = 0
app/utils.py CHANGED
The diff for this file is too large to render. See raw diff
 
app/web_search.py DELETED
@@ -1,255 +0,0 @@
1
- import requests
2
- from dotenv import load_dotenv
3
- import os
4
- import logging
5
-
6
- logger = logging.getLogger(__name__)
7
-
8
- load_dotenv()
9
-
10
- class SearchEngine:
11
- BING_API_KEY = os.getenv("BING_API_KEY")
12
- BING_ENDPOINT = 'https://api.bing.microsoft.com/v7.0'
13
-
14
- @staticmethod
15
- def search(feedback_type_name, search_term, user_id):
16
- logger.info(f"User {user_id}: Initiating search for type '{feedback_type_name}' with term '{search_term}'")
17
- """
18
- Public method to perform a search based on the feedback type.
19
- """
20
- search_methods = {
21
- "General": SearchEngine._search_general,
22
- "Resource Links": SearchEngine._search_relevant_links,
23
- "Book/Podcast Recommendations": SearchEngine._search_books_or_podcasts,
24
- "Inspirational Stories or Case Studies": SearchEngine._search_inspirational_stories,
25
- "Fun Facts": SearchEngine._search_fun_facts,
26
- "Personalised Recommendations": SearchEngine._search_personalized_recommendations,
27
- "Videos": SearchEngine._search_videos
28
- }
29
-
30
- search_method = search_methods.get(feedback_type_name)
31
-
32
- if search_method:
33
- return search_method(search_term)
34
- else:
35
- return (feedback_type_name, search_term)
36
-
37
- @staticmethod
38
- def _search_relevant_links(search_term):
39
- logger.debug(f"Searching relevant links for term: {search_term}")
40
- """
41
- Uses Bing Web Search API to search for relevant links.
42
- """
43
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
44
- params = {'q': search_term, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
45
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
46
- if response.status_code == 200:
47
- logger.debug("Received successful response from Bing Web Search API.")
48
- data = response.json()
49
- links = []
50
- if 'webPages' in data and 'value' in data['webPages']:
51
- for result in data['webPages']['value']:
52
- links.append(result)
53
- return links
54
- else:
55
- logger.error(f"Bing Web Search API returned status code {response.status_code}")
56
- return ["No relevant links found."]
57
-
58
- @staticmethod
59
- def _search_books_or_podcasts(search_term):
60
- logger.debug(f"Searching books or podcasts for term: {search_term}")
61
- """
62
- Uses Bing Web Search API to search for books or podcasts.
63
- """
64
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
65
- query = f"{search_term} book OR podcast"
66
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
67
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
68
- if response.status_code == 200:
69
- logger.debug("Received successful response from Bing Web Search API for books/podcasts.")
70
- data = response.json()
71
- recommendations = []
72
- if 'webPages' in data and 'value' in data['webPages']:
73
- for result in data['webPages']['value']:
74
- title = result.get('name', 'Unknown Title')
75
- url = result.get('url', '')
76
- recommendations.append(f"{title}: {url}")
77
- return recommendations
78
- else:
79
- logger.error(f"Bing Web Search API returned status code {response.status_code} for books/podcasts search.")
80
- return ["No book or podcast recommendations found."]
81
-
82
- @staticmethod
83
- def _search_success_stories(search_term):
84
- logger.debug(f"Searching success stories for term: {search_term}")
85
- """
86
- Uses Bing Web Search API to search for success stories.
87
- """
88
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
89
- query = f"{search_term} success stories"
90
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
91
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
92
- if response.status_code == 200:
93
- logger.debug("Received successful response from Bing Web Search API for success stories.")
94
- data = response.json()
95
- stories = []
96
- if 'webPages' in data and 'value' in data['webPages']:
97
- for result in data['webPages']['value']:
98
- title = result.get('name', 'Unknown Title')
99
- url = result.get('url', '')
100
- stories.append(f"{title}: {url}")
101
- return stories
102
- else:
103
- logger.error(f"Bing Web Search API returned status code {response.status_code} for success stories search.")
104
- return ["No success stories found."]
105
-
106
- @staticmethod
107
- def _search_inspirational_stories(search_term):
108
- logger.debug(f"Searching inspirational stories or case studies for term: {search_term}")
109
- """
110
- Uses Bing Web Search API to search for inspirational stories or case studies.
111
- """
112
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
113
- query = f"{search_term} inspirational stories OR case studies"
114
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
115
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
116
- if response.status_code == 200:
117
- logger.debug("Received successful response from Bing Web Search API for inspirational stories.")
118
- data = response.json()
119
- stories = []
120
- if 'webPages' in data and 'value' in data['webPages']:
121
- for result in data['webPages']['value']:
122
- title = result.get('name', 'Unknown Title')
123
- url = result.get('url', '')
124
- stories.append(f"{title}: {url}")
125
- return stories
126
- else:
127
- logger.error(f"Bing Web Search API returned status code {response.status_code} for inspirational stories search.")
128
- return ["No inspirational stories found."]
129
-
130
- @staticmethod
131
- def _search_fun_facts(search_term):
132
- logger.debug(f"Searching fun facts for term: {search_term}")
133
- """
134
- Uses Bing Web Search API to search for fun facts related to personal growth.
135
- """
136
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
137
- query = f"{search_term} fun facts"
138
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
139
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
140
- if response.status_code == 200:
141
- logger.debug("Received successful response from Bing Web Search API for fun facts.")
142
- data = response.json()
143
- facts = []
144
- if 'webPages' in data and 'value' in data['webPages']:
145
- for result in data['webPages']['value']:
146
- snippet = result.get('snippet', '')
147
- facts.append(snippet)
148
- return facts
149
- else:
150
- logger.error(f"Bing Web Search API returned status code {response.status_code} for fun facts search.")
151
- return ["No fun facts found."]
152
-
153
- @staticmethod
154
- def _search_visual_content(search_term):
155
- logger.debug(f"Searching visual content for term: {search_term}")
156
- """
157
- Uses Bing Image Search API to search for images or infographics.
158
- """
159
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
160
- params = {'q': search_term, 'count': 3}
161
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/images/search", headers=headers, params=params)
162
- if response.status_code == 200:
163
- logger.debug("Received successful response from Bing Image Search API.")
164
- data = response.json()
165
- images = []
166
- if 'value' in data:
167
- for result in data['value']:
168
- image_url = result.get('contentUrl', '')
169
- images.append(image_url)
170
- return images
171
- else:
172
- logger.error(f"Bing Image Search API returned status code {response.status_code} for visual content search.")
173
- return ["No visual content found."]
174
-
175
- @staticmethod
176
- def _search_personalized_recommendations(search_term):
177
- logger.debug(f"Searching personalized recommendations for term: {search_term}")
178
- """
179
- Uses Bing Web Search API to provide personalized recommendations.
180
- """
181
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
182
- query = f"tips for {search_term}"
183
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
184
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
185
- if response.status_code == 200:
186
- logger.debug("Received successful response from Bing Web Search API for personalized recommendations.")
187
- data = response.json()
188
- recommendations = []
189
- if 'webPages' in data and 'value' in data['webPages']:
190
- for result in data['webPages']['value']:
191
- title = result.get('name', 'Unknown Title')
192
- url = result.get('url', '')
193
- recommendations.append(f"{title}: {url}")
194
- return recommendations
195
- else:
196
- logger.error(f"Bing Web Search API returned status code {response.status_code} for personalized recommendations search.")
197
- return ["No personalized recommendations found."]
198
-
199
- @staticmethod
200
- def _search_videos(search_term):
201
- logger.debug(f"Searching videos for term: {search_term}")
202
- """
203
- Uses Bing Video Search API to search for videos, prioritizing YouTube results.
204
- """
205
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
206
- query = f"site:youtube.com {search_term}"
207
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
208
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/videos/search", headers=headers, params=params)
209
- if response.status_code == 200:
210
- logger.debug("Received successful response from Bing Video Search API.")
211
- data = response.json()
212
- videos = []
213
- if 'value' in data:
214
- for result in data['value']:
215
- title = result.get('name', 'Unknown Title')
216
- url = result.get('contentUrl', '')
217
- # Prioritize YouTube results
218
- if 'youtube.com' in url.lower():
219
- videos.append(f"{title}: {url}")
220
- if len(videos) >= 3:
221
- break
222
- # If we don't have enough YouTube results, add other video results
223
- if len(videos) < 3:
224
- for result in data['value']:
225
- title = result.get('name', 'Unknown Title')
226
- url = result.get('contentUrl', '')
227
- if url not in [v.split(': ')[1] for v in videos]:
228
- videos.append(f"{title}: {url}")
229
- if len(videos) >= 3:
230
- break
231
- return videos
232
- else:
233
- logger.error(f"Bing Video Search API returned status code {response.status_code} for video search.")
234
- return ["No video content found."]
235
-
236
- @staticmethod
237
- def _search_general(search_term):
238
- logger.debug(f"Performing a general search for term: {search_term}")
239
- """
240
- Uses Bing Web Search API to perform a general search.
241
- """
242
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
243
- params = {'q': search_term, 'textDecorations': True, 'textFormat': 'HTML', 'count': 5}
244
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
245
- if response.status_code == 200:
246
- logger.debug("Received successful response from Bing Web Search API for general search.")
247
- data = response.json()
248
- results = []
249
- if 'webPages' in data and 'value' in data['webPages']:
250
- for result in data['webPages']['value']:
251
- results.append(result)
252
- return results
253
- else:
254
- logger.error(f"Bing Web Search API returned status code {response.status_code} for general search.")
255
- return ["No results found for general search."]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/zoom_summary.txt CHANGED
@@ -1,42 +1,32 @@
1
  Quick Recap
2
 
3
- Andrea and Yew Wai discussed Yew Wai's work, health, and personal life, addressing the challenges in each area. They explored Yew Wai's motivation, priorities, and personal growth goals, including becoming a founder, being more consistent in health habits, and improving relationships. The conversation concluded with a discussion on the potential effects of focusing on OurCoach on various aspects of his life, strategies to push forward with his goals, and the necessity for Yew Wai to find a balance between his career, health, and relationships.
4
 
5
  Next Steps
6
 
7
- Yew Wai to run user testing with Growth Guide sessions for OurCoach in the next 1-2 weeks.
8
- Yew Wai to launch the new chat and web dashboard for OurCoach this week.
9
- Yew Wai to try scheduling some exercise time, even if just a 10-minute walk.
10
- Yew Wai to be more organized in day-to-day tasks and continue meeting up with the team.
11
- Yew Wai to focus on improving his sleep as his new goal.
12
  Summary
13
 
14
- Work and Personal Life Balance
15
 
16
- In the meeting, Andrea and Yew Wai discussed Yew Wai's work and personal life. Yew Wai expressed that his life is not always balanced due to the demands of work, particularly the development of their coaching platform, OurCoach. He also mentioned upcoming holiday plans to Phuket with his partner and her family. Yew Wai looks forward to the completion of the coaching platform and the subsequent return to a more regular routine.
17
 
18
- Addressing Yew's Personal Habits Challenges
19
 
20
- Andrea and Yew Wai discussed Yew Wai's current challenges related to discipline in personal habits such as sleep, eating, and exercise. Yew Wai admitted to irregular sleep patterns, inconsistent eating times, and infrequent exercise, which negatively impact his mood and energy levels. He also relies on coffee to compensate for his irregular habits. Additionally, Yew Wai shared that he and his fiancé, who lives in Jakarta, often face long-distance challenges.
21
 
22
- Yew's Motivation and Priorities Discussed
23
 
24
- In the meeting, Andrea and Yew Wai explored his motivation and priorities. Yew Wai shared that seeing OurCoach come to fruition and achieving career growth are key motivators. He expressed a desire to make a positive impact with the launch of their product, hoping that users will have a new-age experience. Yew Wai's priorities were identified as career, health, and relationships, with career currently at the forefront. He emphasized the importance of building a valuable product for users.
25
 
26
- Yew's Exercise Routine and Marathon Goal
27
 
28
- Yew Wai discussed his exercise routine, stating that he tries to exercise at least once or twice a week, typically involving running, gym sessions, and tennis. He mentioned that he used to be more into long-distance running but now prefers tennis more often. Yew also shared his goal of running a 42 km marathon by the age of 42, which remains a long-term aspiration.
29
 
30
- Personal Growth and Career Goals
31
 
32
- Yew Wai outlined his personal growth goals for the year, including becoming a founder and improving his relationships. He mentioned the possibility of helping his Indonesian partner, Karina, move to Malaysia by Q1 next year. Yew Wai's motivation for these goals stems from a desire for personal growth and avoiding feelings of stagnation. He views these goals as interconnected, with self-discipline being crucial for their achievement. His immediate priority is his career.
33
-
34
- Coach's Impact on Yew's Life Goals
35
-
36
- Andrea and Yew Wai discussed the potential effects of focusing on OurCoach on various aspects of Yew Wai's life. Yew Wai suggested that dedicating significant focus to building OurCoach may negatively impact his health and relationships due to limited time for these areas. However, successfully building OurCoach could also boost his confidence, as achieving this goal would fulfill his long-held dream of building his own company.
37
-
38
- Yew's Progress and Development Plans
39
-
40
- Andrea and Yew Wai reviewed his progress towards his goals, particularly focusing on building OurCoach and taking a more hands-on approach. Yew expressed a desire to be more organized and meet with the team more frequently to accelerate development. They discussed upcoming user testing and the growth guide. Andrea suggested that Yew Wai could benefit from finding a balance between his career, health, and relationships. They agreed to reconnect next week to reflect on his progress.
41
-
42
- Growth Guide Notes: Yew Wai wants to change his goal to focus on “Improving his sleep”.
 
1
  Quick Recap
2
 
3
+ Farant and the Growth Guide discussed Farant's ongoing efforts to improve his communication skills, specifically addressing his stuttering and anxiety during presentations. They explored his current action plan, the challenges he's facing, and introduced the importance of focusing on his mental well-being to support his primary goal. The conversation also touched on balancing preparation for presentations and managing stress effectively.
4
 
5
  Next Steps
6
 
7
+ Farant to continue practicing presentations regularly, both alone and with a small group of friends over the next 1-2 weeks.
8
+ Implement and refine breathing techniques to manage stuttering and reduce anxiety before speaking.
9
+ Incorporate stress management strategies, such as mindfulness or relaxation exercises, into his daily routine.
10
+ Allocate specific times for preparation to avoid overthinking and excessive focus on details.
11
+ Schedule a follow-up session with the Growth Guide to assess progress and adjust the action plan as needed.
12
  Summary
13
 
14
+ Progress on Communication Skills
15
 
16
+ In the meeting, Farant shared his dedication to improving his communication skills. He has been practicing his presentations more consistently, both individually and in front of friends, and has started using breathing techniques to help manage his stuttering. These efforts have led to a gradual decrease in his anxiety levels before speaking.
17
 
18
+ Addressing Stuttering and Anxiety
19
 
20
+ Farant continues to face challenges with stuttering during presentations, although he reports becoming more comfortable with it over time. His anxiety prior to speaking engagements is slowly diminishing, indicating positive progress. However, he still experiences nervousness, which he is actively working to mitigate through his current practices.
21
 
22
+ New Challenges in Balancing Preparation
23
 
24
+ Farant identified a new challenge related to balancing his preparation for presentations. He tends to spend excessive time on details, which contributes to his stress levels. Finding the right balance between thorough preparation and avoiding overthinking is an area he aims to improve.
25
 
26
+ Focus on Mental Well-Being
27
 
28
+ Beyond his primary goal of enhancing communication skills, Farant expressed a desire to focus on his mental well-being. He recognizes that managing stress is crucial not only for his personal health but also for achieving his communication objectives. Integrating stress management techniques into his routine is seen as complementary to his main goal.
29
 
30
+ Decision on Continuing Current Goals
31
 
32
+ Based on the session, Farant has decided to continue pursuing his current goal of improving communication skills while also incorporating strategies to manage his stress. He believes that addressing both areas simultaneously will lead to more effective and sustainable progress in his presentations and overall well-being.