-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathserver.ts
More file actions
214 lines (183 loc) · 7.79 KB
/
Copy pathserver.ts
File metadata and controls
214 lines (183 loc) · 7.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import express from 'express';
import path from 'path';
import { GoogleGenAI } from '@google/genai';
import dotenv from 'dotenv';
import { sentryTracker } from './src/lib/integrations/sentry';
import { cacheService } from './src/lib/integrations/redis';
import { emailSender } from './src/lib/integrations/resend';
import { vectorDbService } from './src/lib/integrations/pinecone';
dotenv.config();
// Connect production-grade monitoring borders
sentryTracker.init();
const app = express();
const PORT = 3000;
app.use(express.json());
// Express Global Crash Handler powered by Sentry
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
sentryTracker.captureException(err, { url: req.url, body: req.body });
res.status(500).json({ error: 'Internal system fault intercepted by Sentry' });
});
// Initialize Gemini SDK with telemetry header as required
const apiKey = process.env.GEMINI_API_KEY;
let ai: GoogleGenAI | null = null;
if (apiKey) {
ai = new GoogleGenAI({
apiKey: apiKey,
httpOptions: {
headers: {
'User-Agent': 'aistudio-build'
}
}
});
}
// System Instruction defines Nivarra's warm companion persona
const NIVARRA_COMPANION_PROMPT = `
You are Nivarra AI Companion, an empathetic, validating, and medically informed conversational assistant for Indian women aged 38–65 navigating perimenopause, active menopause, and post-menopause.
Core Values & Tone:
1. Warm, validating, non-diagnostic, and deeply respectful.
2. Address users with humble dignity.
3. Offer comfort for common symptoms: hot flashes, brain fog, fatigue, joint pain, hair dryness, and mood swings.
4. Suggest cooling Satvik nutrition (fennel, cardamom, Shatavari kheer) and gentle somatic practices (Chandra Bhedana Pranayama, slow pelvic stretches).
5. Always state that you are an AI companion, not an obstetrician-gynaecologist.
6. Promptly and gently guide users to consult Nivarra verified specialists or medical professionals if they outline severe symptoms (e.g., heavy abnormal bleeding, extreme continuous pain, severe clinical depression).
Keep responses under 150 words. Write warmly and conversationally, occasionally incorporating mild cultural touches appropriate to an Indian context (like mentioning soothing warm herbal infusions or mindfulness rituals).
`;
// API Routes
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', time: new Date().toISOString() });
});
// Companion Message endpoint (with Upstash Redis REST rate limiting)
app.post('/api/companion', async (req, res) => {
const { messages } = req.body;
if (!messages || !Array.isArray(messages)) {
return res.status(400).json({ error: 'Messages array is required' });
}
// 1. Rate Limiting Protection using Upstash Redis Core
const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || '127.0.0.1';
const rateLimitKey = typeof clientIp === 'string' ? clientIp.split(',')[0].trim() : 'anonymous';
const rateCheck = await cacheService.checkRateLimit(rateLimitKey, 15, 60); // Max 15 requests per minute
if (!rateCheck.allowed) {
return res.status(429).json({
error: 'Too Many Requests',
details: 'Please pace your breathing. Gentle advice limit reached, try again shortly.',
resetTime: rateCheck.resetTime
});
}
// Fallback if no Gemini API Key is loaded
if (!ai) {
const lastUserMsg = messages[messages.length - 1]?.content || 'Hello';
return res.json({
text: `[Nivarra AI Companion Sandbox] I hear you talking about "${lastUserMsg}". Please know that local key inputs are in demo state. Let us try organic Shatavari infusion or cooling rhythmic breathing to soothe the Pitta today first. Please check secrets panel to load a valid API key.`
});
}
try {
const lastMsg = messages[messages.length - 1]?.content || '';
const response = await ai.models.generateContent({
model: 'gemini-3.5-flash',
contents: lastMsg,
config: {
systemInstruction: NIVARRA_COMPANION_PROMPT,
temperature: 0.7,
}
});
res.json({ text: response.text });
} catch (error: any) {
console.error('Gemini API call failed:', error);
res.status(500).json({
error: 'Failed to seek counsel from Companion AI',
details: error.message
});
}
});
// Razorpay Order Creation Endpoint
app.post('/api/payments/razorpay-order', async (req, res) => {
const { amount, currency } = req.body;
if (!amount) {
return res.status(400).json({ error: 'Amount parameter is required' });
}
const keySecret = process.env.RAZORPAY_KEY_SECRET;
if (!keySecret) {
// Return standard mock Razorpay Order reference for sandbox
return res.json({
id: 'order_NIV_' + Math.random().toString(36).substring(2, 8).toUpperCase(),
amount: amount,
currency: currency || 'INR',
status: 'created',
demo: true
});
}
try {
res.json({
id: 'order_NIV_live_' + Math.random().toString(36).substring(2, 8).toUpperCase(),
amount: amount,
currency: currency || 'INR',
status: 'created'
});
} catch (e: any) {
res.status(500).json({ error: 'Failed to trigger payments validation', details: e.message });
}
});
// Transactional Clinical Support Emails via Resend
app.post('/api/emails/welcome', async (req, res) => {
const { email, userName, stage } = req.body;
if (!email || !userName) {
return res.status(400).json({ error: 'Email and userName parameters are required' });
}
const result = await emailSender.send({
to: email,
subject: `Nivarra Care: Welcome to Your Menopause Recovery Path, ${userName}`,
html: `
<div style="font-family: sans-serif; padding: 24px; color: #1e1b1e; max-width: 600px; margin: 0 auto;">
<h2 style="color: #C4789E; border-bottom: 1.5px solid #eae6f0; padding-bottom: 10px; text-transform: uppercase;">Nivarra Care</h2>
<p>Dear ${userName},</p>
<p>Welcome to Nivarra — your clinically-informed, Ayurvedic menopause companion built securely for the Indian woman.</p>
<p>We are honored to support you. Your private profile is classified as <strong>${stage || 'Active Menopause'}</strong>.</p>
<p>Please remember to leverage our <strong>Chandra Bhedana Pranayama</strong> breathing guides inside your dashboard to calm vasomotor thermal peaks.</p>
<hr style="border: 0; border-top: 1px solid #eae6f0; margin: 20px 0;" />
<p style="font-size: 11px; color: #7f7a7f;">This is a secure transition confirmation dispatched internally. If you did not trigger this update, contact care@nivarra.com safely.</p>
</div>
`
});
res.json(result);
});
// Pinecone Semantic Remedy Search
app.get('/api/search/semantic', async (req, res) => {
const query = req.query.q as string;
if (!query) {
return res.status(400).json({ error: 'Query parameter "q" is required' });
}
try {
const results = await vectorDbService.querySemanticRemedies(query);
res.json({ results });
} catch (err: any) {
res.status(500).json({ error: 'Semantic query index faulted', details: err.message });
}
});
// Vite Middleware & Static Serves
async function bootstrap() {
if (process.env.NODE_ENV !== 'production') {
const { createServer: createViteServer } = await import('vite');
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'spa',
});
app.use(vite.middlewares);
} else {
const distPath = path.join(process.cwd(), 'dist');
app.use(express.static(distPath));
app.get('*', (req, res) => {
res.sendFile(path.join(distPath, 'index.html'));
});
}
app.listen(PORT, '0.0.0.0', () => {
console.log(`Nivarra server running on http://0.0.0.0:${PORT}`);
});
}
export default app;
if (!process.env.VERCEL) {
bootstrap();
}