Nada commited on
Commit
b6c5517
·
1 Parent(s): 9ed602b

besm ellah

Browse files
Files changed (11) hide show
  1. .dockerignore +63 -0
  2. .env +26 -0
  3. .gitignore +1 -0
  4. Dockerfile +49 -0
  5. app.py +233 -0
  6. chatbot.py +907 -0
  7. conversation_flow.py +467 -0
  8. guidelines.txt +107 -0
  9. hf_spaces.py +38 -0
  10. requirements.txt +28 -0
  11. start.sh +7 -0
.dockerignore ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ env/
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+ *.swo
33
+
34
+ # Logs
35
+ *.log
36
+ logs/
37
+
38
+ # Local development
39
+ .env
40
+ .env.local
41
+ .env.development
42
+ .env.test
43
+ .env.production
44
+
45
+ # Test files
46
+ tests/
47
+ test_*.py
48
+
49
+ # Documentation
50
+ docs/
51
+ *.md
52
+ !README.md
53
+
54
+
55
+ .cache/
56
+ .pytest_cache/
57
+ .mypy_cache/
58
+
59
+ # Session data
60
+ session_data/
61
+ session_summaries/
62
+ vector_db/
63
+ models/
.env ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Configuration
2
+ MODEL_NAME=meta-llama/Llama-3.2-3B-Instruct
3
+ PEFT_MODEL_PATH=llama_fine_tuned
4
+ GUIDELINES_PATH=guidelines.txt
5
+
6
+ # API Configuration
7
+ API_HOST=0.0.0.0
8
+ API_PORT=8080
9
+ DEBUG=False
10
+
11
+ ALLOWED_ORIGINS=http://localhost:8000
12
+
13
+ # Logging
14
+ LOG_LEVEL=INFO
15
+ LOG_FILE=mental_health_chatbot.log
16
+
17
+ # Additional Configuration
18
+ MAX_SESSION_DURATION=45 # in minutes
19
+ MAX_MESSAGES_PER_SESSION=100000
20
+ SESSION_TIMEOUT=44 # in minutes
21
+ EMOTION_THRESHOLD=0.3 # minimum confidence for emotion detection
22
+
23
+
24
+
25
+
26
+ PORT= 8000
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ offload/
Dockerfile ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.9 slim image
2
+ FROM python:3.9-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y \
6
+ build-essential \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Create necessary directories and set permissions
10
+ RUN mkdir -p /tmp/huggingface && \
11
+ chmod -R 777 /tmp/huggingface
12
+
13
+ # Create a non-root user
14
+ RUN useradd -m -s /bin/bash user && \
15
+ chown -R user:user /tmp/huggingface
16
+
17
+ USER user
18
+ ENV HOME=/home/user \
19
+ PATH=/home/user/.local/bin:$PATH
20
+
21
+ # Set working directory
22
+ WORKDIR $HOME/app
23
+
24
+ # Create app directories
25
+ RUN mkdir -p $HOME/app/session_data $HOME/app/session_summaries $HOME/app/vector_db $HOME/app/models
26
+
27
+ # Copy requirements first for better caching
28
+ COPY --chown=user:user requirements.txt .
29
+ RUN pip install --user --no-cache-dir -r requirements.txt
30
+
31
+ # Copy the rest of the application
32
+ COPY --chown=user:user . .
33
+
34
+ # Make start.sh executable
35
+ RUN chmod +x start.sh
36
+
37
+ # Set environment variables
38
+ ENV PORT=7860
39
+ ENV TRANSFORMERS_CACHE=/tmp/huggingface
40
+ ENV HF_HOME=/tmp/huggingface
41
+ ENV TOKENIZERS_PARALLELISM=false
42
+ ENV TRANSFORMERS_VERBOSITY=error
43
+ ENV BITSANDBYTES_NOWELCOME=1
44
+
45
+ # Expose the port
46
+ EXPOSE 7860
47
+
48
+ # Run the application using start.sh
49
+ CMD ["./start.sh"]
app.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, WebSocket, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import FileResponse
4
+ from pydantic import BaseModel
5
+ from typing import Optional, List, Dict, Any
6
+ import os
7
+ from dotenv import load_dotenv
8
+ from chatbot import MentalHealthChatbot
9
+ from datetime import datetime
10
+ import json
11
+ import uvicorn
12
+ import torch
13
+
14
+ # Load environment variables
15
+ load_dotenv()
16
+
17
+ # Initialize FastAPI app
18
+ app = FastAPI(
19
+ title="Mental Health Chatbot",
20
+ description="mental health support chatbot",
21
+ version="1.0.0"
22
+ )
23
+
24
+ # Add CORS middleware - allow all origins for Hugging Face Spaces
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"], # Allows all origins
28
+ allow_credentials=True,
29
+ allow_methods=["*"], # Allows all methods
30
+ allow_headers=["*"], # Allows all headers
31
+ )
32
+
33
+ # Initialize chatbot with Hugging Face Spaces specific settings
34
+ chatbot = MentalHealthChatbot(
35
+ model_name="meta-llama/Llama-3.2-3B-Instruct",
36
+ peft_model_path="nada013/mental-health-chatbot",
37
+ use_4bit=True, # Enable 4-bit quantization for GPU
38
+ device="cuda" if torch.cuda.is_available() else "cpu", # Use GPU if available
39
+ therapy_guidelines_path="guidelines.txt"
40
+ )
41
+
42
+ # Add GPU memory logging
43
+ if torch.cuda.is_available():
44
+ logger.info(f"GPU Device: {torch.cuda.get_device_name(0)}")
45
+ logger.info(f"Available GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f}GB")
46
+
47
+ # pydantic models
48
+ class MessageRequest(BaseModel):
49
+ user_id: str
50
+ message: str
51
+
52
+ class MessageResponse(BaseModel):
53
+ response: str
54
+ session_id: str
55
+
56
+ class SessionSummary(BaseModel):
57
+ session_id: str
58
+ user_id: str
59
+ start_time: str
60
+ end_time: str
61
+ duration_minutes: float
62
+ current_phase: str
63
+ primary_emotions: List[str]
64
+ emotion_progression: List[str]
65
+ summary: str
66
+ recommendations: List[str]
67
+ session_characteristics: Dict[str, Any]
68
+
69
+ class UserReply(BaseModel):
70
+ text: str
71
+ timestamp: str
72
+ session_id: str
73
+
74
+ class Message(BaseModel):
75
+ text: str
76
+ role: str = "user"
77
+
78
+ # API endpoints
79
+ @app.get("/")
80
+ async def root():
81
+ """Root endpoint with API information."""
82
+ return {
83
+ "name": "Mental Health Chatbot API",
84
+ "version": "1.0.0",
85
+ "description": "API for mental health support chatbot",
86
+ "endpoints": {
87
+ "POST /start_session": "Start a new chat session",
88
+ "POST /send_message": "Send a message to the chatbot",
89
+ "POST /end_session": "End the current session",
90
+ "GET /health": "Health check endpoint",
91
+ "GET /docs": "API documentation (Swagger UI)",
92
+ "GET /redoc": "API documentation (ReDoc)",
93
+ "GET /ws": "WebSocket endpoint"
94
+ }
95
+ }
96
+
97
+ @app.post("/start_session", response_model=MessageResponse)
98
+ async def start_session(user_id: str):
99
+ try:
100
+ session_id, initial_message = chatbot.start_session(user_id)
101
+ return MessageResponse(response=initial_message, session_id=session_id)
102
+ except Exception as e:
103
+ raise HTTPException(status_code=500, detail=str(e))
104
+
105
+ @app.post("/send_message", response_model=MessageResponse)
106
+ async def send_message(request: MessageRequest):
107
+ try:
108
+ response = chatbot.process_message(request.user_id, request.message)
109
+ session = chatbot.conversations[request.user_id]
110
+ return MessageResponse(response=response, session_id=session.session_id)
111
+ except Exception as e:
112
+ raise HTTPException(status_code=500, detail=str(e))
113
+
114
+ @app.post("/end_session", response_model=SessionSummary)
115
+ async def end_session(user_id: str):
116
+ try:
117
+ summary = chatbot.end_session(user_id)
118
+ if not summary:
119
+ raise HTTPException(status_code=404, detail="No active session found")
120
+ return summary
121
+ except Exception as e:
122
+ raise HTTPException(status_code=500, detail=str(e))
123
+
124
+ @app.get("/health")
125
+ async def health_check():
126
+ return {"status": "healthy"}
127
+
128
+ @app.get("/session_summary/{session_id}", response_model=SessionSummary)
129
+ async def get_session_summary(
130
+ session_id: str,
131
+ include_summary: bool = True,
132
+ include_recommendations: bool = True,
133
+ include_emotions: bool = True,
134
+ include_characteristics: bool = True,
135
+ include_duration: bool = True,
136
+ include_phase: bool = True
137
+ ):
138
+ try:
139
+ summary = chatbot.get_session_summary(session_id)
140
+ if not summary:
141
+ raise HTTPException(status_code=404, detail="Session summary not found")
142
+
143
+ filtered_summary = {
144
+ "session_id": summary["session_id"],
145
+ "user_id": summary["user_id"],
146
+ "start_time": summary["start_time"],
147
+ "end_time": summary["end_time"],
148
+ "duration_minutes": summary.get("duration_minutes", 0.0),
149
+ "current_phase": summary.get("current_phase", "unknown"),
150
+ "primary_emotions": summary.get("primary_emotions", []),
151
+ "emotion_progression": summary.get("emotion_progression", []),
152
+ "summary": summary.get("summary", ""),
153
+ "recommendations": summary.get("recommendations", []),
154
+ "session_characteristics": summary.get("session_characteristics", {})
155
+ }
156
+
157
+ # Filter out fields based on include parameters
158
+ if not include_summary:
159
+ filtered_summary["summary"] = ""
160
+ if not include_recommendations:
161
+ filtered_summary["recommendations"] = []
162
+ if not include_emotions:
163
+ filtered_summary["primary_emotions"] = []
164
+ filtered_summary["emotion_progression"] = []
165
+ if not include_characteristics:
166
+ filtered_summary["session_characteristics"] = {}
167
+ if not include_duration:
168
+ filtered_summary["duration_minutes"] = 0.0
169
+ if not include_phase:
170
+ filtered_summary["current_phase"] = "unknown"
171
+
172
+ return filtered_summary
173
+ except Exception as e:
174
+ raise HTTPException(status_code=500, detail=str(e))
175
+
176
+ @app.get("/user_replies/{user_id}")
177
+ async def get_user_replies(user_id: str):
178
+ try:
179
+ replies = chatbot.get_user_replies(user_id)
180
+
181
+ # Create a filename with user_id and timestamp
182
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
183
+ filename = f"user_replies_{user_id}_{timestamp}.json"
184
+ filepath = os.path.join("user_replies", filename)
185
+
186
+ # Ensure directory exists
187
+ os.makedirs("user_replies", exist_ok=True)
188
+
189
+ # Write replies to JSON file
190
+ with open(filepath, 'w') as f:
191
+ json.dump({
192
+ "user_id": user_id,
193
+ "timestamp": datetime.now().isoformat(),
194
+ "replies": replies
195
+ }, f, indent=2)
196
+
197
+ # Return the file
198
+ return FileResponse(
199
+ path=filepath,
200
+ filename=filename,
201
+ media_type="application/json"
202
+ )
203
+ except Exception as e:
204
+ raise HTTPException(status_code=500, detail=str(e))
205
+
206
+ @app.websocket("/ws")
207
+ async def websocket_endpoint(websocket: WebSocket):
208
+ await websocket.accept()
209
+ try:
210
+ while True:
211
+ data = await websocket.receive_json()
212
+ user_id = data.get("user_id")
213
+ message = data.get("message")
214
+
215
+ if not user_id or not message:
216
+ await websocket.send_json({"error": "Missing user_id or message"})
217
+ continue
218
+
219
+ response = chatbot.process_message(user_id, message)
220
+ session_id = chatbot.conversations[user_id].session_id
221
+
222
+ await websocket.send_json({
223
+ "response": response,
224
+ "session_id": session_id
225
+ })
226
+ except Exception as e:
227
+ await websocket.send_json({"error": str(e)})
228
+ finally:
229
+ await websocket.close()
230
+
231
+ if __name__ == "__main__":
232
+ port = int(os.getenv("PORT", 7860))
233
+ uvicorn.run(app, host="0.0.0.0", port=port)
chatbot.py ADDED
@@ -0,0 +1,907 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import json
4
+ import torch
5
+ import re
6
+ from typing import List, Dict, Any, Optional, Union
7
+ from datetime import datetime
8
+ from pydantic import BaseModel, Field
9
+ import tempfile
10
+
11
+ # Model imports
12
+ from transformers import (
13
+ pipeline,
14
+ AutoTokenizer,
15
+ AutoModelForCausalLM,
16
+ BitsAndBytesConfig
17
+ )
18
+ from peft import PeftModel, PeftConfig
19
+ from sentence_transformers import SentenceTransformer
20
+
21
+ # LangChain imports
22
+ from langchain.llms import HuggingFacePipeline
23
+ from langchain.chains import LLMChain
24
+ from langchain.memory import ConversationBufferMemory
25
+ from langchain.prompts import PromptTemplate
26
+ from langchain.embeddings import HuggingFaceEmbeddings
27
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
28
+ from langchain.document_loaders import TextLoader
29
+ from langchain.vectorstores import FAISS
30
+
31
+ # Import FlowManager
32
+ from conversation_flow import FlowManager
33
+
34
+ # Configure logging
35
+ logging.basicConfig(
36
+ level=logging.INFO,
37
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
38
+ handlers=[logging.StreamHandler()]
39
+ )
40
+ logger = logging.getLogger(__name__)
41
+
42
+ # Suppress warnings
43
+ import warnings
44
+ warnings.filterwarnings('ignore', category=UserWarning)
45
+
46
+ # Set up cache directories
47
+ def setup_cache_dirs():
48
+ # Check if running in Hugging Face Spaces
49
+ is_spaces = os.environ.get('SPACE_ID') is not None
50
+
51
+ if is_spaces:
52
+ # Use /tmp for Hugging Face Spaces with proper permissions
53
+ cache_dir = '/tmp/huggingface'
54
+ os.environ.update({
55
+ 'TRANSFORMERS_CACHE': cache_dir,
56
+ 'HF_HOME': cache_dir,
57
+ 'TOKENIZERS_PARALLELISM': 'false',
58
+ 'TRANSFORMERS_VERBOSITY': 'error',
59
+ 'BITSANDBYTES_NOWELCOME': '1',
60
+ 'HF_DATASETS_CACHE': cache_dir,
61
+ 'HF_METRICS_CACHE': cache_dir,
62
+ 'HF_MODULES_CACHE': cache_dir,
63
+ 'HUGGING_FACE_HUB_TOKEN': os.environ.get('HF_TOKEN', ''),
64
+ 'HF_TOKEN': os.environ.get('HF_TOKEN', '')
65
+ })
66
+ else:
67
+ # Use default cache for local development
68
+ cache_dir = os.path.expanduser('~/.cache/huggingface')
69
+ os.environ.update({
70
+ 'TOKENIZERS_PARALLELISM': 'false',
71
+ 'TRANSFORMERS_VERBOSITY': 'error',
72
+ 'BITSANDBYTES_NOWELCOME': '1'
73
+ })
74
+
75
+ # Create cache directory if it doesn't exist
76
+ os.makedirs(cache_dir, exist_ok=True)
77
+
78
+ return cache_dir
79
+
80
+ # Set up cache directories
81
+ CACHE_DIR = setup_cache_dirs()
82
+
83
+ # Define base directory and paths
84
+ BASE_DIR = os.path.abspath(os.path.dirname(__file__))
85
+ MODELS_DIR = os.path.join(BASE_DIR, "models")
86
+ VECTOR_DB_PATH = os.path.join(BASE_DIR, "vector_db")
87
+ SESSION_DATA_PATH = os.path.join(BASE_DIR, "session_data")
88
+ SUMMARIES_DIR = os.path.join(BASE_DIR, "session_summaries")
89
+
90
+ # Create necessary directories
91
+ for directory in [MODELS_DIR, VECTOR_DB_PATH, SESSION_DATA_PATH, SUMMARIES_DIR]:
92
+ os.makedirs(directory, exist_ok=True)
93
+
94
+ # Pydantic models
95
+ class Message(BaseModel):
96
+ text: str = Field(..., description="The content of the message")
97
+ timestamp: str = Field(None, description="ISO format timestamp of the message")
98
+ role: str = Field("user", description="The role of the message sender (user or assistant)")
99
+
100
+ class SessionSummary(BaseModel):
101
+ session_id: str = Field(
102
+ ...,
103
+ description="Unique identifier for the session",
104
+ examples=["user_789_session_20240314"]
105
+ )
106
+ user_id: str = Field(
107
+ ...,
108
+ description="Identifier of the user",
109
+ examples=["user_123"]
110
+ )
111
+ start_time: str = Field(
112
+ ...,
113
+ description="ISO format start time of the session"
114
+ )
115
+ end_time: str = Field(
116
+ ...,
117
+ description="ISO format end time of the session"
118
+ )
119
+ message_count: int = Field(
120
+ ...,
121
+ description="Total number of messages in the session"
122
+ )
123
+ duration_minutes: float = Field(
124
+ ...,
125
+ description="Duration of the session in minutes"
126
+ )
127
+ primary_emotions: List[str] = Field(
128
+ ...,
129
+ min_items=1,
130
+ description="List of primary emotions detected",
131
+ examples=[
132
+ ["anxiety", "stress"],
133
+ ["joy", "excitement"],
134
+ ["sadness", "loneliness"]
135
+ ]
136
+ )
137
+ emotion_progression: List[Dict[str, float]] = Field(
138
+ ...,
139
+ description="Progression of emotions throughout the session",
140
+ examples=[
141
+ [
142
+ {"anxiety": 0.8, "stress": 0.6},
143
+ {"calm": 0.7, "anxiety": 0.3},
144
+ {"joy": 0.9, "calm": 0.8}
145
+ ]
146
+ ]
147
+ )
148
+ summary_text: str = Field(
149
+ ...,
150
+ description="Text summary of the session",
151
+ examples=[
152
+ "The session focused on managing work-related stress and developing coping strategies. The client showed improvement in recognizing stress triggers and implementing relaxation techniques.",
153
+ "Discussion centered around relationship challenges and self-esteem issues. The client expressed willingness to try new communication strategies."
154
+ ]
155
+ )
156
+ recommendations: Optional[List[str]] = Field(
157
+ None,
158
+ description="Optional recommendations based on the session"
159
+ )
160
+
161
+ class Conversation(BaseModel):
162
+ user_id: str = Field(
163
+ ...,
164
+ description="Identifier of the user",
165
+ examples=["user_123"]
166
+ )
167
+ session_id: str = Field(
168
+ "",
169
+ description="Identifier of the current session"
170
+ )
171
+ start_time: str = Field(
172
+ "",
173
+ description="ISO format start time of the conversation"
174
+ )
175
+ messages: List[Message] = Field(
176
+ [],
177
+ description="List of messages in the conversation",
178
+ examples=[
179
+ [
180
+ Message(text="I'm feeling anxious", role="user"),
181
+ Message(text="I understand you're feeling anxious. Can you tell me more about what's causing this?", role="assistant")
182
+ ]
183
+ ]
184
+ )
185
+ emotion_history: List[Dict[str, float]] = Field(
186
+ [],
187
+ description="History of emotions detected",
188
+ examples=[
189
+ [
190
+ {"anxiety": 0.8, "stress": 0.6},
191
+ {"calm": 0.7, "anxiety": 0.3}
192
+ ]
193
+ ]
194
+ )
195
+ context: Dict[str, Any] = Field(
196
+ {},
197
+ description="Additional context for the conversation",
198
+ examples=[
199
+ {
200
+ "last_emotion": "anxiety",
201
+ "conversation_topic": "work stress",
202
+ "previous_sessions": 3
203
+ }
204
+ ]
205
+ )
206
+ is_active: bool = Field(
207
+ True,
208
+ description="Whether the conversation is currently active",
209
+ examples=[True, False]
210
+ )
211
+
212
+ class MentalHealthChatbot:
213
+ def __init__(
214
+ self,
215
+ model_name: str = "meta-llama/Llama-3.2-3B-Instruct",
216
+ peft_model_path: str = "nada013/mental-health-chatbot",
217
+ therapy_guidelines_path: str = None,
218
+ use_4bit: bool = True,
219
+ device: str = None
220
+ ):
221
+ # Set device (cuda if available, otherwise cpu)
222
+ if device is None:
223
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
224
+ else:
225
+ self.device = device
226
+
227
+ # Set memory optimization for T4
228
+ if self.device == "cuda":
229
+ torch.cuda.empty_cache() # Clear GPU cache
230
+ # Set smaller batch size for T4
231
+ self.batch_size = 4
232
+ # Enable memory efficient attention
233
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:128"
234
+ else:
235
+ self.batch_size = 8
236
+
237
+ logger.info(f"Using device: {self.device}")
238
+
239
+ # Initialize models
240
+ self.peft_model_path = peft_model_path
241
+
242
+ # Initialize emotion detection model
243
+ logger.info("Loading emotion detection model")
244
+ self.emotion_classifier = self._load_emotion_model()
245
+
246
+ # Initialize LLAMA model
247
+ logger.info(f"Loading LLAMA model: {model_name}")
248
+ self.llama_model, self.llama_tokenizer, self.llm = self._initialize_llm(model_name, use_4bit)
249
+
250
+ # Initialize summary model
251
+ logger.info("Loading summary model")
252
+ self.summary_model = pipeline(
253
+ "summarization",
254
+ model="philschmid/bart-large-cnn-samsum",
255
+ device=0 if self.device == "cuda" else -1,
256
+ model_kwargs={
257
+ "cache_dir": CACHE_DIR,
258
+ "torch_dtype": torch.float16,
259
+ "max_memory": {0: "2GB"} if self.device == "cuda" else None
260
+ }
261
+ )
262
+ logger.info("Summary model loaded successfully")
263
+
264
+ # Initialize FlowManager
265
+ logger.info("Initializing FlowManager")
266
+ self.flow_manager = FlowManager(self.llm)
267
+
268
+ # Setup conversation memory with LangChain
269
+ self.memory = ConversationBufferMemory(
270
+ return_messages=True,
271
+ input_key="input"
272
+ )
273
+
274
+ # Create conversation prompt template
275
+ self.prompt_template = PromptTemplate(
276
+ input_variables=["history", "input", "past_context", "emotion_context", "guidelines"],
277
+ template="""You are a supportive and empathetic mental health conversational AI. Your role is to provide therapeutic support while maintaining professional boundaries.
278
+
279
+ Previous conversation:
280
+ {history}
281
+
282
+ EMOTIONAL CONTEXT:
283
+ {emotion_context}
284
+
285
+ Past context: {past_context}
286
+
287
+ Relevant therapeutic guidelines:
288
+ {guidelines}
289
+
290
+ Current message: {input}
291
+
292
+ Provide a supportive response that:
293
+ 1. Validates the user's feelings without using casual greetings
294
+ 2. Asks relevant follow-up questions
295
+ 3. Maintains a conversational tone , professional and empathetic tone
296
+ 4. Focuses on understanding and support
297
+ 5. Avoids repeating previous responses
298
+
299
+ Response:"""
300
+ )
301
+
302
+ # Create the conversation chain
303
+ self.conversation = LLMChain(
304
+ llm=self.llm,
305
+ prompt=self.prompt_template,
306
+ memory=self.memory,
307
+ verbose=False
308
+ )
309
+
310
+ # Setup embeddings for vector search
311
+ self.embeddings = HuggingFaceEmbeddings(
312
+ model_name="sentence-transformers/all-MiniLM-L6-v2"
313
+ )
314
+
315
+ # Setup vector database for retrieving relevant past conversations
316
+ if therapy_guidelines_path and os.path.exists(therapy_guidelines_path):
317
+ self.setup_vector_db(therapy_guidelines_path)
318
+ else:
319
+ self.setup_vector_db(None)
320
+
321
+ # Initialize conversation storage
322
+ self.conversations = {}
323
+
324
+ # Load existing session summaries
325
+ self.session_summaries = {}
326
+ self._load_existing_summaries()
327
+
328
+ logger.info("All models and components initialized successfully")
329
+
330
+ def _load_emotion_model(self):
331
+ try:
332
+ # Load emotion model directly from Hugging Face
333
+ return pipeline(
334
+ "text-classification",
335
+ model="SamLowe/roberta-base-go_emotions",
336
+ top_k=None,
337
+ device_map="auto" if torch.cuda.is_available() else None,
338
+ model_kwargs={
339
+ "cache_dir": CACHE_DIR,
340
+ "torch_dtype": torch.float16, # Use float16
341
+ "max_memory": {0: "2GB"} if torch.cuda.is_available() else None # Limit memory usage
342
+ },
343
+ )
344
+ except Exception as e:
345
+ logger.error(f"Error loading emotion model: {e}")
346
+ # Fallback to a simpler model
347
+ try:
348
+ return pipeline(
349
+ "text-classification",
350
+ model="j-hartmann/emotion-english-distilroberta-base",
351
+ return_all_scores=True,
352
+ device_map="auto" if torch.cuda.is_available() else None,
353
+ model_kwargs={
354
+ "cache_dir": CACHE_DIR,
355
+ "torch_dtype": torch.float16,
356
+ "max_memory": {0: "2GB"} if torch.cuda.is_available() else None
357
+ },
358
+ )
359
+ except Exception as e:
360
+ logger.error(f"Error loading fallback emotion model: {e}")
361
+ # Return a simple pipeline that always returns neutral
362
+ return lambda text: [{"label": "neutral", "score": 1.0}]
363
+
364
+ def _initialize_llm(self, model_name: str, use_4bit: bool):
365
+ try:
366
+ # Configure quantization only if CUDA is available
367
+ if use_4bit and torch.cuda.is_available():
368
+ quantization_config = BitsAndBytesConfig(
369
+ load_in_4bit=True,
370
+ bnb_4bit_compute_dtype=torch.float16,
371
+ bnb_4bit_quant_type="nf4",
372
+ bnb_4bit_use_double_quant=True,
373
+ )
374
+
375
+ # Set max memory for T4 GPU
376
+ max_memory = {0: "14GB"} # Leave 2GB buffer for other operations
377
+ else:
378
+ quantization_config = None
379
+ max_memory = None
380
+ logger.info("CUDA not available, running without quantization")
381
+
382
+ # Load base model
383
+ logger.info(f"Loading base model: {model_name}")
384
+ base_model = AutoModelForCausalLM.from_pretrained(
385
+ model_name,
386
+ quantization_config=quantization_config,
387
+ device_map="auto",
388
+ max_memory=max_memory,
389
+ trust_remote_code=True,
390
+ cache_dir=CACHE_DIR,
391
+ use_auth_token=os.environ.get('HF_TOKEN'),
392
+ torch_dtype=torch.float16 # Use float16 for better memory efficiency
393
+ )
394
+
395
+ # Load tokenizer
396
+ logger.info("Loading tokenizer")
397
+ tokenizer = AutoTokenizer.from_pretrained(
398
+ model_name,
399
+ cache_dir=CACHE_DIR,
400
+ use_auth_token=os.environ.get('HF_TOKEN') # Add auth token for gated models
401
+ )
402
+ tokenizer.pad_token = tokenizer.eos_token
403
+
404
+ # Load PEFT model
405
+ logger.info(f"Loading PEFT model from {self.peft_model_path}")
406
+ model = PeftModel.from_pretrained(
407
+ base_model,
408
+ self.peft_model_path,
409
+ cache_dir=CACHE_DIR,
410
+ use_auth_token=os.environ.get('HF_TOKEN') # Add auth token for gated models
411
+ )
412
+ logger.info("Successfully loaded PEFT model")
413
+
414
+ # Create text generation pipeline
415
+ text_generator = pipeline(
416
+ "text-generation",
417
+ model=model,
418
+ tokenizer=tokenizer,
419
+ max_new_tokens=512,
420
+ temperature=0.7,
421
+ top_p=0.95,
422
+ repetition_penalty=1.1,
423
+ do_sample=True,
424
+ device_map="auto" if torch.cuda.is_available() else None
425
+ )
426
+
427
+ # Create LangChain wrapper
428
+ llm = HuggingFacePipeline(pipeline=text_generator)
429
+
430
+ return model, tokenizer, llm
431
+
432
+ except Exception as e:
433
+ logger.error(f"Error initializing LLM: {str(e)}")
434
+ raise
435
+
436
+ def setup_vector_db(self, guidelines_path: str = None):
437
+
438
+ logger.info("Setting up FAISS vector database")
439
+
440
+ # Check if vector DB exists
441
+ vector_db_exists = os.path.exists(os.path.join(VECTOR_DB_PATH, "index.faiss"))
442
+
443
+ if not vector_db_exists:
444
+ # Load therapy guidelines
445
+ if guidelines_path and os.path.exists(guidelines_path):
446
+ loader = TextLoader(guidelines_path)
447
+ documents = loader.load()
448
+
449
+ # Split documents into chunks with better overlap for context
450
+ text_splitter = RecursiveCharacterTextSplitter(
451
+ chunk_size=500, # Smaller chunks for more precise retrieval
452
+ chunk_overlap=100,
453
+ separators=["\n\n", "\n", " ", ""]
454
+ )
455
+ chunks = text_splitter.split_documents(documents)
456
+
457
+ # Create and save the vector store
458
+ self.vector_db = FAISS.from_documents(chunks, self.embeddings)
459
+ self.vector_db.save_local(VECTOR_DB_PATH)
460
+ logger.info("Successfully loaded and indexed therapy guidelines")
461
+ else:
462
+ # Initialize with empty vector DB
463
+ self.vector_db = FAISS.from_texts(["Initial empty vector store"], self.embeddings)
464
+ self.vector_db.save_local(VECTOR_DB_PATH)
465
+ logger.warning("No guidelines file provided, using empty vector store")
466
+ else:
467
+ # Load existing vector DB
468
+ self.vector_db = FAISS.load_local(VECTOR_DB_PATH, self.embeddings, allow_dangerous_deserialization=True)
469
+ logger.info("Loaded existing vector database")
470
+
471
+ def _load_existing_summaries(self):
472
+ if not os.path.exists(SUMMARIES_DIR):
473
+ return
474
+
475
+ for filename in os.listdir(SUMMARIES_DIR):
476
+ if filename.endswith('.json'):
477
+ try:
478
+ with open(os.path.join(SUMMARIES_DIR, filename), 'r') as f:
479
+ summary_data = json.load(f)
480
+ session_id = summary_data.get('session_id')
481
+ if session_id:
482
+ self.session_summaries[session_id] = summary_data
483
+ except Exception as e:
484
+ logger.warning(f"Failed to load summary from {filename}: {e}")
485
+
486
+ def detect_emotion(self, text: str) -> Dict[str, float]:
487
+ try:
488
+ results = self.emotion_classifier(text)[0]
489
+ return {result['label']: result['score'] for result in results}
490
+ except Exception as e:
491
+ logger.error(f"Error detecting emotions: {e}")
492
+ return {"neutral": 1.0}
493
+
494
+ def retrieve_relevant_context(self, query: str, k: int = 3) -> str:
495
+ # Retrieve relevant past conversations using vector similarity
496
+ if not hasattr(self, 'vector_db'):
497
+ return ""
498
+
499
+ try:
500
+ # Retrieve similar documents from vector DB
501
+ docs = self.vector_db.similarity_search(query, k=k)
502
+
503
+ # Combine the content of retrieved documents
504
+ relevant_context = "\n".join([doc.page_content for doc in docs])
505
+ return relevant_context
506
+ except Exception as e:
507
+ logger.error(f"Error retrieving context: {e}")
508
+ return ""
509
+
510
+ def retrieve_relevant_guidelines(self, query: str, emotion_context: str) -> str:
511
+ if not hasattr(self, 'vector_db'):
512
+ return ""
513
+
514
+ try:
515
+ # Combine query and emotion context for better relevance
516
+ search_query = f"{query} {emotion_context}"
517
+
518
+ # Retrieve similar documents from vector DB
519
+ docs = self.vector_db.similarity_search(search_query, k=2)
520
+
521
+ # Combine the content of retrieved documents
522
+ relevant_guidelines = "\n".join([doc.page_content for doc in docs])
523
+ return relevant_guidelines
524
+ except Exception as e:
525
+ logger.error(f"Error retrieving guidelines: {e}")
526
+ return ""
527
+
528
+ def generate_response(self, prompt: str, emotion_data: Dict[str, float], conversation_history: List[Dict]) -> str:
529
+
530
+ # Get primary and secondary emotions
531
+ sorted_emotions = sorted(emotion_data.items(), key=lambda x: x[1], reverse=True)
532
+ primary_emotion = sorted_emotions[0][0] if sorted_emotions else "neutral"
533
+
534
+ # Get secondary emotions (if any)
535
+ secondary_emotions = []
536
+ for emotion, score in sorted_emotions[1:3]: # Get 2nd and 3rd strongest emotions
537
+ if score > 0.2: # Only include if reasonably strong
538
+ secondary_emotions.append(emotion)
539
+
540
+ # Create emotion context string
541
+ emotion_context = f"User is primarily feeling {primary_emotion}"
542
+ if secondary_emotions:
543
+ emotion_context += f" with elements of {' and '.join(secondary_emotions)}"
544
+ emotion_context += "."
545
+
546
+ # Retrieve relevant guidelines
547
+ guidelines = self.retrieve_relevant_guidelines(prompt, emotion_context)
548
+
549
+ # Retrieve past context
550
+ past_context = self.retrieve_relevant_context(prompt)
551
+
552
+ # Generate response using the conversation chain
553
+ response = self.conversation.predict(
554
+ input=prompt,
555
+ past_context=past_context,
556
+ emotion_context=emotion_context,
557
+ guidelines=guidelines
558
+ )
559
+
560
+ # Clean up the response to only include the actual message
561
+ response = response.split("Response:")[-1].strip()
562
+ response = response.split("---")[0].strip()
563
+ response = response.split("Note:")[0].strip()
564
+
565
+ # Remove any casual greetings like "Hey" or "Hi"
566
+ response = re.sub(r'^(Hey|Hi|Hello|Hi there|Hey there),\s*', '', response)
567
+
568
+ # Ensure the response is unique and not repeating previous messages
569
+ if len(conversation_history) > 0:
570
+ last_responses = [msg["text"] for msg in conversation_history[-4:] if msg["role"] == "assistant"]
571
+ if response in last_responses:
572
+ # Generate a new response with a different angle
573
+ response = self.conversation.predict(
574
+ input=f"{prompt} (Please provide a different perspective)",
575
+ past_context=past_context,
576
+ emotion_context=emotion_context,
577
+ guidelines=guidelines
578
+ )
579
+ response = response.split("Response:")[-1].strip()
580
+ response = re.sub(r'^(Hey|Hi|Hello|Hi there|Hey there),\s*', '', response)
581
+
582
+ return response.strip()
583
+
584
+ def generate_session_summary(
585
+ self,
586
+ flow_manager_session: Dict = None
587
+ ) -> Dict:
588
+
589
+ if not flow_manager_session:
590
+ return {
591
+ "session_id": "",
592
+ "user_id": "",
593
+ "start_time": "",
594
+ "end_time": datetime.now().isoformat(),
595
+ "duration_minutes": 0,
596
+ "current_phase": "unknown",
597
+ "primary_emotions": [],
598
+ "emotion_progression": [],
599
+ "summary": "Error: No session data provided",
600
+ "recommendations": ["Unable to generate recommendations"],
601
+ "session_characteristics": {}
602
+ }
603
+
604
+ # Get session data from FlowManager
605
+ session_id = flow_manager_session.get('session_id', '')
606
+ user_id = flow_manager_session.get('user_id', '')
607
+ current_phase = flow_manager_session.get('current_phase')
608
+
609
+ if current_phase:
610
+ # Convert ConversationPhase to dict
611
+ current_phase = {
612
+ 'name': current_phase.name,
613
+ 'description': current_phase.description,
614
+ 'goals': current_phase.goals,
615
+ 'started_at': current_phase.started_at,
616
+ 'ended_at': current_phase.ended_at,
617
+ 'completion_metrics': current_phase.completion_metrics
618
+ }
619
+
620
+ session_start = flow_manager_session.get('started_at')
621
+ if isinstance(session_start, str):
622
+ session_start = datetime.fromisoformat(session_start)
623
+ session_duration = (datetime.now() - session_start).total_seconds() / 60 if session_start else 0
624
+
625
+ # Get emotion progression and primary emotions
626
+ emotion_progression = flow_manager_session.get('emotion_progression', [])
627
+ emotion_history = flow_manager_session.get('emotion_history', [])
628
+
629
+ # Extract primary emotions from emotion history
630
+ primary_emotions = []
631
+ if emotion_history:
632
+ # Get the most frequent emotions
633
+ emotion_counts = {}
634
+ for entry in emotion_history:
635
+ emotions = entry.get('emotions', {})
636
+ if isinstance(emotions, dict):
637
+ primary = max(emotions.items(), key=lambda x: x[1])[0]
638
+ emotion_counts[primary] = emotion_counts.get(primary, 0) + 1
639
+
640
+ # sort by frequency and get top 3
641
+ primary_emotions = sorted(emotion_counts.items(), key=lambda x: x[1], reverse=True)[:3]
642
+ primary_emotions = [emotion for emotion, _ in primary_emotions]
643
+
644
+ # get session
645
+ session_characteristics = flow_manager_session.get('llm_context', {}).get('session_characteristics', {})
646
+
647
+ # prepare the text for summarization
648
+ summary_text = f"""
649
+ Session Overview:
650
+ - Session ID: {session_id}
651
+ - User ID: {user_id}
652
+ - Phase: {current_phase.get('name', 'unknown') if current_phase else 'unknown'}
653
+ - Duration: {session_duration:.1f} minutes
654
+
655
+ Emotional Analysis:
656
+ - Primary Emotions: {', '.join(primary_emotions) if primary_emotions else 'No primary emotions detected'}
657
+ - Emotion Progression: {', '.join(emotion_progression) if emotion_progression else 'No significant emotion changes noted'}
658
+
659
+ Session Characteristics:
660
+ - Therapeutic Alliance: {session_characteristics.get('alliance_strength', 'N/A')}
661
+ - Engagement Level: {session_characteristics.get('engagement_level', 'N/A')}
662
+ - Emotional Pattern: {session_characteristics.get('emotional_pattern', 'N/A')}
663
+ - Cognitive Pattern: {session_characteristics.get('cognitive_pattern', 'N/A')}
664
+
665
+ Key Observations:
666
+ - The session focused on {current_phase.get('description', 'general discussion') if current_phase else 'general discussion'}
667
+ - Main emotional themes: {', '.join(primary_emotions) if primary_emotions else 'not identified'}
668
+ - Session progress: {session_characteristics.get('progress_quality', 'N/A')}
669
+ """
670
+
671
+ # Generate summary using BART
672
+ summary = self.summary_model(
673
+ summary_text,
674
+ max_length=150,
675
+ min_length=50,
676
+ do_sample=False
677
+ )[0]['summary_text']
678
+
679
+ # Generate recommendations using Llama
680
+ recommendations_prompt = f"""
681
+ Based on the following session summary, provide 2-3 specific recommendations for follow-up:
682
+
683
+ {summary}
684
+
685
+ Session Characteristics:
686
+ - Therapeutic Alliance: {session_characteristics.get('alliance_strength', 'N/A')}
687
+ - Engagement Level: {session_characteristics.get('engagement_level', 'N/A')}
688
+ - Emotional Pattern: {session_characteristics.get('emotional_pattern', 'N/A')}
689
+ - Cognitive Pattern: {session_characteristics.get('cognitive_pattern', 'N/A')}
690
+
691
+ Recommendations should be:
692
+ 1. Actionable and specific
693
+ 2. Based on the session content
694
+ 3. Focused on next steps
695
+ """
696
+
697
+ recommendations = self.llm.invoke(recommendations_prompt)
698
+
699
+
700
+ recommendations = recommendations.split('\n')
701
+ recommendations = [r.strip() for r in recommendations if r.strip()]
702
+ recommendations = [r for r in recommendations if not r.startswith(('Based on', 'Session', 'Recommendations'))]
703
+
704
+
705
+ return {
706
+ "session_id": session_id,
707
+ "user_id": user_id,
708
+ "start_time": session_start.isoformat() if isinstance(session_start, datetime) else str(session_start),
709
+ "end_time": datetime.now().isoformat(),
710
+ "duration_minutes": session_duration,
711
+ "current_phase": current_phase.get('name', 'unknown') if current_phase else 'unknown',
712
+ "primary_emotions": primary_emotions,
713
+ "emotion_progression": emotion_progression,
714
+ "summary": summary,
715
+ "recommendations": recommendations,
716
+ "session_characteristics": session_characteristics
717
+ }
718
+
719
+ def start_session(self, user_id: str) -> tuple[str, str]:
720
+ # Generate session id
721
+ session_id = f"{user_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
722
+
723
+ # Initialize FlowManager session
724
+ self.flow_manager.initialize_session(user_id)
725
+
726
+ # Create a new conversation
727
+ self.conversations[user_id] = Conversation(
728
+ user_id=user_id,
729
+ session_id=session_id,
730
+ start_time=datetime.now().isoformat(),
731
+ is_active=True
732
+ )
733
+
734
+ # Clear conversation memory
735
+ self.memory.clear()
736
+
737
+ # Generate initial greeting and question
738
+ initial_message = """Hello! I'm here to support you today. How have you been feeling lately?"""
739
+
740
+ # Add the initial message to conversation history
741
+ assistant_message = Message(
742
+ text=initial_message,
743
+ timestamp=datetime.now().isoformat(),
744
+ role="assistant"
745
+ )
746
+ self.conversations[user_id].messages.append(assistant_message)
747
+
748
+ logger.info(f"Session started for user {user_id}")
749
+ return session_id, initial_message
750
+
751
+ def end_session(
752
+ self,
753
+ user_id: str,
754
+ flow_manager: Optional[Any] = None
755
+ ) -> Optional[Dict]:
756
+
757
+ if user_id not in self.conversations or not self.conversations[user_id].is_active:
758
+ return None
759
+
760
+ conversation = self.conversations[user_id]
761
+ conversation.is_active = False
762
+
763
+ # Get FlowManager session data
764
+ flow_manager_session = self.flow_manager.user_sessions.get(user_id)
765
+
766
+ # Generate session summary
767
+ try:
768
+ session_summary = self.generate_session_summary(flow_manager_session)
769
+
770
+ # Save summary to disk
771
+ summary_path = os.path.join(SUMMARIES_DIR, f"{session_summary['session_id']}.json")
772
+ with open(summary_path, 'w') as f:
773
+ json.dump(session_summary, f, indent=2)
774
+
775
+ # Store in memory
776
+ self.session_summaries[session_summary['session_id']] = session_summary
777
+
778
+ # Clear conversation memory
779
+ self.memory.clear()
780
+
781
+ return session_summary
782
+ except Exception as e:
783
+ logger.error(f"Failed to generate session summary: {e}")
784
+ return None
785
+
786
+ def process_message(self, user_id: str, message: str) -> str:
787
+
788
+ # Check for risk flags
789
+ risk_keywords = ["suicide", "kill myself", "end my life", "self-harm", "hurt myself"]
790
+ risk_detected = any(keyword in message.lower() for keyword in risk_keywords)
791
+
792
+ # Create or get conversation
793
+ if user_id not in self.conversations or not self.conversations[user_id].is_active:
794
+ self.start_session(user_id)
795
+
796
+ conversation = self.conversations[user_id]
797
+
798
+ # user message -> conversation history
799
+ new_message = Message(
800
+ text=message,
801
+ timestamp=datetime.now().isoformat(),
802
+ role="user"
803
+ )
804
+ conversation.messages.append(new_message)
805
+
806
+ # For crisis
807
+ if risk_detected:
808
+ logger.warning(f"Risk flag detected in session {user_id}")
809
+
810
+ crisis_response = """ I'm really sorry you're feeling this way — it sounds incredibly heavy, and I want you to know that you're not alone.
811
+
812
+ You don't have to face this by yourself. Our app has licensed mental health professionals who are ready to support you. I can connect you right now if you'd like.
813
+
814
+ In the meantime, I'm here to listen and talk with you. You can also do grounding exercises or calming techniques with me if you prefer. Just say "help me calm down" or "I need a break."
815
+
816
+ Would you like to connect with a professional now, or would you prefer to keep talking with me for a bit? Either way, I'm here for you."""
817
+
818
+ # assistant response -> conversation history
819
+ assistant_message = Message(
820
+ text=crisis_response,
821
+ timestamp=datetime.now().isoformat(),
822
+ role="assistant"
823
+ )
824
+ conversation.messages.append(assistant_message)
825
+
826
+ return crisis_response
827
+
828
+ # Detect emotions
829
+ emotions = self.detect_emotion(message)
830
+ conversation.emotion_history.append(emotions)
831
+
832
+ # Process message with FlowManager
833
+ flow_context = self.flow_manager.process_message(user_id, message, emotions)
834
+
835
+ # Format conversation history
836
+ conversation_history = []
837
+ for msg in conversation.messages:
838
+ conversation_history.append({
839
+ "text": msg.text,
840
+ "timestamp": msg.timestamp,
841
+ "role": msg.role
842
+ })
843
+
844
+ # Generate response
845
+ response_text = self.generate_response(message, emotions, conversation_history)
846
+
847
+ # Generate a follow-up question if the response is too short
848
+ if len(response_text.split()) < 20 and not response_text.endswith('?'):
849
+ follow_up_prompt = f"""Based on the conversation so far:
850
+ {chr(10).join([f"{msg['role']}: {msg['text']}" for msg in conversation_history[-3:]])}
851
+
852
+ Generate a thoughtful follow-up question that:
853
+ 1. Shows you're actively listening
854
+ 2. Encourages deeper exploration
855
+ 3. Maintains therapeutic rapport
856
+ 4. Is open-ended and non-judgmental
857
+
858
+ Respond with just the question."""
859
+
860
+ follow_up = self.llm.invoke(follow_up_prompt)
861
+ response_text += f"\n\n{follow_up}"
862
+
863
+ # assistant response -> conversation history
864
+ assistant_message = Message(
865
+ text=response_text,
866
+ timestamp=datetime.now().isoformat(),
867
+ role="assistant"
868
+ )
869
+ conversation.messages.append(assistant_message)
870
+
871
+ # Update context
872
+ conversation.context.update({
873
+ "last_emotion": emotions,
874
+ "last_interaction": datetime.now().isoformat(),
875
+ "flow_context": flow_context
876
+ })
877
+
878
+ # Store this interaction in vector database
879
+ current_interaction = f"User: {message}\nChatbot: {response_text}"
880
+ self.vector_db.add_texts([current_interaction])
881
+ self.vector_db.save_local(VECTOR_DB_PATH)
882
+
883
+ return response_text
884
+
885
+ def get_session_summary(self, session_id: str) -> Optional[Dict[str, Any]]:
886
+
887
+ return self.session_summaries.get(session_id)
888
+
889
+ def get_user_replies(self, user_id: str) -> List[Dict[str, Any]]:
890
+ if user_id not in self.conversations:
891
+ return []
892
+
893
+ conversation = self.conversations[user_id]
894
+ user_replies = []
895
+
896
+ for message in conversation.messages:
897
+ if message.role == "user":
898
+ user_replies.append({
899
+ "text": message.text,
900
+ "timestamp": message.timestamp,
901
+ "session_id": conversation.session_id
902
+ })
903
+
904
+ return user_replies
905
+
906
+ if __name__ == "__main__":
907
+ pass
conversation_flow.py ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import json
3
+ import json5
4
+ import time
5
+ from datetime import datetime
6
+ from typing import List, Dict, Any, Optional
7
+ from pydantic import BaseModel, Field
8
+
9
+ # Configure logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class PhaseTransitionResponse(BaseModel):
13
+ goals_progress: Dict[str, float]
14
+ should_transition: bool
15
+ next_phase: str
16
+ reasoning: str
17
+
18
+ class SessionCharacteristics(BaseModel):
19
+ alliance_strength: float = Field(ge=0.0, le=1.0)
20
+ engagement_level: float = Field(ge=0.0, le=1.0)
21
+ emotional_pattern: str
22
+ cognitive_pattern: str
23
+ coping_mechanisms: List[str] = Field(min_items=2)
24
+ progress_quality: float = Field(ge=0.0, le=1.0)
25
+ recommended_focus: str
26
+
27
+ class ConversationPhase(BaseModel):
28
+ name: str
29
+ description: str
30
+ goals: List[str]
31
+ typical_duration: int # in minutes
32
+ started_at: Optional[str] = None # ISO timestamp
33
+ ended_at: Optional[str] = None # ISO timestamp
34
+ completion_metrics: Dict[str, float] = Field(default_factory=dict) # e.g., {'goal_progress': 0.8}
35
+
36
+ class FlowManager:
37
+
38
+ # Define conversation phases
39
+ PHASES = {
40
+ 'introduction': {
41
+ 'description': 'Establishing rapport and identifying main concerns',
42
+ 'goals': [
43
+ 'build therapeutic alliance',
44
+ 'identify primary concerns',
45
+ 'understand client expectations',
46
+ 'establish session structure'
47
+ ],
48
+ 'typical_duration': 5 # In mins
49
+ },
50
+ 'exploration': {
51
+ 'description': 'In-depth exploration of issues and their context',
52
+ 'goals': [
53
+ 'examine emotional responses',
54
+ 'explore thought patterns',
55
+ 'identify behavioral patterns',
56
+ 'understand situational context',
57
+ 'recognize relationship dynamics'
58
+ ],
59
+ 'typical_duration': 15 # In mins
60
+ },
61
+ 'intervention': {
62
+ 'description': 'Providing strategies, insights, and therapeutic interventions',
63
+ 'goals': [
64
+ 'introduce coping techniques',
65
+ 'reframe negative thinking',
66
+ 'provide emotional validation',
67
+ 'offer perspective shifts',
68
+ 'suggest behavioral modifications'
69
+ ],
70
+ 'typical_duration': 20 # In minutes
71
+ },
72
+ 'conclusion': {
73
+ 'description': 'Summarizing insights and establishing next steps',
74
+ 'goals': [
75
+ 'review key insights',
76
+ 'consolidate learning',
77
+ 'identify action items',
78
+ 'set intentions',
79
+ 'provide closure'
80
+ ],
81
+ 'typical_duration': 5 # In minutes
82
+ }
83
+ }
84
+
85
+ def __init__(self, llm, session_duration: int = 45):
86
+
87
+ self.llm = llm
88
+ self.session_duration = session_duration * 60 # Convert to seconds
89
+
90
+ # User session data structures
91
+ self.user_sessions = {} # user_id -> session data
92
+
93
+ logger.info(f"Initialized FlowManager with {session_duration} minute sessions")
94
+
95
+ def _ensure_user_session(self, user_id: str):
96
+
97
+ if user_id not in self.user_sessions:
98
+ self.initialize_session(user_id)
99
+
100
+ def initialize_session(self, user_id: str):
101
+
102
+ now = datetime.now().isoformat()
103
+
104
+ # Create initial phase
105
+ initial_phase = ConversationPhase(
106
+ name='introduction',
107
+ description=self.PHASES['introduction']['description'],
108
+ goals=self.PHASES['introduction']['goals'],
109
+ typical_duration=self.PHASES['introduction']['typical_duration'],
110
+ started_at=now
111
+ )
112
+
113
+ # Generate session ID
114
+ session_id = f"{user_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
115
+
116
+ # Initialize session data
117
+ self.user_sessions[user_id] = {
118
+ 'session_id': session_id,
119
+ 'user_id': user_id,
120
+ 'started_at': now,
121
+ 'updated_at': now,
122
+ 'current_phase': initial_phase,
123
+ 'phase_history': [initial_phase],
124
+ 'message_count': 0,
125
+ 'emotion_history': [],
126
+ 'emotion_progression': [],
127
+ 'flags': {
128
+ 'crisis_detected': False,
129
+ 'long_silences': False
130
+ },
131
+ 'llm_context': {
132
+ 'session_characteristics': {}
133
+ }
134
+ }
135
+
136
+ logger.info(f"Initialized new session for user {user_id}")
137
+ return self.user_sessions[user_id]
138
+
139
+ def process_message(self, user_id: str, message: str, emotions: Dict[str, float]) -> Dict[str, Any]:
140
+
141
+ self._ensure_user_session(user_id)
142
+ session = self.user_sessions[user_id]
143
+
144
+ # Update session
145
+ now = datetime.now().isoformat()
146
+ session['updated_at'] = now
147
+ session['message_count'] += 1
148
+
149
+ # Track emotions
150
+ emotion_entry = {
151
+ 'timestamp': now,
152
+ 'emotions': emotions,
153
+ 'message_idx': session['message_count']
154
+ }
155
+ session['emotion_history'].append(emotion_entry)
156
+
157
+ # Update emotion progression
158
+ if not session.get('emotion_progression'):
159
+ session['emotion_progression'] = []
160
+
161
+ # Get primary emotion (highest confidence)
162
+ primary_emotion = max(emotions.items(), key=lambda x: x[1])[0]
163
+ session['emotion_progression'].append(primary_emotion)
164
+
165
+ # Check for phase transition
166
+ self._check_phase_transition(user_id, message, emotions)
167
+
168
+ # Update session characteristics via LLM analysis (periodically)
169
+ if session['message_count'] % 5 == 0:
170
+ self._update_session_characteristics(user_id)
171
+
172
+ # Create flow context for response generation
173
+ flow_context = self._create_flow_context(user_id)
174
+
175
+ return flow_context
176
+
177
+ def _check_phase_transition(self, user_id: str, message: str, emotions: Dict[str, float]):
178
+
179
+ session = self.user_sessions[user_id]
180
+ current_phase = session['current_phase']
181
+
182
+ # Calculate session progress
183
+ started_at = datetime.fromisoformat(session['started_at'])
184
+ now = datetime.now()
185
+ elapsed_seconds = (now - started_at).total_seconds()
186
+ session_progress = elapsed_seconds / self.session_duration
187
+
188
+ # Create prompt for LLM to evaluate phase transition
189
+ phase_context = {
190
+ 'current': current_phase.name,
191
+ 'description': current_phase.description,
192
+ 'goals': current_phase.goals,
193
+ 'time_in_phase': (now - datetime.fromisoformat(current_phase.started_at)).total_seconds() / 60,
194
+ 'session_progress': session_progress,
195
+ 'message_count': session['message_count']
196
+ }
197
+
198
+ # Only check for transition if we've spent some time in current phase
199
+ min_time_in_phase_minutes = max(2, current_phase.typical_duration * 0.5)
200
+ if phase_context['time_in_phase'] < min_time_in_phase_minutes:
201
+ return
202
+
203
+ prompt = f"""
204
+ Evaluate whether this therapeutic conversation should transition to the next phase.
205
+
206
+ Current conversation state:
207
+ - Current phase: {current_phase.name} ("{current_phase.description}")
208
+ - Goals for this phase: {', '.join(current_phase.goals)}
209
+ - Time spent in this phase: {phase_context['time_in_phase']:.1f} minutes
210
+ - Session progress: {session_progress * 100:.1f}% complete
211
+ - Message count: {session['message_count']}
212
+
213
+ Latest message from user: "{message}"
214
+
215
+ Current emotions: {', '.join([f"{e} ({score:.2f})" for e, score in
216
+ sorted(emotions.items(), key=lambda x: x[1], reverse=True)[:3]])}
217
+
218
+ Phases in a therapeutic conversation:
219
+ 1. introduction: {self.PHASES['introduction']['description']}
220
+ 2. exploration: {self.PHASES['exploration']['description']}
221
+ 3. intervention: {self.PHASES['intervention']['description']}
222
+ 4. conclusion: {self.PHASES['conclusion']['description']}
223
+
224
+ Consider:
225
+ 1. Have the goals of the current phase been sufficiently addressed?
226
+ 2. Is the timing appropriate considering overall session progress?
227
+ 3. Is there a natural transition point in the conversation?
228
+ 4. Does the emotional content suggest readiness to move forward?
229
+
230
+ First, provide your analysis of whether the key goals of the current phase have been met.
231
+ Then decide if the conversation should transition to the next phase.
232
+
233
+ Respond with a JSON object in this format:
234
+ {{
235
+ "goals_progress": {{
236
+ "goal1": 0.5,
237
+ "goal2": 0.7
238
+ }},
239
+ "should_transition": false,
240
+ "next_phase": "exploration",
241
+ "reasoning": "brief explanation"
242
+ }}
243
+
244
+ Output ONLY valid JSON without additional text.
245
+ """
246
+
247
+ response = self.llm.invoke(prompt)
248
+
249
+ try:
250
+ # Parse with json5 for more tolerant parsing
251
+ evaluation = json5.loads(response)
252
+ # Validate with Pydantic
253
+ phase_transition = PhaseTransitionResponse.parse_obj(evaluation)
254
+
255
+ # Update goal progress metrics
256
+ for goal, score in phase_transition.goals_progress.items():
257
+ if goal in current_phase.goals:
258
+ current_phase.completion_metrics[goal] = score
259
+
260
+ # Check if we should transition
261
+ if phase_transition.should_transition:
262
+ if phase_transition.next_phase in self.PHASES:
263
+ self._transition_to_phase(user_id, phase_transition.next_phase, phase_transition.reasoning)
264
+ except (json5.Json5DecodeError, ValueError):
265
+ self._check_time_based_transition(user_id)
266
+
267
+ def _check_time_based_transition(self, user_id: str):
268
+
269
+ session = self.user_sessions[user_id]
270
+ current_phase = session['current_phase']
271
+
272
+ # Get elapsed time
273
+ started_at = datetime.fromisoformat(session['started_at'])
274
+ now = datetime.now()
275
+ elapsed_minutes = (now - started_at).total_seconds() / 60
276
+
277
+ # Calculate phase thresholds
278
+ intro_threshold = self.PHASES['introduction']['typical_duration']
279
+ explore_threshold = intro_threshold + self.PHASES['exploration']['typical_duration']
280
+ intervention_threshold = explore_threshold + self.PHASES['intervention']['typical_duration']
281
+
282
+ # Transition based on time
283
+ next_phase = None
284
+ if current_phase.name == 'introduction' and elapsed_minutes >= intro_threshold:
285
+ next_phase = 'exploration'
286
+ elif current_phase.name == 'exploration' and elapsed_minutes >= explore_threshold:
287
+ next_phase = 'intervention'
288
+ elif current_phase.name == 'intervention' and elapsed_minutes >= intervention_threshold:
289
+ next_phase = 'conclusion'
290
+
291
+ if next_phase:
292
+ self._transition_to_phase(user_id, next_phase, "Time-based transition")
293
+
294
+ def _transition_to_phase(self, user_id: str, next_phase_name: str, reason: str):
295
+
296
+ session = self.user_sessions[user_id]
297
+ current_phase = session['current_phase']
298
+
299
+ # End current phase
300
+ now = datetime.now().isoformat()
301
+ current_phase.ended_at = now
302
+
303
+ # Create new phase
304
+ new_phase = ConversationPhase(
305
+ name=next_phase_name,
306
+ description=self.PHASES[next_phase_name]['description'],
307
+ goals=self.PHASES[next_phase_name]['goals'],
308
+ typical_duration=self.PHASES[next_phase_name]['typical_duration'],
309
+ started_at=now
310
+ )
311
+
312
+ # Update session
313
+ session['current_phase'] = new_phase
314
+ session['phase_history'].append(new_phase)
315
+
316
+ logger.info(f"User {user_id} transitioned from {current_phase.name} to {next_phase_name}: {reason}")
317
+
318
+ def _update_session_characteristics(self, user_id: str):
319
+ session = self.user_sessions[user_id]
320
+
321
+ # Only do this periodically to save LLM calls
322
+ if session['message_count'] < 5:
323
+ return
324
+
325
+ # Create a summary of the conversation so far
326
+ message_sample = []
327
+ emotion_summary = {}
328
+
329
+ # Get recent messages
330
+ for i, emotion_data in enumerate(session['emotion_history'][-10:]):
331
+ msg_idx = emotion_data['message_idx']
332
+ if i % 2 == 0: # Just include a subset of messages
333
+ message_sample.append(f"Message {msg_idx}: User emotions: {', '.join([f'{e}({s:.2f})' for e, s in sorted(emotion_data['emotions'].items(), key=lambda x: x[1], reverse=True)[:2]])}")
334
+
335
+ # Aggregate emotions
336
+ for emotion, score in emotion_data['emotions'].items():
337
+ if score > 0.3:
338
+ emotion_summary[emotion] = emotion_summary.get(emotion, 0) + score
339
+
340
+ # Normalize emotion summary
341
+ if emotion_summary:
342
+ total = sum(emotion_summary.values())
343
+ emotion_summary = {e: s/total for e, s in emotion_summary.items()}
344
+
345
+ # prompt for LLM
346
+ prompt = f"""
347
+ Analyze this therapy session and provide a JSON response with the following characteristics:
348
+
349
+ Current session state:
350
+ - Phase: {session['current_phase'].name} ({session['current_phase'].description})
351
+ - Message count: {session['message_count']}
352
+ - Emotion summary: {', '.join([f'{e}({s:.2f})' for e, s in sorted(emotion_summary.items(), key=lambda x: x[1], reverse=True)])}
353
+
354
+ Recent messages:
355
+ {chr(10).join(message_sample)}
356
+
357
+ Required JSON format:
358
+ {{
359
+ "alliance_strength": 0.8,
360
+ "engagement_level": 0.7,
361
+ "emotional_pattern": "brief description of emotional pattern",
362
+ "cognitive_pattern": "brief description of cognitive pattern",
363
+ "coping_mechanisms": ["mechanism1", "mechanism2"],
364
+ "progress_quality": 0.6,
365
+ "recommended_focus": "brief therapeutic recommendation"
366
+ }}
367
+
368
+ Important:
369
+ 1. Respond with ONLY the JSON object
370
+ 2. Use numbers between 0.0 and 1.0 for alliance_strength, engagement_level, and progress_quality
371
+ 3. Keep descriptions brief and focused
372
+ 4. Include at least 2 coping mechanisms
373
+ 5. Provide a specific recommended focus
374
+
375
+ JSON Response:
376
+ """
377
+
378
+ response = self.llm.invoke(prompt)
379
+
380
+ try:
381
+ # Parse with json5 for more tolerant parsing
382
+ characteristics = json5.loads(response)
383
+ # Validate with Pydantic
384
+ session_chars = SessionCharacteristics.parse_obj(characteristics)
385
+ session['llm_context']['session_characteristics'] = session_chars.dict()
386
+ logger.info(f"Updated session characteristics for user {user_id}")
387
+ except (json5.Json5DecodeError, ValueError) as e:
388
+ logger.warning(f"Failed to parse session characteristics: {e}")
389
+
390
+ def _create_flow_context(self, user_id: str) -> Dict[str, Any]:
391
+
392
+ session = self.user_sessions[user_id]
393
+ current_phase = session['current_phase']
394
+
395
+ # Calculate session times
396
+ started_at = datetime.fromisoformat(session['started_at'])
397
+ now = datetime.now()
398
+ elapsed_seconds = (now - started_at).total_seconds()
399
+ remaining_seconds = max(0, self.session_duration - elapsed_seconds)
400
+
401
+ # Get primary emotions
402
+ emotions_summary = {}
403
+ for emotion_data in session['emotion_history'][-3:]: # Last 3 messages
404
+ for emotion, score in emotion_data['emotions'].items():
405
+ emotions_summary[emotion] = emotions_summary.get(emotion, 0) + score
406
+
407
+ if emotions_summary:
408
+ primary_emotions = sorted(emotions_summary.items(), key=lambda x: x[1], reverse=True)[:3]
409
+ else:
410
+ primary_emotions = []
411
+
412
+ # Create guidance based on phase
413
+ phase_guidance = []
414
+
415
+ # Add phase-specific guidance
416
+ if current_phase.name == 'introduction':
417
+ phase_guidance.append("Build rapport and identify main concerns")
418
+ if session['message_count'] > 3:
419
+ phase_guidance.append("Begin exploring emotional context")
420
+
421
+ elif current_phase.name == 'exploration':
422
+ phase_guidance.append("Deepen understanding of issues and contexts")
423
+ phase_guidance.append("Connect emotional patterns to identify themes")
424
+
425
+ elif current_phase.name == 'intervention':
426
+ phase_guidance.append("Offer support strategies and therapeutic insights")
427
+ if remaining_seconds < 600: # Less than 10 minutes left
428
+ phase_guidance.append("Begin consolidating key insights")
429
+
430
+ elif current_phase.name == 'conclusion':
431
+ phase_guidance.append("Summarize insights and establish next steps")
432
+ phase_guidance.append("Provide closure while maintaining supportive presence")
433
+
434
+ # Add guidance based on session characteristics
435
+ if 'session_characteristics' in session['llm_context']:
436
+ char = session['llm_context']['session_characteristics']
437
+
438
+ # Low alliance strength
439
+ if char.get('alliance_strength', 0.8) < 0.6:
440
+ phase_guidance.append("Focus on strengthening therapeutic alliance")
441
+
442
+ # Low engagement
443
+ if char.get('engagement_level', 0.8) < 0.6:
444
+ phase_guidance.append("Increase engagement with more personalized responses")
445
+
446
+ # Add recommended focus if available
447
+ if 'recommended_focus' in char:
448
+ phase_guidance.append(char['recommended_focus'])
449
+
450
+ # Create flow context
451
+ flow_context = {
452
+ 'phase': {
453
+ 'name': current_phase.name,
454
+ 'description': current_phase.description,
455
+ 'goals': current_phase.goals
456
+ },
457
+ 'session': {
458
+ 'elapsed_minutes': elapsed_seconds / 60,
459
+ 'remaining_minutes': remaining_seconds / 60,
460
+ 'progress_percentage': (elapsed_seconds / self.session_duration) * 100,
461
+ 'message_count': session['message_count']
462
+ },
463
+ 'emotions': [{'name': e, 'intensity': s} for e, s in primary_emotions],
464
+ 'guidance': phase_guidance
465
+ }
466
+
467
+ return flow_context
guidelines.txt ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Therapeutic Guidelines:
2
+ 1. Build Trust and Rapport
3
+
4
+ Begin with warmth and understanding.
5
+
6
+ Use active listening: reflect back emotions and key points.
7
+
8
+ Be supportive and non-threatening in tone.
9
+
10
+ Always keep the tone calm, supportive, and emotionally intelligent.
11
+
12
+ Empower users to explore their own thoughts and solutions.
13
+
14
+ Ask open-ended questions to deepen self-reflection.
15
+
16
+ Avoid giving commands or rigid advice.
17
+
18
+ Avoid assumptions based on culture, gender, or personal history.
19
+
20
+ Create psychological safety — reassure the user that their thoughts and emotions are welcome and valid.
21
+
22
+
23
+ 2. Be Non-Judgmental
24
+
25
+ Accept all emotions and experiences without criticism.
26
+
27
+ Never blame or shame the user.
28
+
29
+ Normalize their feelings when appropriate
30
+
31
+ 3. Use Evidence-Based Techniques
32
+
33
+ Apply suitable techniques such as:
34
+ 1. Cognitive Behavioral Therapy (CBT)
35
+ Help users identify negative thought patterns (cognitive distortions) and reframe them:
36
+
37
+ “Let’s try to challenge that thought — is there evidence that supports or contradicts it?”
38
+
39
+ “What might be a more balanced way to look at this?”
40
+
41
+ 2. Dialectical Behavior Therapy (DBT)
42
+ Focus on emotional regulation, distress tolerance, and mindfulness:
43
+
44
+ “Let’s take a moment to breathe and notice what you’re feeling without judgment.”
45
+
46
+ “What can you do right now to self-soothe or ground yourself?”
47
+
48
+ 3. Acceptance and Commitment Therapy (ACT)
49
+ Promote acceptance of thoughts and values-based living:
50
+
51
+ “Instead of fighting that thought, can we observe it and let it be?”
52
+
53
+ “What matters to you right now? What small step can you take in that direction?”
54
+
55
+ 4. Motivational Interviewing
56
+ Help ambivalent users explore change:
57
+
58
+ “On a scale from 1 to 10, how ready do you feel to make a change?”
59
+
60
+ “What would it take to move one step closer?”
61
+
62
+
63
+ 4. Structured Conversation Flow
64
+ Begin with empathy → explore the problem → validate emotions → apply a therapeutic tool → summarize insight or coping step.
65
+
66
+ End each message with a question or reflection prompt to continue engagement.
67
+
68
+
69
+ 5. Add Actionable Suggestions
70
+
71
+ Offer gentle, realistic, and practical steps the user can try.
72
+
73
+ Tailor suggestions to their emotional state — prioritize simplicity and emotional safety.
74
+
75
+ Use empowering language that invites, not instructs:
76
+
77
+ “Would you be open to trying…?”
78
+
79
+ “Some people find this helpful — would you like to explore it together?”
80
+
81
+ Examples of actionable suggestions include:
82
+
83
+ Grounding Techniques
84
+ “Can you name five things you see around you right now, four things you can touch, three you can hear, two you can smell, and one you can taste?”
85
+
86
+ Mindful Breathing
87
+ “Let’s try a simple breathing exercise: inhale slowly for 4 counts, hold for 4, exhale for 4. Can we do this together for a few rounds?”
88
+
89
+ Journaling Prompts
90
+ “Would writing down your thoughts help make sense of what you're feeling? You might start with: ‘Right now, I’m feeling… because…’”
91
+
92
+ Self-Compassion Reminders
93
+ “Can you speak to yourself the way you would to a friend going through this?”
94
+
95
+ Behavioral Activation
96
+ “Sometimes doing one small activity, even if it feels meaningless at first, can help shift your energy. What’s one thing you could do today that used to bring you comfort?”
97
+
98
+ Connection Check-In
99
+ “Is there someone you trust that you might feel comfortable talking to or spending time with today, even briefly?”
100
+
101
+ End with an open tone:
102
+
103
+ “How does that sound to you?”
104
+
105
+ “Would you like to try that and let me know how it goes?”
106
+
107
+
hf_spaces.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Spaces GPU configuration
3
+ """
4
+ import os
5
+ import torch
6
+
7
+ # Set environment variables for Hugging Face Spaces
8
+ os.environ.update({
9
+ 'TRANSFORMERS_CACHE': '/tmp/huggingface',
10
+ 'HF_HOME': '/tmp/huggingface',
11
+ 'TOKENIZERS_PARALLELISM': 'false',
12
+ 'TRANSFORMERS_VERBOSITY': 'error',
13
+ 'BITSANDBYTES_NOWELCOME': '1',
14
+ 'PYTORCH_CUDA_ALLOC_CONF': 'max_split_size_mb:128' # Memory efficient attention
15
+ })
16
+
17
+ # Create necessary directories
18
+ for directory in ['/tmp/huggingface', '/tmp/vector_db', '/tmp/session_data', '/tmp/session_summaries']:
19
+ os.makedirs(directory, exist_ok=True)
20
+
21
+ # Hugging Face Spaces specific settings
22
+ SPACES_CONFIG = {
23
+ 'port': 7860, # Default port for Hugging Face Spaces
24
+ 'host': '0.0.0.0',
25
+ 'workers': 1, # Single worker for Hugging Face Spaces
26
+ 'timeout': 180, # Increased timeout for model loading
27
+ 'log_level': 'info'
28
+ }
29
+
30
+ # Model settings optimized for T4 GPU
31
+ MODEL_CONFIG = {
32
+ 'model_name': 'meta-llama/Llama-3.2-3B-Instruct',
33
+ 'peft_model_path': 'nada013/mental-health-chatbot',
34
+ 'use_4bit': True,
35
+ 'device': 'cuda' if torch.cuda.is_available() else 'cpu', # Use GPU if available
36
+ 'batch_size': 4, # Optimized for T4 GPU
37
+ 'max_memory': {0: "14GB"} if torch.cuda.is_available() else None # T4 GPU memory limit
38
+ }
requirements.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ transformers>=4.49.0
2
+ torch>=2.2.0
3
+ sentence-transformers>=3.4.1
4
+ langchain>=0.3.21
5
+ langchain-community>=0.3.20
6
+ langchain-core>=0.3.47
7
+ langchain-huggingface>=0.1.2
8
+ pydantic>=2.10.6
9
+ pydantic-settings>=2.8.1
10
+ fastapi>=0.115.11
11
+ uvicorn>=0.34.0
12
+ python-dotenv>=1.0.1
13
+ pytest>=7.4.0
14
+ gunicorn>=21.2.0
15
+ accelerate>=1.5.2
16
+ bitsandbytes>=0.45.3
17
+ chromadb>=0.6.3
18
+ datasets>=3.4.1
19
+ faiss-cpu>=1.10.0
20
+ huggingface-hub>=0.29.3
21
+ peft>=0.15.1
22
+ safetensors>=0.5.3
23
+ tokenizers>=0.21.1
24
+ tiktoken>=0.9.0
25
+ starlette>=0.46.1
26
+ websockets>=15.0.1
27
+ python-multipart>=0.0.6
28
+ json5>=0.9.14
start.sh ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ echo "Starting Uvicorn..."
3
+ echo "Current directory: $(pwd)"
4
+ echo "Listing files:"
5
+ ls -la
6
+ echo "Starting app..."
7
+ exec uvicorn app:app --host 0.0.0.0 --port 7860