apKargbo commited on
Commit
91a64d2
·
verified ·
1 Parent(s): 7f1aa58

Initial backend files

Browse files
Files changed (5) hide show
  1. Dockerfile +45 -0
  2. database.py +98 -0
  3. main.py +184 -0
  4. nlp_model.py +74 -0
  5. requirements.txt +8 -0
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile
2
+
3
+ # --- STAGE 1: Build & Install Dependencies ---
4
+ # Use a Python base image suitable for slim deployments
5
+ FROM python:3.11-slim AS builder
6
+
7
+ # Set working directory inside the container
8
+ WORKDIR /app
9
+
10
+ # Install system dependencies needed for some Python packages (like psycopg2 for Postgres)
11
+ # We include curl and build-essential just in case, which is good practice.
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ build-essential \
14
+ libpq-dev \
15
+ curl \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # Copy the requirements file
19
+ COPY requirements.txt .
20
+
21
+ # Install Python dependencies (transformers, torch, fastapi, pandas, etc.)
22
+ # --no-cache-dir reduces image size
23
+ RUN pip install --no-cache-dir -r requirements.txt
24
+
25
+ # --- STAGE 2: Final Runtime Image ---
26
+ # Use the same slim base image for the final production environment
27
+ FROM python:3.11-slim AS runtime
28
+
29
+ # Set working directory
30
+ WORKDIR /app
31
+
32
+ # Copy only the installed Python packages from the builder stage
33
+ # This saves space by not including build dependencies like build-essential
34
+ COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
35
+
36
+ # Copy application code (main.py, nlp_model.py, database.py)
37
+ COPY ./backend /app/
38
+
39
+ # IMPORTANT: Set the internal port. Hugging Face Spaces defaults to 7860.
40
+ EXPOSE 7860
41
+
42
+ # Command to run the application using Uvicorn
43
+ # 0.0.0.0 is needed to listen on all interfaces.
44
+ # --port 7860 must match the EXPOSE and the space.yaml configuration.
45
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
database.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # database.py (MongoDB Version)
2
+ import pymongo
3
+ from pymongo.errors import ConnectionFailure
4
+ import datetime
5
+ import os
6
+ # from dotenv import load_dotenv
7
+
8
+ # Optional: Load environment variables from a .env file for security
9
+ # load_dotenv()
10
+
11
+ # --- Configuration ---
12
+ # Get connection string from environment variable or replace directly
13
+ # Example local URI: "mongodb://localhost:27017/"
14
+ # Example Atlas URI: "mongodb+srv://<username>:<password>@clustername.mongodb.net/..."
15
+ MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017/")
16
+ DB_NAME = 'amhci_data_db'
17
+ COLLECTION_NAME = 'checkin_entries'
18
+ # ---------------------
19
+
20
+ # Global variables for the client and collection objects
21
+ mongo_client = None
22
+ mongo_collection = None
23
+
24
+ def get_mongo_collection():
25
+ """
26
+ Establishes the MongoDB connection and returns the collection object.
27
+ This function should be called before performing any database operations.
28
+ """
29
+ global mongo_client, mongo_collection
30
+
31
+ if mongo_collection is not None:
32
+ return mongo_collection
33
+
34
+ try:
35
+ # Create a connection using MongoClient
36
+ mongo_client = pymongo.MongoClient(MONGO_URI)
37
+
38
+ # Access the specified database and collection
39
+ db = mongo_client[DB_NAME]
40
+ mongo_collection = db[COLLECTION_NAME]
41
+
42
+ print(f"Connected to MongoDB: Database '{DB_NAME}', Collection '{COLLECTION_NAME}'")
43
+ return mongo_collection
44
+
45
+ except ConnectionFailure as e:
46
+ print(f"ERROR: Could not connect to MongoDB: {e}")
47
+ # Exit the program or handle the error gracefully
48
+ raise
49
+
50
+ def close_mongo_connection():
51
+ """Closes the MongoDB connection."""
52
+ global mongo_client
53
+ if mongo_client:
54
+ mongo_client.close()
55
+ print("MongoDB connection closed.")
56
+
57
+
58
+ def insert_checkin_entry(user_text, sentiment_score, keyword_intensity, anomaly_flag=False):
59
+ """
60
+ Inserts a new check-in document into the MongoDB collection.
61
+ """
62
+ collection = get_mongo_collection()
63
+
64
+ # MongoDB stores data as documents (Python dictionaries)
65
+ entry_data = {
66
+ "timestamp": datetime.datetime.now(), # MongoDB handles datetime objects natively
67
+ "user_text": user_text,
68
+ "sentiment_score": sentiment_score,
69
+ "keyword_intensity": keyword_intensity,
70
+ "anomaly_flag": anomaly_flag
71
+ }
72
+
73
+ # Insert the document
74
+ result = collection.insert_one(entry_data)
75
+ print(f"Inserted document ID: {result.inserted_id}")
76
+ return result.inserted_id
77
+
78
+ # --- Example Usage ---
79
+ if __name__ == '__main__':
80
+ try:
81
+ # Example of how to use the insert function
82
+ insert_checkin_entry(
83
+ user_text="I feel great today, everything is going well.",
84
+ sentiment_score=0.9,
85
+ keyword_intensity=0.1
86
+ )
87
+
88
+ # Example of reading back data
89
+ print("\nRetrieving all entries:")
90
+ for doc in get_mongo_collection().find():
91
+ print(doc)
92
+
93
+ except Exception as e:
94
+ print(f"An error occurred during database operations: {e}")
95
+ finally:
96
+ # Ensure the connection is closed when the script finishes
97
+ close_mongo_connection()
98
+
main.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+ import datetime
3
+ from typing import List, Optional
4
+
5
+ # Third-party libraries
6
+ from fastapi import FastAPI, HTTPException
7
+ from pydantic import BaseModel
8
+ import pandas as pd
9
+
10
+ # Local modules
11
+ from database import get_mongo_collection, insert_checkin_entry, close_mongo_connection
12
+ from nlp_model import analyze_text
13
+ from bson import ObjectId
14
+
15
+ # Initialize the FastAPI application
16
+ app = FastAPI()
17
+
18
+ # --- Pydantic Models for Data Validation ---
19
+
20
+ class CheckinRequest(BaseModel):
21
+ """Model for incoming user check-in data."""
22
+ user_text: str
23
+
24
+ class CheckinResponse(BaseModel):
25
+ """Model for data returned after a single check-in."""
26
+ id: str
27
+ timestamp: datetime.datetime
28
+ sentiment_score: float
29
+ anomaly_flag: bool
30
+ support_message: Optional[str] = None # The supportive message/nudge
31
+ user_text: Optional[str] = None
32
+
33
+ # --- Helper Functions ---
34
+
35
+ def check_for_anomaly(all_entries: List[dict], new_score: float) -> bool:
36
+ """
37
+ Checks if the new score represents a significant, negative shift
38
+ using the Interquartile Range (IQR) rule against historical data.
39
+ """
40
+
41
+ # Needs at least 4 past data points to establish a stable baseline (e.g., 3 days + new day)
42
+ if len(all_entries) < 4:
43
+ return False
44
+
45
+ # Gather only historical scores (sentiment_score is a float from 0.0 to 1.0)
46
+ historical_scores = [entry['sentiment_score'] for entry in all_entries]
47
+
48
+ df = pd.DataFrame(historical_scores, columns=['score'])
49
+
50
+ # Calculate key statistics (Median and IQR are robust against outliers)
51
+ Q1 = df['score'].quantile(0.25)
52
+ Q3 = df['score'].quantile(0.75)
53
+ IQR = Q3 - Q1
54
+
55
+ # Anomaly Rule: 1.5 * IQR below the first quartile (Q1)
56
+ LOWER_BOUND = Q1 - (1.5 * IQR)
57
+
58
+ # Anomaly is flagged if the new score is significantly below the typical low point.
59
+ if new_score < LOWER_BOUND:
60
+ print(f"ANOMALY DETECTED! New Score ({new_score:.2f}) is below Lower Bound ({LOWER_BOUND:.2f})")
61
+ return True
62
+
63
+ return False
64
+
65
+
66
+ def generate_support_message(sentiment_score: float, is_anomaly: bool) -> str:
67
+ """
68
+ Generates a supportive message (nudge) based on the analysis.
69
+ """
70
+
71
+ # 1. Anomaly/Crisis Nudge (Highest Priority)
72
+ if is_anomaly:
73
+ return (
74
+ "⚠️ Significant Change Detected. Your recent entries show a notable dip below your typical baseline. "
75
+ "Please reach out to a support professional or review your coping strategies. "
76
+ "Remember: small steps are still progress."
77
+ )
78
+
79
+ # 2. Low Sentiment Nudge
80
+ if sentiment_score < 0.3:
81
+ return (
82
+ "🫂 It sounds like you are going through a difficult time. "
83
+ "It's okay to feel overwhelmed. Focus on one small, manageable task today."
84
+ )
85
+
86
+ # 3. Mid-Range/Neutral Nudge
87
+ elif sentiment_score < 0.6:
88
+ return (
89
+ "⚖️ A steady day is still a good day. If you feel stuck, try a short break or a mindfulness exercise. "
90
+ "Keep an eye on how you feel tomorrow."
91
+ )
92
+
93
+ # 4. Positive Nudge (Reinforcement)
94
+ else:
95
+ return (
96
+ "✨ Great job! Your reflection shows a positive mindset. "
97
+ "Take a moment to recognize what made today successful and carry that momentum forward."
98
+ )
99
+
100
+
101
+ # --- API Endpoints ---
102
+
103
+ @app.post("/checkin", response_model=CheckinResponse)
104
+ def submit_checkin(request: CheckinRequest):
105
+ """
106
+ Receives a new check-in entry, runs AI analysis, checks for anomalies,
107
+ saves the data, and returns the result with a supportive message.
108
+ """
109
+
110
+ try:
111
+ # 1. Analyze the text using the sentiment model
112
+ analysis = analyze_text(request.user_text)
113
+
114
+ # 2. Retrieve all past entries for robust anomaly check
115
+ collection = get_mongo_collection()
116
+ # Fetch all entries, ordered by time (descending)
117
+ past_entries = list(collection.find().sort("timestamp", -1))
118
+
119
+ # 3. Check for anomaly
120
+ is_anomaly = check_for_anomaly(past_entries, analysis["sentiment"])
121
+
122
+ # 4. Generate the supportive message
123
+ support_message = generate_support_message(analysis["sentiment"], is_anomaly)
124
+
125
+ # 5. Save the new entry to the database
126
+ entry_id = insert_checkin_entry(
127
+ user_text=request.user_text,
128
+ sentiment_score=analysis["sentiment"],
129
+ keyword_intensity=analysis["intensity"],
130
+ anomaly_flag=is_anomaly
131
+ )
132
+
133
+ # 6. Return the saved entry ALONGSIDE the generated message
134
+ return CheckinResponse(
135
+ id=str(entry_id),
136
+ timestamp=datetime.datetime.now(),
137
+ sentiment_score=analysis["sentiment"],
138
+ anomaly_flag=is_anomaly,
139
+ support_message=support_message,
140
+ user_text=request.user_text
141
+ )
142
+
143
+ except Exception as e:
144
+ raise HTTPException(status_code=500, detail=f"Error processing check-in: {str(e)}")
145
+
146
+ # --- 2. GET Endpoint for Timeline Data ---
147
+ @app.get("/timeline", response_model=List[CheckinResponse])
148
+ def get_timeline():
149
+ try:
150
+ # Retrieve all entries, ordered by time
151
+ collection = get_mongo_collection()
152
+ entries = list(collection.find().sort("timestamp", 1))
153
+
154
+ # Convert MongoDB documents to response format
155
+ timeline_data = []
156
+ for entry in entries:
157
+ timeline_data.append(CheckinResponse(
158
+ id=str(entry["_id"]),
159
+ timestamp=entry["timestamp"],
160
+ sentiment_score=entry["sentiment_score"],
161
+ anomaly_flag=entry.get("anomaly_flag", False),
162
+ user_text=entry.get("user_text", "")
163
+ ))
164
+
165
+ return timeline_data
166
+
167
+ except Exception as e:
168
+ raise HTTPException(status_code=500, detail=f"Error retrieving timeline: {str(e)}")
169
+
170
+ # --- Health Check Endpoint ---
171
+ @app.get("/health")
172
+ def health_check():
173
+ return {"status": "healthy", "timestamp": datetime.datetime.now()}
174
+
175
+ # --- CORS Headers (Crucial for Hosting) ---
176
+ # Enable CORS for frontend development
177
+ from fastapi.middleware.cors import CORSMiddleware
178
+ app.add_middleware(
179
+ CORSMiddleware,
180
+ allow_origins=["http://localhost:5173", "http://localhost:3000"], # Vite dev server
181
+ allow_credentials=True,
182
+ allow_methods=["*"],
183
+ allow_headers=["*"],
184
+ )
nlp_model.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # nlp_model.py
2
+ import torch
3
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline
4
+
5
+ # --- Configuration ---
6
+ # You can replace this with any specific fine-tuned RoBERTa model ID
7
+ # from the Hugging Face Model Hub, especially one tuned for mood/stress.
8
+ # Example: 'finiteautomata/bertweet-base-sentiment-analysis' or a more general one.
9
+ MODEL_NAME = "cardiffnlp/twitter-roberta-base-sentiment-latest"
10
+
11
+ # --- Global Model Initialization ---
12
+ # This runs ONLY ONCE when the FastAPI server starts.
13
+ # We will use a general sentiment pipeline as a placeholder.
14
+ # In a real project, replace this with a fine-tuned mental health model.
15
+ try:
16
+ print(f"Loading RoBERTa model: {MODEL_NAME}...")
17
+
18
+ # Use the pipeline for quick, high-level analysis in a hackathon
19
+ sentiment_pipeline = pipeline(
20
+ "sentiment-analysis",
21
+ model=MODEL_NAME,
22
+ tokenizer=MODEL_NAME,
23
+ device=0 if torch.cuda.is_available() else -1 # Use GPU if available
24
+ )
25
+
26
+ # Store the labels (e.g., ['negative', 'neutral', 'positive']) for later use
27
+ global_labels = sentiment_pipeline.model.config.id2label
28
+ print(f"Model loaded with labels: {global_labels}")
29
+
30
+ except Exception as e:
31
+ print(f"Error loading RoBERTa model: {e}. Check network connection and model name.")
32
+ sentiment_pipeline = None
33
+ global_labels = {}
34
+
35
+ def map_label_to_score(label: str, score: float) -> float:
36
+ """Maps the model's categorical output (e.g., NEGATIVE) to a numerical 0.0 to 1.0 score."""
37
+ label = label.upper()
38
+
39
+ # This mapping is arbitrary but necessary for a quantitative timeline chart.
40
+ if "POSITIVE" in label:
41
+ return score # Closer to 1.0 is more positive
42
+ elif "NEGATIVE" in label:
43
+ return 1.0 - score # Closer to 0.0 is more negative
44
+ else: # NEUTRAL, etc.
45
+ # Neutral scores should hover near the midpoint (0.5)
46
+ return 0.5 + (score * 0.1) # Give it a slight boost based on confidence, but keep it centered
47
+
48
+ def analyze_text(text: str) -> dict:
49
+ """Performs RoBERTa analysis and returns scores."""
50
+ if not sentiment_pipeline:
51
+ return {"sentiment": 0.5, "intensity": 0.0}
52
+
53
+ # 1. Run the pipeline on the input text
54
+ result = sentiment_pipeline(text)[0]
55
+
56
+ label = result['label']
57
+ raw_score = result['score']
58
+
59
+ # 2. Map the result to our desired numerical output
60
+ numerical_sentiment = map_label_to_score(label, raw_score)
61
+
62
+ # 3. Use the raw confidence score as a proxy for intensity/certainty
63
+ intensity_score = raw_score
64
+
65
+ # Note: For hackathon extension: You could add a separate NER model
66
+ # here to extract keywords like 'hopeless' or 'anxiety' for the intensity score.
67
+
68
+ return {
69
+ "sentiment": numerical_sentiment, # A continuous score between 0.0 and 1.0
70
+ "intensity": intensity_score # The confidence level of the model's prediction
71
+ }
72
+
73
+ # Ensure the database.py and main.py files are correctly referencing this updated
74
+ # 'analyze_text' function and handling the float outputs. (They already do!)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.104.1
2
+ uvicorn>=0.24.0
3
+ pydantic>=2.5.0
4
+ pymongo>=4.6.0
5
+ torch>=2.9.0
6
+ transformers>=4.35.0
7
+ pandas>=2.1.3
8
+ python-dotenv>=1.0.0