diff --git a/main.py b/main.py index 4e88f5a8..5e265e7a 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) - +#from textChatMode.chat import router as ask_router from textChatMode.chat import router as ask_router #from textChatMode.chatmistral import router as ask_router from voiceChatMode.voice import router as voice_router @@ -16,7 +16,9 @@ from monitoringAgentSystem.monotoring_agent_system import router as monitoring_router #from MonitoringAgent.monitor_api import router as monitor_router from LearningAgent.Feedback_Therapy.app_fastapi import router as therapyFeedback_router + from therapyAgent.therapyAgent import router as therapy_router +from therapyAgent.VoiceTherapyAgent.voiceTherapyAgent import voice_therapy_router, tts_router app = FastAPI() @@ -36,12 +38,15 @@ app.include_router(level_detection_router) #app.include_router(agent_router) app.include_router(game_router) -app.include_router(therapy_router) app.include_router(monitoring_router) #app.include_router(monitor_router) + app.include_router(therapy_router) app.include_router(therapyFeedback_router) +app.include_router(voice_therapy_router) +app.include_router(tts_router) + @app.get("/") def root(): - return {"message": "All endpoints are loaded successfully"} \ No newline at end of file + return {"message": "All endpoints loaded successfully"} diff --git a/textChatMode/chat.py b/textChatMode/chat.py index c97bc6d9..98281cc1 100644 --- a/textChatMode/chat.py +++ b/textChatMode/chat.py @@ -196,7 +196,7 @@ async def ask_question(data: QueryRequest): response = requests.post( f"{key_param.llm_base}/monitor-agent/track-activity", json=monitor_payload, - timeout=15 + timeout=0.1 ) print("Logged chat activity to Monitor Agent:", response) except Exception as e: diff --git a/therapyAgent/VoiceTherapyAgent/voiceTherapyAgent.py b/therapyAgent/VoiceTherapyAgent/voiceTherapyAgent.py new file mode 100644 index 00000000..4935b219 --- /dev/null +++ b/therapyAgent/VoiceTherapyAgent/voiceTherapyAgent.py @@ -0,0 +1,250 @@ +from fastapi import APIRouter, UploadFile, File, Form +from fastapi.responses import JSONResponse, FileResponse +from pydub import AudioSegment +from io import BytesIO +import json +import requests +import re +from datetime import datetime +from pymongo import MongoClient +import os + +import key_param +from textChatMode.chat import QueryRequest +from ..utils.therapy_selector import get_therapy_recommendation +from ..utils.history_tracker import save_therapy_history, get_user_therapy_history +from langchain_openai import ChatOpenAI +from fastapi.responses import StreamingResponse +from io import BytesIO + +voice_therapy_router = APIRouter(prefix="/voice-therapy-agent", tags=["Voice Therapy Agent"]) +tts_router = APIRouter(prefix="/tts", tags=["TTS"]) + +OPENAI_API_KEY = key_param.openai_api_key + +# Ensure audio folder exists +@tts_router.post("") +async def generate_tts(payload: dict): + try: + text = payload.get("text") + if not text: + return JSONResponse(status_code=400, content={"error": "Text required"}) + + res = requests.post( + "https://api.openai.com/v1/audio/speech", + headers={ + "Authorization": f"Bearer {OPENAI_API_KEY}", + "Content-Type": "application/json", + }, + json={"model": "gpt-4o-mini-tts", "input": text}, + ) + + if res.status_code != 200: + return JSONResponse(status_code=500, content={"error": "TTS failed"}) + + audio_bytes = BytesIO(res.content) + + return StreamingResponse( + audio_bytes, + media_type="audio/mpeg" + ) + + except Exception as e: + return JSONResponse(status_code=500, content={"error": str(e)}) + +@voice_therapy_router.post("/chat") +async def voice_therapy_chat( + audio: UploadFile = File(...), + asked_phq_ids: str = Form("[]"), + summaries: str = Form("[]"), + history: str = Form(""), + user_id: str = Form(...), + session_id: str = Form(...), + level: str = Form(...), + therapy_feedback: str = Form(None) +): + try: + asked_ids = json.loads(asked_phq_ids) + summary_list = json.loads(summaries) + audio_bytes = await audio.read() + + # ------------------------------ + # CASE 1: NO AUDIO + # ------------------------------ + if len(audio_bytes) == 0: + user_query = ( + history.strip().split("\n")[-1].replace("User: ", "") + if history else "" + ) + else: + # ------------------------------ + # Whisper STT + # ------------------------------ + files = {"file": ("audio.webm", BytesIO(audio_bytes), audio.content_type)} + headers = {"Authorization": f"Bearer {OPENAI_API_KEY}"} + data = {"model": "whisper-1", "language": "en"} + + whisper_response = requests.post( + "https://api.openai.com/v1/audio/transcriptions", + headers=headers, + files=files, + data=data + ) + + if whisper_response.status_code != 200: + return JSONResponse(status_code=500, content={"error": "Whisper transcription failed"}) + + user_query = whisper_response.json()["text"].strip() + + # ------------------------------ + # DB + THERAPY LOGIC + # ------------------------------ + client = MongoClient(key_param.MONGO_URI) + db = client["blissMe"] + + history_records = get_user_therapy_history(db, user_id) + + recent_history = "\n".join( + f"{h['therapy_name']} on {h['date']} (duration {h['duration']} mins)" + for h in history_records + ) if history_records else "No prior therapies found." + + suggestion = get_therapy_recommendation(db, level, history_records) + + therapy_name = suggestion.get("name") + therapy_id = suggestion.get("id") + therapy_path = suggestion.get("path") + therapy_description = suggestion.get("description", "") + + # ------------------------------ + # LLM + # ------------------------------ + system_prompt = f""" +You are a warm, friendly therapy assistant. +Your main job is to support the user emotionally AND suggest a therapy when APPROPRIATE. +Don't suggest therapies every time—only when it fits naturally in the conversation. +If the user seems distressed, PRIORITIZE empathy and understanding first. +DON'T push therapies if the user is not open to it.User therapy history: +{recent_history} + +User: "{user_query}" +Depression level: {level} + +Rules: +- Keep responses short, caring, simple. +- Never mention depression level. +- Suggest therapies gently when appropriate. +- If the user seems distressed, PRIORITIZE empathy and understanding first. +- If the user declines a therapy, respect their choice and continue the chat supportively. +- Don't suggest therapies every time—only when it fits naturally in the conversation. +- also can ask about previously done therapies, which those helped or not. +After responding kindly, you MAY suggest: +"Would you like to start the {therapy_name} therapy now?" + +IF USER AGREES, return EXACTLY: +ACTION:START_THERAPY:{therapy_id} +""" + + bot = ChatOpenAI(model="gpt-3.5-turbo", openai_api_key=OPENAI_API_KEY) + + response = bot.invoke([ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_query} + ]) + + bot_reply = response.content.strip() + + # ------------------------------ + # ACTION DETECTION + # ------------------------------ + action_pattern = r"action\s*:\s*start[_\- ]therapy\s*:\s*([A-Za-z0-9]+)" + match = re.search(action_pattern, bot_reply.lower()) + action_detected = match.group(1).strip() if match else None + + if action_detected: + save_therapy_history( + db, user_id, session_id, + therapy_name, therapy_id, + duration=None, feedback=None + ) + + # ------------------------------ + # SUGGESTION DETECTION + # ------------------------------ + suggestion_phrases = [ + f"start the {therapy_name.lower()} therapy", + f"try the {therapy_name.lower()} therapy", + f"{therapy_name.lower()} therapy now", + ] + + is_therapy_suggested = any(p in bot_reply.lower() for p in suggestion_phrases) + if action_detected: + is_therapy_suggested = False + + # ------------------------------ + # CLEAN BOT MESSAGE + # ------------------------------ + clean_reply = re.sub( + r"ACTION\s*:\s*START[_\- ]THERAPY\s*:\s*[A-Za-z0-9]+", + "", + bot_reply, + flags=re.IGNORECASE + ).strip() + + # ------------------------------ + # TEXT → SPEECH (NO FILE SAVE) + # ------------------------------ + audio_base64 = None + + tts_res = requests.post( + "https://api.openai.com/v1/audio/speech", + headers={ + "Authorization": f"Bearer {OPENAI_API_KEY}", + "Content-Type": "application/json" + }, + json={"model": "gpt-4o-mini-tts", "input": clean_reply} + ) + + if tts_res.status_code == 200: + import base64 + audio_base64 = base64.b64encode(tts_res.content).decode("utf-8") + + client.close() + + # ------------------------------ + # FINAL RESPONSE + # ------------------------------ + return { + "user_query": user_query, + "bot_response": clean_reply, + + "action": "START_THERAPY" if (action_detected or is_therapy_suggested) else None, + "therapy_id": therapy_id if (action_detected or is_therapy_suggested) else None, + "therapy_name": therapy_name if (action_detected or is_therapy_suggested) else None, + "therapy_description": therapy_description if (action_detected or is_therapy_suggested) else None, + "therapy_path": therapy_path, + + "isTherapySuggested": is_therapy_suggested, + "therapySuggestion": { + "id": therapy_id, + "name": therapy_name, + "path": therapy_path, + } if is_therapy_suggested else None, + + # AUDIO (NO FILE) + "audio_base64": audio_base64 + } + + except Exception as e: + return JSONResponse(status_code=500, content={"error": str(e)}) + + +# ============================================================ +# SERVE AUDIO FILES +# ============================================================ +@voice_therapy_router.get("/audio/{filename}") +async def get_audio(filename: str): + path = f"audio/{filename}" + if not os.path.exists(path): + return JSONResponse(status_code=404, content={"error": "Audio not found"}) + return FileResponse(path, media_type="audio/mpeg") \ No newline at end of file diff --git a/therapyAgent/therapyAgent.py b/therapyAgent/therapyAgent.py index db9c2e59..7b36f91e 100644 --- a/therapyAgent/therapyAgent.py +++ b/therapyAgent/therapyAgent.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter +from fastapi import APIRouter,BackgroundTasks from pydantic import BaseModel from pymongo import MongoClient from langchain_openai import ChatOpenAI @@ -47,7 +47,7 @@ def send_monitor_event(event_name: str, data: dict, user_id: int, session_id: in "timestamp": datetime.utcnow().isoformat() } try: - requests.post(MONITOR_URL, json=payload, timeout=15) + requests.post(MONITOR_URL, json=payload, timeout=0.2) print(f"Logged Therapy Event → {event_name}") except Exception as e: print("Monitor Agent Logging Failed:", e) @@ -86,7 +86,8 @@ def send_end_therapy_event(user_id: int, session_id: int, therapy_id: str, thera # FEEDBACK ENDPOINT # ======================= @router.post("/feedback") -async def save_therapy_feedback(data: TherapyFeedback): +async def save_therapy_feedback( data: TherapyFeedback, + background_tasks: BackgroundTasks): client = MongoClient(key_param.MONGO_URI) db = client["blissMe"] history_collection = db["TherapyHistory"] @@ -119,7 +120,8 @@ async def save_therapy_feedback(data: TherapyFeedback): ) # log event - send_end_therapy_event( + background_tasks.add_task( + send_end_therapy_event, data.user_id, data.session_id, data.therapy_id, @@ -134,28 +136,28 @@ async def save_therapy_feedback(data: TherapyFeedback): # ======================= # CHAT ENDPOINT -# ======================= +# ====================== + @router.post("/chat") -async def therapy_chat(data: TherapyRequest): - """ - Therapy Agent main route: - Handles chat, suggests therapies, tracks user progress. - """ +async def therapy_chat(data: TherapyRequest, background_tasks: BackgroundTasks): client = MongoClient(key_param.MONGO_URI) db = client["blissMe"] + # Fetch therapy history history_records = get_user_therapy_history(db, data.user_id) recent_history = "\n".join( [ - f"{h.get('therapy_name', 'Unknown Therapy')} on {h.get('date', 'Unknown Date')} " - f"(duration {h.get('duration', 'N/A')} mins)" + f"{h['therapy_name']} on {h['date']} (duration {h['duration']} mins)" for h in history_records ] ) if history_records else "No prior therapies found." - # therapy suggestion - therapy_suggestion = get_therapy_recommendation(db, data.depression_level, history_records) + # Get new suggestion + therapy_suggestion = get_therapy_recommendation( + db, data.depression_level, history_records + ) + therapy_name = therapy_suggestion.get("name") therapy_id = therapy_suggestion.get("id") therapy_path = therapy_suggestion.get("path", None) @@ -167,10 +169,14 @@ async def therapy_chat(data: TherapyRequest): ) # ======================= - # LLM prompt + # BASE PROMPT # ======================= prompt = f""" -You are a warm, friendly therapy assistant. +You are a warm, friendly therapy assistant. +Your main job is to support the user emotionally AND suggest a therapy when APPROPRIATE. +Don't suggest therapies every time—only when it fits naturally in the conversation. +If the user seems distressed, PRIORITIZE empathy and understanding first. +DON'T push therapies if the user is not open to it. The user has previous therapy feedback and usage history. You can consider this feedback when recommending therapies. @@ -179,117 +185,122 @@ async def therapy_chat(data: TherapyRequest): {data.therapy_feedback_conclusion or "No feedback summary available."} Rules: -- Keep responses short and caring. +- Keep responses short, caring, simple. - Never mention depression level. -- Suggest a therapy gently when appropriate. -- If suggesting, ask: "Would you like to start the {therapy_name} therapy now?" -If the user agrees you MUST respond: +- Suggest therapies gently when appropriate. +- If the user seems distressed, PRIORITIZE empathy and understanding first. +- If the user declines a therapy, respect their choice and continue the chat supportively. +- Don't suggest therapies every time—only when it fits naturally in the conversation. +- also can ask about previously done therapies, which those helped or not. +- If suggesting a therapy, ask: + "Would you like to start the {therapy_name} therapy now?" + +If the user agrees, you MUST respond with EXACT format: ACTION:START_THERAPY:{therapy_id} User history: {recent_history} -If the user has moderate or minimal depression, suggest small helpful activities or therapies from the system. -Therapies can include relaxation breathing, mindfulness, journaling, or gratitude reflection. -don't use log sentences. keep it short and simple. -don't mention about depression level or depression to the user. - - -If a therapy matches one from the system, gently ask: -"Would you like to start the {therapy_suggestion['name']} therapy now?" - -If the user agrees, return: -ACTION:START_THERAPY:{therapy_suggestion['id']} - -Otherwise, continue gentle conversation and emotional support. - User message: "{data.user_query}" """ bot = ChatOpenAI(model="gpt-3.5-turbo", openai_api_key=key_param.openai_api_key) response = bot.invoke([{"role": "user", "content": prompt}]) - original_reply = response.content.strip() - reply_lower = original_reply.lower() + reply_text = response.content.strip().lower() - # detect suggestion + # =============================================================== + # 1. Detect if model SUGGESTED the therapy (frontend uses this) + # =============================================================== suggestion_phrases = [ - "start the", - "would you like to start", - "try the", - "therapy now", - therapy_name.lower() + f"start the {therapy_name.lower()} therapy", + f"try the {therapy_name.lower()} therapy", + f"{therapy_name.lower()} therapy now", ] - is_therapy_suggested = any(p in reply_lower for p in suggestion_phrases) - - if is_therapy_suggested: - send_monitor_event( - "THERAPY_SUGGESTED", - { - "input": {"user_query": data.user_query}, - "output": {"therapy_id": therapy_id, "therapy_name": therapy_name} - }, - data.user_id, - data.session_id - ) - # detect ACTION - action_pattern = r"action\s*[:\- ]+\s*start[_\- ]?therapy\s*[:\- ]+\s*([A-Za-z0-9]+)" - match = re.search(action_pattern, reply_lower) + is_therapy_suggested = any(p in reply_text for p in suggestion_phrases) + + # =============================================================== + # 2. Detect ACTION:START_THERAPY in ALL POSSIBLE MODEL VARIATIONS + # =============================================================== + # regex covers: + # ACTION:START_THERAPY:ID + # action: start_therapy : id + # Action:Start-Therapy:id + # --------------------------------------------------------------- + action_pattern = r"action\s*:\s*start[_\- ]therapy\s*:\s*([A-Za-z0-9]+)" + + match = re.search(action_pattern, reply_text, re.IGNORECASE) + action_detected = match.group(1).strip() if match else None + # If ACTION detected → save history if action_detected: save_therapy_history( db, data.user_id, data.session_id, - therapy_suggestion["name"], - therapy_suggestion["id"] - ) - - send_therapy_progress_event( - data.user_id, - data.session_id, - therapy_id, therapy_name, - progress=0.0 + therapy_id, + duration=None, + feedback=None ) - is_therapy_suggested = False + background_tasks.add_task( + send_monitor_event, + "THERAPY_STARTED", + { + "input": {}, + "output": { + "therapy_id": therapy_id, + "therapy_name": therapy_name + } + }, + data.user_id, # no int() + data.session_id # no int() + ) + is_therapy_suggested = False # because user already started - client.close() - clean_reply = original_reply.replace("ACTION:START_THERAPY", "").strip() + client.close() + # =============================================== + # FINAL RESPONSE TO FRONTEND (Fully Consistent) + # =============================================== return { - "response": clean_reply, + "response": response.content.replace("ACTION:START_THERAPY", "").strip(), "action": "START_THERAPY" if action_detected else None, "therapy_id": therapy_id if (action_detected or is_therapy_suggested) else None, "therapy_name": therapy_name if (action_detected or is_therapy_suggested) else None, - "therapy_description": therapy_description, + "therapy_description": therapy_description if (action_detected or is_therapy_suggested) else None, "therapy_path": therapy_path, "isTherapySuggested": is_therapy_suggested, "therapySuggestion": { "id": therapy_id, "name": therapy_name, - "path": therapy_path + "path": therapy_path, } if is_therapy_suggested else None, } - class ManualStartRequest(BaseModel): user_id: int session_id: int therapy_id: str therapy_name: str +from fastapi import BackgroundTasks @router.post("/end-start") -async def manual_start_therapy(data: ManualStartRequest): +async def manual_start_therapy( + data: ManualStartRequest, + background_tasks: BackgroundTasks +): client = MongoClient(key_param.MONGO_URI) db = client["blissMe"] - # save history + # ---------------------- + # Save therapy history + # ---------------------- save_therapy_history( db, data.user_id, @@ -300,8 +311,11 @@ async def manual_start_therapy(data: ManualStartRequest): feedback=None ) - # log start - send_monitor_event( + # ---------------------- + # Run monitor logs in background (NON-BLOCKING) + # ---------------------- + background_tasks.add_task( + send_monitor_event, "THERAPY_STARTED", { "input": {}, @@ -314,17 +328,23 @@ async def manual_start_therapy(data: ManualStartRequest): data.session_id ) - # progress 0% - send_therapy_progress_event( + background_tasks.add_task( + send_therapy_progress_event, data.user_id, data.session_id, data.therapy_id, data.therapy_name, - progress=0.0 + 0.0 ) + # ---------------------- + # Close DB immediately + # ---------------------- client.close() + # ---------------------- + # Fast response + # ---------------------- return { "success": True, "message": "Therapy session started", diff --git a/therapyAgent/utils/history_tracker.py b/therapyAgent/utils/history_tracker.py index 1313ddc2..c31a86af 100644 --- a/therapyAgent/utils/history_tracker.py +++ b/therapyAgent/utils/history_tracker.py @@ -1,6 +1,6 @@ from datetime import datetime -def save_therapy_history(db, user_id, session_id, therapy_name, therapy_id): +def save_therapy_history(db, user_id, session_id, therapy_name, therapy_id ,duration=None, feedback=None): history_collection = db["TherapyHistory"] record = { "user_id": user_id, diff --git a/therapyAgent/utils/therapy_selector.py b/therapyAgent/utils/therapy_selector.py index a96653cd..39ad6bc0 100644 --- a/therapyAgent/utils/therapy_selector.py +++ b/therapyAgent/utils/therapy_selector.py @@ -9,14 +9,14 @@ "path": "/therapy/all-songs", "durationMinutes": 6, }, - { - "therapyID": "T002", - "name": "Mindful Breathing", - "applicableLevel": "Moderate", - "description": "A breathing control game designed to synchronize breathing patterns with calming visuals.", - "path": "therapy/breathing", - "durationMinutes": 10, - }, + # { + # "therapyID": "T002", + # "name": "Mindful Breathing", + # "applicableLevel": "Moderate", + # "description": "A breathing control game designed to synchronize breathing patterns with calming visuals.", + # "path": "therapy/breathing", + # "durationMinutes": 10, + # }, { "therapyID": "T003", "name": "Meditation Session", @@ -62,7 +62,7 @@ "name": "Number Guessing Game", "applicableLevel": "Minimal", "description": "A fun and engaging number guessing game to distract and entertain users.", - "path": "/therapy/number-guessing-game", + "path": "/game/therapy_game", "durationMinutes": 10, }, { @@ -81,14 +81,6 @@ "path": "/therapy/zen", "durationMinutes": 5, }, - { - "therapyID": "T0011", - "name": "Anxiety_Games", - "applicableLevel": "Moderate", - "description": "A fun game-based therapy to reduce anxiety levels through interactive relaxation challenges.", - "path": "/dash/anxiety", - "durationMinutes": 15, - }, ] def get_therapy_recommendation(db, depression_level, history_records): diff --git a/voiceChatMode/voice.py b/voiceChatMode/voice.py index be3b4f9f..7a3224a9 100644 --- a/voiceChatMode/voice.py +++ b/voiceChatMode/voice.py @@ -24,7 +24,7 @@ async def voice_chat( asked_phq_ids: str = Form("[]"), history: str = Form(""), summaries: str = Form("[]"), - emotion_history: str = Form("[]") + emotion_history: str = Form("[]"), ): print("\n===== START /voice-chat DEBUG LOG =====\n") @@ -89,7 +89,9 @@ async def voice_chat( user_query=user_query, history=history, summaries=summary_list, - asked_phq_ids=asked_ids + asked_phq_ids=asked_ids, + user_id= 2, + session_id=3 ) ask_result = await ask_question(query_data)