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