From 972b4c38bdf2845ac70e5b27bf6814eeb2ca4a42 Mon Sep 17 00:00:00 2001 From: bhavanaharshan Date: Sat, 9 May 2026 20:23:25 +0530 Subject: [PATCH 1/2] feat(backend): Member 4 - Core API, Pathway Controller, Safety Plan CRUD, FCM Notifications, BigQuery Insights --- .gitignore | 8 +++ Dockerfile | 16 ++++++ app/__init__.py | 0 app/core/config.py | 16 ++++++ app/main.py | 29 +++++++++++ app/models/schemas.py | 52 +++++++++++++++++++ app/routers/insights.py | 16 ++++++ app/routers/notifications.py | 17 +++++++ app/routers/pathway.py | 44 ++++++++++++++++ app/routers/safety_plan.py | 32 ++++++++++++ app/services/ai_service.py | 83 +++++++++++++++++++++++++++++++ app/services/bigquery_service.py | 42 ++++++++++++++++ app/services/fcm_service.py | 34 +++++++++++++ app/services/firestore_service.py | 41 +++++++++++++++ requirements.txt | 11 ++++ 15 files changed, 441 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app/__init__.py create mode 100644 app/core/config.py create mode 100644 app/main.py create mode 100644 app/models/schemas.py create mode 100644 app/routers/insights.py create mode 100644 app/routers/notifications.py create mode 100644 app/routers/pathway.py create mode 100644 app/routers/safety_plan.py create mode 100644 app/services/ai_service.py create mode 100644 app/services/bigquery_service.py create mode 100644 app/services/fcm_service.py create mode 100644 app/services/firestore_service.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d9592a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.env +venv/ +serviceAccountKey.json +__pycache__/ +*.pyc +.DS_Store +*.egg-info/ +.pytest_cache/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b78c616 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install dependencies first (Docker layer caching) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Cloud Run injects PORT env variable +ENV PORT=8080 +EXPOSE 8080 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..ee6bed6 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,16 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + PROJECT_ID: str + REGION: str = "us-central1" + FIREBASE_SERVICE_ACCOUNT_KEY: str = "./serviceAccountKey.json" + BIGQUERY_DATASET: str = "mindbridge_insights" + BIGQUERY_TABLE: str = "distress_events" + VERTEX_AI_MODEL: str = "gemini-1.5-flash-001" + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..f9d897d --- /dev/null +++ b/app/main.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.routers import pathway, safety_plan, notifications, insights + +app = FastAPI( + title="MindBridge Backend API", + description="Privacy-first distress triage and community support platform.", + version="1.0.0", +) + +# Allow Member 3's frontend to call this API +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, restrict to your frontend domain + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Register all routers +app.include_router(pathway.router) +app.include_router(safety_plan.router) +app.include_router(notifications.router) +app.include_router(insights.router) + + +@app.get("/health") +def health_check(): + return {"status": "ok", "service": "MindBridge Backend"} diff --git a/app/models/schemas.py b/app/models/schemas.py new file mode 100644 index 0000000..2345b0d --- /dev/null +++ b/app/models/schemas.py @@ -0,0 +1,52 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime + + +# --- Pathway Controller --- +class DistressSignal(BaseModel): + user_id: str + distress_score: float # 0.0 to 1.0 from Member 3's edge AI + distress_category: str # e.g. "anxiety", "grief", "burnout" + region_id: Optional[str] = "unknown" # anonymized region (no city name) + + +class SupportPathway(BaseModel): + pathway_type: str # "grounding", "breathing", "peer_support", "crisis" + tool_title: str + tool_description: str + tool_url: Optional[str] = None + crisis_hotline: Optional[str] = None + ai_message: str + + +# --- Safety Plan --- +class SafetyPlanItem(BaseModel): + user_id: str + plan_id: Optional[str] = None + warning_signs: List[str] + coping_strategies: List[str] + support_contacts: List[str] + crisis_numbers: List[str] = ["iCall: 9152987821", "Vandrevala: 1860-2662-345"] + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class SafetyPlanResponse(SafetyPlanItem): + plan_id: str + + +# --- Notifications --- +class NotificationRequest(BaseModel): + user_id: str + fcm_token: str + message_title: str = "MindBridge Check-in" + message_body: str = "How are you feeling today? Take a moment to check in. ๐Ÿ’™" + + +# --- Insights --- +class InsightSummary(BaseModel): + region_id: str + stress_category: str + count: int + date: str diff --git a/app/routers/insights.py b/app/routers/insights.py new file mode 100644 index 0000000..ddb0087 --- /dev/null +++ b/app/routers/insights.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, Query +from app.services.bigquery_service import get_community_insights +from app.models.schemas import InsightSummary +from typing import List + +router = APIRouter(prefix="/insights", tags=["Admin Insights"]) + + +@router.get("/community", response_model=List[InsightSummary]) +async def community_health(days: int = Query(default=7, ge=1, le=90)): + """ + Admin-only endpoint: returns anonymized community stress trends. + Used by Member 3's admin dashboard. No PII is returned. + """ + data = get_community_insights(days=days) + return [InsightSummary(**row) for row in data] diff --git a/app/routers/notifications.py b/app/routers/notifications.py new file mode 100644 index 0000000..15cda70 --- /dev/null +++ b/app/routers/notifications.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, HTTPException +from app.models.schemas import NotificationRequest +from app.services.fcm_service import send_checkin_notification + +router = APIRouter(prefix="/notifications", tags=["Notifications"]) + + +@router.post("/checkin") +async def send_checkin(req: NotificationRequest): + """Send a proactive check-in push notification to a user.""" + try: + result = send_checkin_notification( + req.fcm_token, req.message_title, req.message_body + ) + return {"status": "sent", "fcm_response": result} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Notification failed: {str(e)}") diff --git a/app/routers/pathway.py b/app/routers/pathway.py new file mode 100644 index 0000000..d963a4e --- /dev/null +++ b/app/routers/pathway.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, HTTPException +from app.models.schemas import DistressSignal, SupportPathway +from app.services.ai_service import determine_pathway, get_support_message +from app.services.bigquery_service import log_distress_event + +router = APIRouter(prefix="/pathway", tags=["Pathway Controller"]) + + +@router.post("/analyze", response_model=SupportPathway) +async def analyze_distress(signal: DistressSignal): + """ + Main endpoint: receives distress metadata from Member 3's frontend. + Returns the appropriate support pathway + AI-generated message. + Raw journal text NEVER reaches this endpoint โ€” only the score and category. + """ + if not (0.0 <= signal.distress_score <= 1.0): + raise HTTPException( + status_code=400, detail="distress_score must be between 0.0 and 1.0" + ) + + # 1. Determine pathway based on score + pathway = determine_pathway(signal.distress_score, signal.distress_category) + + # 2. Get AI-generated supportive message + try: + ai_message = get_support_message( + signal.distress_category, signal.distress_score + ) + except Exception: + ai_message = ( + "You're doing great by checking in. Remember, it's okay to ask for help." + ) + + # 3. Log anonymized event to BigQuery (no PII) + try: + log_distress_event( + region_id=signal.region_id or "unknown", + stress_category=signal.distress_category, + distress_score=signal.distress_score, + ) + except Exception: + pass # Don't fail the response if analytics logging fails + + return SupportPathway(ai_message=ai_message, **pathway) diff --git a/app/routers/safety_plan.py b/app/routers/safety_plan.py new file mode 100644 index 0000000..6d9c845 --- /dev/null +++ b/app/routers/safety_plan.py @@ -0,0 +1,32 @@ +from fastapi import APIRouter, HTTPException +from app.models.schemas import SafetyPlanItem, SafetyPlanResponse +from app.services import firestore_service as fs + +router = APIRouter(prefix="/safety-plan", tags=["Safety Plan"]) + + +@router.post("/", response_model=SafetyPlanResponse) +async def create_plan(plan: SafetyPlanItem): + created = fs.create_safety_plan(plan.model_dump()) + return SafetyPlanResponse(**created) + + +@router.get("/{user_id}", response_model=SafetyPlanResponse) +async def get_plan(user_id: str): + plan = fs.get_safety_plan(user_id) + if not plan: + raise HTTPException( + status_code=404, detail="No safety plan found for this user" + ) + return SafetyPlanResponse(**plan) + + +@router.put("/{plan_id}", response_model=dict) +async def update_plan(plan_id: str, updates: dict): + return fs.update_safety_plan(plan_id, updates) + + +@router.delete("/{plan_id}") +async def delete_plan(plan_id: str): + fs.delete_safety_plan(plan_id) + return {"message": "Safety plan deleted successfully"} diff --git a/app/services/ai_service.py b/app/services/ai_service.py new file mode 100644 index 0000000..0375e1c --- /dev/null +++ b/app/services/ai_service.py @@ -0,0 +1,83 @@ +import vertexai +from vertexai.generative_models import ( + GenerativeModel, + SafetySetting, + HarmCategory, + HarmBlockThreshold, +) +from app.core.config import settings + +vertexai.init(project=settings.PROJECT_ID, location=settings.REGION) + +# Safety settings โ€” block anything that could be harmful +SAFETY_SETTINGS = [ + SafetySetting( + category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold=HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, + ), + SafetySetting( + category=HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold=HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, + ), +] + +SYSTEM_INSTRUCTION = """ +You are MindBridge, a compassionate mental wellness support companion. +STRICT RULES: +1. NEVER diagnose any mental illness or medical condition. +2. NEVER prescribe medication or treatments. +3. ALWAYS remind users that you are not a replacement for professional help. +4. Offer only evidence-based coping tools: breathing exercises, grounding techniques, journaling prompts. +5. If distress is severe, ALWAYS surface crisis hotline numbers. +6. Use warm, non-judgmental, simple language. Max 3 sentences. +""" + + +def get_support_message(distress_category: str, distress_score: float) -> str: + model = GenerativeModel( + settings.VERTEX_AI_MODEL, + system_instruction=SYSTEM_INSTRUCTION, + ) + prompt = f""" + A user is experiencing {distress_category} with a distress level of {distress_score:.1f}/1.0. + Provide a brief, warm, supportive message and suggest ONE specific coping tool they can try right now. + Do not diagnose. Do not use clinical language. + """ + response = model.generate_content(prompt, safety_settings=SAFETY_SETTINGS) + return response.text + + +def determine_pathway(distress_score: float, distress_category: str) -> dict: + """ + Core triage logic โ€” maps distress score to a support pathway. + This is the Pathway Controller engine. + """ + if distress_score >= 0.85: + return { + "pathway_type": "crisis", + "tool_title": "You're Not Alone โ€” Immediate Support", + "tool_description": "Your feelings matter. Please reach out to a crisis counselor right now.", + "crisis_hotline": "iCall: 9152987821 | Vandrevala Foundation: 1860-2662-345", + "tool_url": "https://icallhelpline.org", + } + elif distress_score >= 0.65: + return { + "pathway_type": "peer_support", + "tool_title": "Talk to Someone", + "tool_description": "Sometimes sharing helps. Consider reaching out to a trusted friend or peer support group.", + "tool_url": "https://7cups.com", + } + elif distress_score >= 0.40: + return { + "pathway_type": "breathing", + "tool_title": "4-7-8 Breathing Exercise", + "tool_description": "Inhale for 4 counts, hold for 7, exhale for 8. Repeat 3 times to calm your nervous system.", + "tool_url": "https://mindbridge.app/tools/breathing", + } + else: + return { + "pathway_type": "grounding", + "tool_title": "5-4-3-2-1 Grounding Technique", + "tool_description": "Notice 5 things you see, 4 you can touch, 3 you hear, 2 you smell, 1 you taste.", + "tool_url": "https://mindbridge.app/tools/grounding", + } diff --git a/app/services/bigquery_service.py b/app/services/bigquery_service.py new file mode 100644 index 0000000..331ef7e --- /dev/null +++ b/app/services/bigquery_service.py @@ -0,0 +1,42 @@ +from google.cloud import bigquery +from google.oauth2 import service_account +from app.core.config import settings + +credentials = service_account.Credentials.from_service_account_file( + "serviceAccountKey.json" +) + +client = bigquery.Client(credentials=credentials, project=settings.PROJECT_ID) + + +def log_distress_event(region_id: str, stress_category: str, distress_score: float): + """Log anonymized event โ€” NO user_id, NO PII ever enters BigQuery.""" + rows = [ + { + "region_id": region_id, + "stress_category": stress_category, + "distress_score_bucket": round(distress_score, 1), # bucketed, not exact + "timestamp": "AUTO", # BigQuery CURRENT_TIMESTAMP + } + ] + errors = client.insert_rows_json(TABLE_REF, rows) + if errors: + raise RuntimeError(f"BigQuery insert errors: {errors}") + + +def get_community_insights(days: int = 7) -> list[dict]: + """Fetch aggregated community stress trends for admin dashboard.""" + query = f""" + SELECT + region_id, + stress_category, + COUNT(*) as count, + DATE(timestamp) as date + FROM `{TABLE_REF}` + WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL {days} DAY) + GROUP BY region_id, stress_category, date + ORDER BY date DESC + LIMIT 200 + """ + results = client.query(query).result() + return [dict(row) for row in results] diff --git a/app/services/fcm_service.py b/app/services/fcm_service.py new file mode 100644 index 0000000..b7a30ac --- /dev/null +++ b/app/services/fcm_service.py @@ -0,0 +1,34 @@ +import httpx +import google.auth +import google.auth.transport.requests +from app.core.config import settings + +FCM_URL = f"https://fcm.googleapis.com/v1/projects/{settings.PROJECT_ID}/messages:send" + + +def get_access_token() -> str: + """Get a short-lived OAuth2 token for FCM v1 API.""" + credentials, _ = google.auth.default( + scopes=["https://www.googleapis.com/auth/firebase.messaging"] + ) + credentials.refresh(google.auth.transport.requests.Request()) + return credentials.token + + +def send_checkin_notification(fcm_token: str, title: str, body: str) -> dict: + token = get_access_token() + payload = { + "message": { + "token": fcm_token, + "notification": {"title": title, "body": body}, + "data": {"type": "checkin", "source": "mindbridge"}, + } + } + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + with httpx.Client() as client: + response = client.post(FCM_URL, json=payload, headers=headers) + response.raise_for_status() + return response.json() diff --git a/app/services/firestore_service.py b/app/services/firestore_service.py new file mode 100644 index 0000000..75c7ae0 --- /dev/null +++ b/app/services/firestore_service.py @@ -0,0 +1,41 @@ +import firebase_admin +from firebase_admin import credentials, firestore +from app.core.config import settings +import uuid +from datetime import datetime + +# Initialize Firebase only once +if not firebase_admin._apps: + cred = credentials.Certificate(settings.FIREBASE_SERVICE_ACCOUNT_KEY) + firebase_admin.initialize_app(cred) + +db = firestore.client() + +COLLECTION = "safety_plans" + + +def create_safety_plan(plan_data: dict) -> dict: + plan_id = str(uuid.uuid4()) + plan_data["plan_id"] = plan_id + plan_data["created_at"] = datetime.utcnow() + plan_data["updated_at"] = datetime.utcnow() + db.collection(COLLECTION).document(plan_id).set(plan_data) + return plan_data + + +def get_safety_plan(user_id: str) -> dict | None: + docs = db.collection(COLLECTION).where("user_id", "==", user_id).limit(1).stream() + for doc in docs: + return doc.to_dict() + return None + + +def update_safety_plan(plan_id: str, updates: dict) -> dict: + updates["updated_at"] = datetime.utcnow() + db.collection(COLLECTION).document(plan_id).update(updates) + return {"plan_id": plan_id, **updates} + + +def delete_safety_plan(plan_id: str) -> bool: + db.collection(COLLECTION).document(plan_id).delete() + return True diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b1e2bb6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +fastapi==0.111.0 +uvicorn[standard]==0.30.1 +pydantic==2.7.1 +pydantic-settings==2.3.0 +google-cloud-firestore==2.16.0 +google-cloud-bigquery==3.23.1 +google-cloud-aiplatform==1.57.0 +firebase-admin==6.5.0 +python-dotenv==1.0.1 +httpx==0.27.0 +python-jose[cryptography]==3.3.0 \ No newline at end of file From b807eaa07300e2a835234393b1bd85d03ffe189e Mon Sep 17 00:00:00 2001 From: bhavana <155368794+bhavanaharshan@users.noreply.github.com> Date: Sat, 9 May 2026 20:42:46 +0530 Subject: [PATCH 2/2] Update README.md --- README.md | 388 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 386 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8b121c3..f79163c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,386 @@ -# MindBridge -Privacy-First Distress Signal Triage & Community Support Platform +# MindBridge Backend โ€” Member 4: Backend Logic & Integration + +**Branch:** `feature/backend-api-member4` +**Owner:** Ayyappa Das โ€” AM.SC.U4CSE23209 +**Stack:** Python 3.11 ยท FastAPI ยท Firebase Firestore ยท Vertex AI ยท BigQuery ยท FCM ยท Cloud Run + +--- + +## What This Module Does + +This is the core backend engine of MindBridge. It receives distress metadata from the frontend (Member 3), routes it through an AI triage pipeline (Member 2's Vertex AI), and returns the appropriate support tool โ€” all without ever storing raw journal text. + +``` +Frontend (Member 3) + โ”‚ + โ”‚ POST /pathway/analyze + โ”‚ { distress_score: 0.75, distress_category: "anxiety" } + โ–ผ +Pathway Controller โ”€โ”€โ–บ Vertex AI (Member 2) โ”€โ”€โ–บ Support Message + โ”‚ + โ”œโ”€โ”€โ–บ Firestore: Safety Plan CRUD + โ”œโ”€โ”€โ–บ FCM: Push Notification Scheduler + โ””โ”€โ”€โ–บ BigQuery: Anonymized Community Insights (Admin Dashboard) +``` + +--- + +## Project Structure + +``` +mindbridge-backend/ +โ”œโ”€โ”€ app/ +โ”‚ โ”œโ”€โ”€ main.py # FastAPI app entry point + CORS +โ”‚ โ”œโ”€โ”€ core/ +โ”‚ โ”‚ โ””โ”€โ”€ config.py # Env var loading via pydantic-settings +โ”‚ โ”œโ”€โ”€ models/ +โ”‚ โ”‚ โ””โ”€โ”€ schemas.py # All Pydantic request/response models +โ”‚ โ”œโ”€โ”€ routers/ +โ”‚ โ”‚ โ”œโ”€โ”€ pathway.py # POST /pathway/analyze +โ”‚ โ”‚ โ”œโ”€โ”€ safety_plan.py # CRUD /safety-plan/ +โ”‚ โ”‚ โ”œโ”€โ”€ notifications.py # POST /notifications/checkin +โ”‚ โ”‚ โ””โ”€โ”€ insights.py # GET /insights/community +โ”‚ โ””โ”€โ”€ services/ +โ”‚ โ”œโ”€โ”€ ai_service.py # Vertex AI orchestration + triage logic +โ”‚ โ”œโ”€โ”€ firestore_service.py # Firestore read/write for safety plans +โ”‚ โ”œโ”€โ”€ fcm_service.py # Firebase Cloud Messaging +โ”‚ โ””โ”€โ”€ bigquery_service.py # BigQuery insert + query +โ”œโ”€โ”€ Dockerfile # Optimized for Cloud Run cold starts +โ”œโ”€โ”€ requirements.txt +โ””โ”€โ”€ .env # LOCAL ONLY โ€” never committed +``` + +--- + +## API Endpoints + +### 1. Pathway Controller โ€” Core Feature +``` +POST /pathway/analyze +``` +Receives distress metadata from Member 3's edge AI. Returns a support tool recommendation and an AI-generated empathetic message. + +**Request:** +```json +{ + "user_id": "firebase_uid_here", + "distress_score": 0.75, + "distress_category": "anxiety", + "region_id": "kerala" +} +``` + +**Response:** +```json +{ + "pathway_type": "peer_support", + "tool_title": "Talk to Someone", + "tool_description": "Sometimes sharing helps...", + "tool_url": "https://7cups.com", + "crisis_hotline": null, + "ai_message": "It sounds like you're carrying a lot right now..." +} +``` + +**Triage Logic (score โ†’ pathway):** + +| Score Range | Pathway | Tool Returned | +|---|---|---| +| 0.00 โ€“ 0.39 | `grounding` | 5-4-3-2-1 technique | +| 0.40 โ€“ 0.64 | `breathing` | 4-7-8 breathing exercise | +| 0.65 โ€“ 0.84 | `peer_support` | 7cups / trusted contact | +| 0.85 โ€“ 1.00 | `crisis` | iCall + Vandrevala hotlines | + +> **Privacy guarantee:** Raw journal text never reaches this endpoint. Only the score and category (produced on-device by Member 3) are transmitted. + +--- + +### 2. Safety Plan CRUD + +``` +POST /safety-plan/ # Create a new safety plan +GET /safety-plan/{user_id} # Fetch plan by Firebase UID +PUT /safety-plan/{plan_id} # Update existing plan +DELETE /safety-plan/{plan_id} # Delete a plan +``` + +**Create request body:** +```json +{ + "user_id": "firebase_uid_here", + "warning_signs": ["can't sleep", "feeling isolated"], + "coping_strategies": ["call a friend", "go for a walk"], + "support_contacts": ["Mom: 98765xxxxx"], + "crisis_numbers": ["iCall: 9152987821"] +} +``` + +Stored in Firebase Firestore under the `safety_plans` collection. Member 3's frontend reads this for the offline safety plan feature. + +--- + +### 3. Push Notification (FCM) + +``` +POST /notifications/checkin +``` + +**Request:** +```json +{ + "user_id": "firebase_uid_here", + "fcm_token": "device_fcm_token_from_firebase", + "message_title": "MindBridge Check-in", + "message_body": "How are you feeling today? ๐Ÿ’™" +} +``` + +Uses FCM HTTP v1 API with short-lived OAuth2 tokens (not legacy server keys). Member 3 needs to send their device's FCM token when registering. + +--- + +### 4. Community Insights (Admin Dashboard) + +``` +GET /insights/community?days=7 +``` + +Returns anonymized, aggregated BigQuery data for the admin dashboard. No user IDs or PII ever enter BigQuery โ€” only `region_id`, `stress_category`, `distress_score_bucket`, and `timestamp`. + +**Response:** +```json +[ + { "region_id": "kerala", "stress_category": "anxiety", "count": 42, "date": "2025-06-10" }, + { "region_id": "kerala", "stress_category": "burnout", "count": 18, "date": "2025-06-10" } +] +``` + +--- + +### 5. Health Check + +``` +GET /health +โ†’ { "status": "ok", "service": "MindBridge Backend" } +``` + +--- + +## Local Setup (for teammates who want to run this) + +**Prerequisites:** Python 3.11, pip, a `serviceAccountKey.json` from GCP + +```bash +# 1. Clone and switch to this branch +git clone https://github.com/ayyappadasmt/MindBridge.git +cd MindBridge +git checkout feature/backend-api-member4 +cd mindbridge-backend + +# 2. Create virtual environment +python -m venv venv +source venv/bin/activate # Mac/Linux +.\venv\Scripts\activate # Windows + +# 3. Install dependencies +pip install -r requirements.txt + +# 4. Create .env (ask Ayyappa for values) +cp .env.example .env + +# 5. Run the server +uvicorn app.main:app --reload --port 8080 + +# 6. Open Swagger UI to test all endpoints +# โ†’ http://localhost:8080/docs +``` + +--- + +## Environment Variables + +Create a `.env` file locally. **Never commit this file.** + +```env +PROJECT_ID=your-gcp-project-id +REGION=us-central1 +FIREBASE_SERVICE_ACCOUNT_KEY=./serviceAccountKey.json +BIGQUERY_DATASET=mindbridge_insights +BIGQUERY_TABLE=distress_events +VERTEX_AI_MODEL=gemini-1.5-flash-001 +``` + +For Cloud Run deployment, these are set as environment variables by Member 1 (do not hardcode them). + +--- + +## Deployment + +This service is containerized and deployed to Cloud Run by Member 1. + +```bash +# Build image +docker build -t gcr.io/PROJECT_ID/mindbridge-backend . + +# Push to registry +docker push gcr.io/PROJECT_ID/mindbridge-backend + +# Deploy (Member 1 runs this) +gcloud run deploy mindbridge-backend \ + --image gcr.io/PROJECT_ID/mindbridge-backend \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated \ + --set-env-vars PROJECT_ID=...,REGION=us-central1,... +``` + +After deployment, Cloud Run provides a public HTTPS URL โ€” share it with Member 3 as the `BASE_URL`. + +--- + +## Integration Notes by Teammate + +--- + +### For Member 1 โ€” Cloud Infrastructure + +**IAM roles required** for the Cloud Run service account: + +| Role | Purpose | +|---|---| +| `roles/datastore.user` | Firestore read/write for safety plans | +| `roles/bigquery.dataEditor` | Insert anonymized events | +| `roles/aiplatform.user` | Vertex AI text generation | +| `roles/firebase.sdkAdminServiceAgent` | FCM push notifications | + +**BigQuery table to create before first deploy:** + +``` +Dataset: mindbridge_insights +Table: distress_events + +Schema: + region_id STRING REQUIRED + stress_category STRING REQUIRED + distress_score_bucket FLOAT64 REQUIRED + timestamp TIMESTAMP REQUIRED (default: CURRENT_TIMESTAMP) +``` + +> Do NOT add `user_id` or any other user-identifiable column to this table. Privacy by design. + +**Secret Manager:** Store `serviceAccountKey.json` as a secret, mount it at runtime instead of bundling in the image. + +--- + +### For Member 2 โ€” AI & Data Engineer + +**Vertex AI model in use:** + +``` +gemini-1.5-flash-001 +``` + +Update me if you're using a different model version โ€” I'll change the `VERTEX_AI_MODEL` env var. + +**System instruction lives in** `app/services/ai_service.py โ†’ SYSTEM_INSTRUCTION`. You can refine the wording, but these rules must be preserved: +- Never diagnose a mental illness +- Never recommend medication +- Always surface crisis resources at high distress scores +- Max 3 sentences in responses + +**BigQuery schema I'm inserting** (confirm this matches your design): + +``` +region_id STRING (broad region, e.g. "kerala_south" โ€” no city/street) +stress_category STRING (e.g. "anxiety", "grief", "burnout") +distress_score_bucket FLOAT64 (rounded to 1 decimal, e.g. 0.7 โ€” not raw score) +timestamp TIMESTAMP +``` + +--- + +### For Member 3 โ€” Frontend & Edge AI + +**Base URL** (available after Member 1 deploys): + +``` +https://mindbridge-backend-xxxx-uc.a.run.app +``` + +**The one endpoint your app calls on every journal analysis:** + +``` +POST {BASE_URL}/pathway/analyze +Content-Type: application/json + +{ + "user_id": "", + "distress_score": 0.75, โ† float from your TFLite model, 0.0โ€“1.0 + "distress_category": "anxiety", โ† string label from your on-device classifier + "region_id": "kerala" โ† broad region only (no street/city names) +} +``` + +> Only send the **metadata** from your on-device model. Raw journal text must never leave the device. + +**Safety plan endpoints** (for offline safety plan UI): + +``` +GET {BASE_URL}/safety-plan/{firebase_uid} โ†’ load user's plan on app start +POST {BASE_URL}/safety-plan/ โ†’ save new plan +PUT {BASE_URL}/safety-plan/{plan_id} โ†’ update plan +``` + +Cache the GET response locally (AsyncStorage / SharedPreferences) so the plan loads even without internet โ€” this satisfies the offline mode requirement. + +**FCM integration:** + +``` +POST {BASE_URL}/notifications/checkin +{ + "user_id": "", + "fcm_token": "", โ† get this from FirebaseMessaging.getToken() + "message_title": "MindBridge Check-in", + "message_body": "How are you feeling today? ๐Ÿ’™" +} +``` + +Send me the FCM token when the user logs in or when it refreshes. + +**Admin dashboard data** (if you're building the insights view): + +``` +GET {BASE_URL}/insights/community?days=7 +``` + +Returns an array of `{ region_id, stress_category, count, date }` โ€” no PII. + +**Test all endpoints interactively:** `{BASE_URL}/docs` + +--- + +## Safety & Privacy Design + +| Principle | Implementation | +|---|---| +| Raw journal text stays on device | Pathway endpoint receives only score + category | +| No PII in BigQuery | Only region, category, bucketed score, timestamp | +| AI cannot diagnose | System instruction + Vertex AI safety filters block harmful output | +| Crisis resources always surface at high distress | Hard-coded threshold at score โ‰ฅ 0.85 | +| Firestore access scoped per user | Queries filter by `user_id` from Firebase Auth | + +--- + +## SDG Alignment + +- **SDG 3 โ€“ Good Health:** Early, non-clinical distress triage with escalating support pathways +- **SDG 10 โ€“ Reduced Inequalities:** Privacy-first design removes cost and stigma barriers; multilingual-ready API +- **SDG 9 โ€“ Innovation:** Serverless, scalable microservice architecture on GCP Cloud Run + +--- + +## Contact + +**Ayyappa Das** โ€” AM.SC.U4CSE23209 +Branch: `feature/backend-api-member4` +For questions about API contracts or integration, open a GitHub issue on this repo tagging `@ayyappadasmt`.