# main.py import datetime from typing import List, Optional # Third-party libraries from fastapi import FastAPI, HTTPException from pydantic import BaseModel import pandas as pd # Local modules from database import get_mongo_collection, insert_checkin_entry, close_mongo_connection from nlp_model import analyze_text from bson import ObjectId # Initialize the FastAPI application app = FastAPI() # --- Pydantic Models for Data Validation --- class CheckinRequest(BaseModel): """Model for incoming user check-in data.""" user_text: str class CheckinResponse(BaseModel): """Model for data returned after a single check-in.""" id: str timestamp: datetime.datetime sentiment_score: float anomaly_flag: bool support_message: Optional[str] = None # The supportive message/nudge user_text: Optional[str] = None # --- Helper Functions --- def check_for_anomaly(all_entries: List[dict], new_score: float) -> bool: """ Checks if the new score represents a significant, negative shift using the Interquartile Range (IQR) rule against historical data. """ # Needs at least 4 past data points to establish a stable baseline (e.g., 3 days + new day) if len(all_entries) < 4: return False # Gather only historical scores (sentiment_score is a float from 0.0 to 1.0) historical_scores = [entry['sentiment_score'] for entry in all_entries] df = pd.DataFrame(historical_scores, columns=['score']) # Calculate key statistics (Median and IQR are robust against outliers) Q1 = df['score'].quantile(0.25) Q3 = df['score'].quantile(0.75) IQR = Q3 - Q1 # Anomaly Rule: 1.5 * IQR below the first quartile (Q1) LOWER_BOUND = Q1 - (1.5 * IQR) # Anomaly is flagged if the new score is significantly below the typical low point. if new_score < LOWER_BOUND: print(f"ANOMALY DETECTED! New Score ({new_score:.2f}) is below Lower Bound ({LOWER_BOUND:.2f})") return True return False def generate_support_message(sentiment_score: float, is_anomaly: bool) -> str: """ Generates a supportive message (nudge) based on the analysis. """ # 1. Anomaly/Crisis Nudge (Highest Priority) if is_anomaly: return ( "⚠️ Significant Change Detected. Your recent entries show a notable dip below your typical baseline. " "Please reach out to a support professional or review your coping strategies. " "Remember: small steps are still progress." ) # 2. Low Sentiment Nudge if sentiment_score < 0.3: return ( "🫂 It sounds like you are going through a difficult time. " "It's okay to feel overwhelmed. Focus on one small, manageable task today." ) # 3. Mid-Range/Neutral Nudge elif sentiment_score < 0.6: return ( "⚖️ A steady day is still a good day. If you feel stuck, try a short break or a mindfulness exercise. " "Keep an eye on how you feel tomorrow." ) # 4. Positive Nudge (Reinforcement) else: return ( "✨ Great job! Your reflection shows a positive mindset. " "Take a moment to recognize what made today successful and carry that momentum forward." ) # --- API Endpoints --- @app.post("/checkin", response_model=CheckinResponse) def submit_checkin(request: CheckinRequest): """ Receives a new check-in entry, runs AI analysis, checks for anomalies, saves the data, and returns the result with a supportive message. """ try: # 1. Analyze the text using the sentiment model analysis = analyze_text(request.user_text) # 2. Retrieve all past entries for robust anomaly check collection = get_mongo_collection() # Fetch all entries, ordered by time (descending) past_entries = list(collection.find().sort("timestamp", -1)) # 3. Check for anomaly is_anomaly = check_for_anomaly(past_entries, analysis["sentiment"]) # 4. Generate the supportive message support_message = generate_support_message(analysis["sentiment"], is_anomaly) # 5. Save the new entry to the database entry_id = insert_checkin_entry( user_text=request.user_text, sentiment_score=analysis["sentiment"], keyword_intensity=analysis["intensity"], anomaly_flag=is_anomaly ) # 6. Return the saved entry ALONGSIDE the generated message return CheckinResponse( id=str(entry_id), timestamp=datetime.datetime.now(), sentiment_score=analysis["sentiment"], anomaly_flag=is_anomaly, support_message=support_message, user_text=request.user_text ) except Exception as e: raise HTTPException(status_code=500, detail=f"Error processing check-in: {str(e)}") # --- 2. GET Endpoint for Timeline Data --- @app.get("/timeline", response_model=List[CheckinResponse]) def get_timeline(): try: # Retrieve all entries, ordered by time collection = get_mongo_collection() entries = list(collection.find().sort("timestamp", 1)) # Convert MongoDB documents to response format timeline_data = [] for entry in entries: timeline_data.append(CheckinResponse( id=str(entry["_id"]), timestamp=entry["timestamp"], sentiment_score=entry["sentiment_score"], anomaly_flag=entry.get("anomaly_flag", False), user_text=entry.get("user_text", "") )) return timeline_data except Exception as e: raise HTTPException(status_code=500, detail=f"Error retrieving timeline: {str(e)}") # --- Health Check Endpoint --- @app.get("/health") def health_check(): return {"status": "healthy", "timestamp": datetime.datetime.now()} # --- CORS Headers (Crucial for Hosting) --- # Enable CORS for frontend development from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173", "http://localhost:3000", "https://aliekargbo.github.io/Daily-Mental-Health-Check-in/"], # Vite dev server allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )