Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ai/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PORT=5000
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,https://pulse-11de7.web.app
GROQ_API_KEY=your_groq_api_key
GEMINI_API_KEY=your_gemini_api_key
15 changes: 15 additions & 0 deletions ai/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PORT:-5000} app:app"]
17 changes: 15 additions & 2 deletions ai/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,20 @@
import os
import requests
app = Flask(__name__)
CORS(app) # Allows Node.js backend to call this
default_allowed_origins = [
"https://pulse-11de7.web.app",
"https://pulse-11de7.firebaseapp.com",
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
]
allowed_origins = default_allowed_origins + [
origin.strip()
for origin in os.getenv("CORS_ORIGINS", "").split(",")
if origin.strip()
]
CORS(app, resources={r"/*": {"origins": allowed_origins}})

# ── Health check ──────────────────────────────
@app.route('/health', methods=['GET'])
Expand Down Expand Up @@ -393,5 +406,5 @@ def pre_alert():
print(" POST /cluster")
print(" POST /match")
print(" POST /escalate")
app.run(host='0.0.0.0', port=port, debug=True)
app.run(host='0.0.0.0', port=port, debug=os.getenv("FLASK_DEBUG") == "1")

3 changes: 2 additions & 1 deletion ai/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
flask
flask-cors
gunicorn
groq
google-genai
firebase-admin
python-dotenv
python-dotenv
15 changes: 15 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
PORT=3000
AI_BASE_URL=http://localhost:5000
FRONTEND_PUBLIC_URL=http://localhost:5173
BACKEND_PUBLIC_URL=http://localhost:3000
TWILIO_WEBHOOK_BASE_URL=http://localhost:3000
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,https://pulse-11de7.web.app

# Use this on Render. For local dev you can also place serviceAccountKey.json in backend/.
FIREBASE_SERVICE_ACCOUNT_JSON={"type":"service_account","project_id":"pulse-11de7"}

TWILIO_ACCOUNT_SID=your_twilio_account_sid
TWILIO_AUTH_TOKEN=your_twilio_auth_token
TWILIO_PHONE_NUMBER=+14155238886
TWILIO_REAL_NUMBER=+15551234567
MY_PHONE_NUMBER=+919999999999
13 changes: 13 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

ENV NODE_ENV=production
EXPOSE 3000

CMD ["node", "index.js"]
102 changes: 76 additions & 26 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,71 @@ const express = require('express');
const cors = require('cors');
const admin = require('firebase-admin');
const twilio = require('twilio'); // MUST COME BEFORE CLIENT
const serviceAccount = require('./serviceAccountKey.json');
const fs = require('fs');
const path = require('path');

const app = express();
app.get('/test', (req, res) => {
res.send("TEST WORKING");
});
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());

const defaultAllowedOrigins = [
'https://pulse-11de7.web.app',
'https://pulse-11de7.firebaseapp.com',
'http://localhost:5173',
'http://localhost:3000',
'http://127.0.0.1:5173',
'http://127.0.0.1:3000'
];

const allowedOrigins = [
...defaultAllowedOrigins,
...(process.env.CORS_ORIGINS || '').split(',').map(origin => origin.trim()).filter(Boolean)
];

app.use(cors({
origin(origin, callback) {
if (!origin || allowedOrigins.includes(origin)) return callback(null, true);
return callback(new Error(`CORS blocked origin: ${origin}`));
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));

const client = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);

function loadFirebaseCredential() {
if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
return JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
}

const localServiceAccountPath = path.join(__dirname, 'serviceAccountKey.json');
if (fs.existsSync(localServiceAccountPath)) {
return require(localServiceAccountPath);
}

throw new Error('Missing Firebase Admin credentials. Set FIREBASE_SERVICE_ACCOUNT_JSON or add backend/serviceAccountKey.json locally.');
}

admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
credential: admin.credential.cert(loadFirebaseCredential())
});

const db = admin.firestore();
const PORT = process.env.PORT || 3000;
const AI_BASE_URL = (process.env.AI_BASE_URL || 'http://localhost:5000').replace(/\/$/, '');
const FRONTEND_PUBLIC_URL = (process.env.FRONTEND_PUBLIC_URL || process.env.PUBLIC_BASE_URL || 'http://localhost:5173').replace(/\/$/, '');
const BACKEND_PUBLIC_URL = (process.env.BACKEND_PUBLIC_URL || process.env.RENDER_EXTERNAL_URL || `http://localhost:${PORT}`).replace(/\/$/, '');
const TWILIO_WEBHOOK_BASE_URL = (process.env.TWILIO_WEBHOOK_BASE_URL || process.env.NGROK_URL || BACKEND_PUBLIC_URL).replace(/\/$/, '');

function aiEndpoint(route) {
return `${AI_BASE_URL}${route}`;
}
// ─── HELPER FUNCTIONS ───────────────────────────────────────────────

// Distance between two coordinates in km
Expand Down Expand Up @@ -54,7 +98,7 @@ function safe(val, fallback = '') {

async function enrichWithPULSEAI(reportId, rawText) {
try {
const res = await fetch('http://localhost:5000/analyze', {
const res = await fetch(aiEndpoint('/analyze'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: rawText })
Expand Down Expand Up @@ -103,7 +147,7 @@ async function enrichWithPULSEAI(reportId, rawText) {
affected_people: doc.data().affected_people || 0
}));

const clusterRes = await fetch('http://localhost:5000/cluster', {
const clusterRes = await fetch(aiEndpoint('/cluster'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reports })
Expand All @@ -129,7 +173,7 @@ for (const cluster of clusterData.clusters) {
}

// Escalate urgency on old unresolved reports
fetch('http://localhost:5000/escalate', {
fetch(aiEndpoint('/escalate'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reports: reports })
Expand Down Expand Up @@ -194,7 +238,7 @@ async function deleteConversation(senderNumber) {
// Detect language using AI
async function detectLanguage(text) {
try {
const res = await fetch('http://localhost:5000/analyze', {
const res = await fetch(aiEndpoint('/analyze'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
Expand Down Expand Up @@ -388,7 +432,7 @@ async function processVerificationAsync(mediaUrl, senderNumber, task, taskId, vo
`${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
).toString('base64');

const verifyRes = await fetch('http://localhost:5000/verify-proof', {
const verifyRes = await fetch(aiEndpoint('/verify-proof'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Expand Down Expand Up @@ -1074,14 +1118,19 @@ app.post('/sms-reply', async (req, res) => {
// START CALL
app.post("/start-call", async (req, res) => {
try {
const toPhone = req.body.phone || process.env.MY_PHONE_NUMBER;
if (!toPhone) {
return res.status(400).json({ error: 'Missing destination phone number' });
}

await client.calls.create({
to: process.env.MY_PHONE_NUMBER, // 👈 YOUR PHONE HERE
to: toPhone,
from: process.env.TWILIO_REAL_NUMBER, // 👈 TWILIO NUMBER
url: `${process.env.NGROK_URL}/incoming-call`
url: `${TWILIO_WEBHOOK_BASE_URL}/incoming-call`
});
// twiml: `
// <Response>
// <Redirect>${process.env.NGROK_URL}/incoming-call</Redirect>
// <Redirect>${TWILIO_WEBHOOK_BASE_URL}/incoming-call</Redirect>
// </Response>
// `

Expand All @@ -1100,7 +1149,7 @@ app.all('/incoming-call', (req, res) => {
res.set('Content-Type', 'text/xml');
res.send(`
<Response>
<Gather action="${process.env.NGROK_URL}/handle-language" method="POST" numDigits="1" timeout="10">
<Gather action="${TWILIO_WEBHOOK_BASE_URL}/handle-language" method="POST" numDigits="1" timeout="10">
<Say language="hi-IN" voice="Polly.Aditi">
Namaste. PULSE mein aapka swagat hai.
Hindi ke liye 1 dabaiye.
Expand All @@ -1109,7 +1158,7 @@ app.all('/incoming-call', (req, res) => {
English ke liye 4 dabaiye.
</Say>
</Gather>
<Redirect>${process.env.NGROK_URL}/incoming-call</Redirect>
<Redirect>${TWILIO_WEBHOOK_BASE_URL}/incoming-call</Redirect>
</Response>
`);
});
Expand All @@ -1120,7 +1169,7 @@ app.all('/handle-language', (req, res) => {
if (!digit) {
return res.send(`
<Response>
<Redirect>${process.env.NGROK_URL}/incoming-call</Redirect>
<Redirect>${TWILIO_WEBHOOK_BASE_URL}/incoming-call</Redirect>
</Response>
`);
}
Expand All @@ -1142,13 +1191,13 @@ app.all('/handle-language', (req, res) => {
res.set('Content-Type', 'text/xml');
res.send(`
<Response>
<Gather action="${process.env.NGROK_URL}/handle-keypress?lang=${lang.code}&amp;langname=${lang.name}" method="POST" numDigits="1" timeout="10">
<Gather action="${TWILIO_WEBHOOK_BASE_URL}/handle-keypress?lang=${lang.code}&amp;langname=${lang.name}" method="POST" numDigits="1" timeout="10">
<Say language="${lang.code}">
${menus[lang.code]}
</Say>
</Gather>
<!-- THIS SAVES YOUR CALL FROM DYING -->
<Redirect>${process.env.NGROK_URL}/handle-language</Redirect>
<Redirect>${TWILIO_WEBHOOK_BASE_URL}/handle-language</Redirect>
</Response>
`);
});
Expand All @@ -1175,7 +1224,7 @@ app.all('/handle-keypress', (req, res) => {
<Response>
<Say language="${lang}">${confirms[lang]}</Say>
<Record
action="${process.env.NGROK_URL}/handle-recording?need_type=${needType}&amp;lang=${lang}&amp;langname=${langname}"
action="${TWILIO_WEBHOOK_BASE_URL}/handle-recording?need_type=${needType}&amp;lang=${lang}&amp;langname=${langname}"
method="POST"
maxLength="30"
playBeep="true"
Expand Down Expand Up @@ -1319,7 +1368,7 @@ async function runEscalation() {
days_unmet: doc.data().days_unmet || 0
}));

const res = await fetch('http://localhost:5000/escalate', {
const res = await fetch(aiEndpoint('/escalate'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reports })
Expand All @@ -1330,7 +1379,7 @@ async function runEscalation() {
console.log(`⬆️ Hourly escalation: ${data.escalated_count} reports escalated`);

// Re-run clustering after escalation
const clusterRes = await fetch('http://localhost:5000/cluster', {
const clusterRes = await fetch(aiEndpoint('/cluster'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reports })
Expand Down Expand Up @@ -1441,7 +1490,7 @@ app.post('/generate-report', async (req, res) => {
}

// Call Person A's report generator
const flaskRes = await fetch('http://localhost:5000/generate-report', {
const flaskRes = await fetch(aiEndpoint('/generate-report'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cluster, reports: reportsData })
Expand Down Expand Up @@ -1913,8 +1962,9 @@ app.post('/chat', async (req, res) => {
const text = message.toLowerCase();

// 🔥 Get number from ENV (NO HARDCODE)
const phone = process.env.TWILIO_PHONE_NUMBER;
const baseUrl = process.env.PUBLIC_BASE_URL || 'http://localhost:3000';
const phone = process.env.TWILIO_PHONE_NUMBER || '+14155238886';
const frontendUrl = FRONTEND_PUBLIC_URL;
const backendUrl = BACKEND_PUBLIC_URL;

// Convert to WhatsApp link format (remove +)
const whatsappLink = `https://wa.me/${phone.replace('+', '')}`;
Expand Down Expand Up @@ -1948,7 +1998,7 @@ Please report it properly so we can act fast:
actions: [
{
label: "Open Intake Form",
link: `${baseUrl}/intake?msg=${encodeURIComponent(message)}`
link: `${frontendUrl}/intake?msg=${encodeURIComponent(message)}`
},
{ label: "WhatsApp Report", link: whatsappLink }
]
Expand All @@ -1958,7 +2008,7 @@ Please report it properly so we can act fast:
// ─── 3. ANALYTICS MODE 📊 ─────────────────────────

if (isAnalytics) {
const analyticsRes = await fetch(`${baseUrl}/analytics`);
const analyticsRes = await fetch(`${backendUrl}/analytics`);
const data = await analyticsRes.json();

const affected = data.analytics?.reports?.total_affected || 0;
Expand All @@ -1977,7 +2027,7 @@ Volunteers Active: ${volunteers}`
// ─── 4. ALERT MODE ⚠️ ─────────────────────────────

if (isAlerts) {
const alertRes = await fetch(`${baseUrl}/predictive-alerts`);
const alertRes = await fetch(`${backendUrl}/predictive-alerts`);
const data = await alertRes.json();

if (!data.alerts || data.alerts.length === 0) {
Expand Down Expand Up @@ -2012,7 +2062,7 @@ Basically… we turn chaos into coordinated action.`

// ─── 6. AI MODE 🤖 ─────────────────────────────

const aiRes = await fetch(`${process.env.AI_BASE_URL}/ask-ai`, {
const aiRes = await fetch(aiEndpoint('/ask-ai'), {
method: "POST",
headers: {
"Content-Type": "application/json"
Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
Expand Down
Loading