sonuprasad23 commited on
Commit
0c14067
·
1 Parent(s): bd7e728
Files changed (4) hide show
  1. Dockerfile +20 -0
  2. google-credentials.json +13 -0
  3. package.json +18 -0
  4. server.js +171 -0
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official lightweight Node.js image
2
+ FROM node:18-alpine
3
+
4
+ # Set the working directory inside the container
5
+ WORKDIR /app
6
+
7
+ # Copy package.json and package-lock.json to leverage Docker cache
8
+ COPY package*.json ./
9
+
10
+ # Install production dependencies
11
+ RUN npm install --production
12
+
13
+ # Copy the rest of your application code
14
+ COPY . .
15
+
16
+ # Expose the port the app runs on
17
+ EXPOSE 3001
18
+
19
+ # The command to run your application
20
+ CMD ["node", "server.js"]
google-credentials.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "service_account",
3
+ "project_id": "staff-sheet-chatbot",
4
+ "private_key_id": "450ad00226e86e8b79be3fa7f55099cfb005ca4c",
5
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDgKOvVq3WkZZ5G\n8qxXLWwTUnLgvfEgodOXtFF1aO+Cb8VbB1AR43ksrKaTVE3M2mtoN+jaaQ72KTwH\nn+rIyJUDITaw9GBthmzjLIquECj7haa3h1BRBjiZY1FMTysdIg3HK8GQDjbNf5UE\nEwS3NOp6EvNp8gTVejF1c819KZ/RCDQPlNWC+Ol8eet9GIiEHzgsqv0YRHNdqwV6\nrV7oTxOZ3F7s6J7a6bVkQkP51M/l5rv6xObVaikkk+XcUrFY77FjkzvbuyKEYRoq\neHJ1FTs02GplhUhoy/zIxNAvAZF3fMhk1kXNUlgPcS20LwHi2ybt+joFMYaUjzBl\nCBS9b/S5AgMBAAECggEABmS8Z0WC7zuzwj/JMRpbTwjGvIjGYCElfRn4SdJCpJSG\nC0Tf4Ia9rcvF24Vny6w+LISNuG8PLW+7JdeGlY/9ChAQNIdf2bfgErGySektlhD3\nkpwCKzFze1KFHtlSODxAqtnhZTe2kfFt4WYeeBBDENTKCw9LWoBn+TrilUuS4p4c\n3JpGnSuI1MWyzMkggGMC3GyLzQhUrhfKk8GNrW76Mpc91WiHyy9xv9tvk4ipMVpc\n0yNzyzLe7UniJY0MsUKw759YtfWgkhr4mO8/4SNWo/tyI1Qgy+SGIJpWR77KTkvj\nVHvd9ynMQdx4JZXI0s0RzlmJ6MbADJFA8ZKzM2uuwQKBgQD1tUHUSU1rN9V2PVl8\noAW5uSKTkHBtB7hlsvhmc3oEUhkx+hfJcYnvut9vivZPQmAN9vSizL5jWE8a9kWq\nAdGOOObMDP3i3/y+/hyBfPcvQnItbUpIGIizFeLhpB2Dv8eyWHkQsdA3oqkGMh/V\nAU7KxQn5jIlSkGfz05c3CrWgoQKBgQDpjJn8RKPboNeZpYM6HGK65A2AA685QjI3\nNbJgpTlcK5e0CE1DJnCXkd+I8RRLSyY9gfGfiEE0rI5G4naWGx0m2xCkFPM0PkDb\ngu9CkxWewEv+wVge9Dh3QKsZFup7v2sut6pr/MKtQndrU6Q6MCPQ0ipjJVVWCjTL\nIEN2c/wlGQKBgCarMgMAzkhTbyq+mPi+DmieNEzY7HicQG7w7ZriDY/kg8aTv26a\nicTKuiNJ0V3D6m4IMZP1ePkVis2JeIk65Ann9pqiaMptyc8dGsteKWCsql9v1cLm\n6YyVozEzrNlxMzJkLhlsgoJe/Q7WkJ/rJsVOoTU+4tW1mEaNRD0LWG5BAoGBANQc\nR0nvCSaSSN2Zjbo8R4rXTk+CiSSmszajRRMzXAoS6V+v3oPrKHzwFiWoVjcf1GYu\nW0T5mcb+JOXKDcu8alh8wvtSr6M2Gu+CVkyPiyY4HY0NCjxqr9HW4M4Tmviy2+R9\nGIGk+SYkZJMMTu4uBIlGJwsOwBc5g8Mr1TmCk3EZAoGAQtPF5LD4yYAzFrbwYtzZ\nyThMMa0Fp35TLTTYXoZ2GIyUAJTLbW0SSvT5kglvNxz72iTikCJCJ8YRZ5qe69w3\nnzWO3vwPMKdDuBMkbHF1J6j6yesosidEOhRH5pg29z6AlmzgfUV/c2JVzhkb28aB\nuZS3oPNqlNcMv81appq53/Y=\n-----END PRIVATE KEY-----\n",
6
+ "client_email": "sheet-reader-bot@staff-sheet-chatbot.iam.gserviceaccount.com",
7
+ "client_id": "108371730199027670321",
8
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9
+ "token_uri": "https://oauth2.googleapis.com/token",
10
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/sheet-reader-bot%40staff-sheet-chatbot.iam.gserviceaccount.com",
12
+ "universe_domain": "googleapis.com"
13
+ }
package.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chatbot-backend",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1"
7
+ },
8
+ "keywords": [],
9
+ "author": "",
10
+ "license": "ISC",
11
+ "description": "",
12
+ "dependencies": {
13
+ "@google/generative-ai": "^0.24.1",
14
+ "dotenv": "^17.2.1",
15
+ "express": "^5.1.0",
16
+ "googleapis": "^154.0.0"
17
+ }
18
+ }
server.js ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const { google } = require('googleapis');
3
+ const { GoogleGenerativeAI } = require('@google/generative-ai');
4
+ const cors = require('cors');
5
+ require('dotenv').config();
6
+
7
+ const app = express();
8
+ app.use(express.json());
9
+
10
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
11
+ app.use(cors({ origin: frontendUrl }));
12
+
13
+ const PORT = process.env.PORT || 3001;
14
+
15
+ const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
16
+ const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
17
+
18
+ let processedStaffCache = null;
19
+ let rawSheetCache = null;
20
+ const CACHE_DURATION_MS = 300000;
21
+
22
+ const isValidName = (name) => name && name.trim() !== '' && name.trim() !== '-';
23
+
24
+ function getGoogleAuth() {
25
+ if (process.env.GOOGLE_CREDENTIALS_JSON) {
26
+ const credentials = JSON.parse(process.env.GOOGLE_CREDENTIALS_JSON);
27
+ return new google.auth.GoogleAuth({
28
+ credentials,
29
+ scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
30
+ });
31
+ }
32
+ return new google.auth.GoogleAuth({
33
+ keyFile: 'google-credentials.json',
34
+ scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
35
+ });
36
+ }
37
+
38
+ async function updateSheetCache() {
39
+ try {
40
+ console.log('Refreshing spreadsheet data cache...');
41
+ const auth = getGoogleAuth();
42
+ const sheets = google.sheets({ version: 'v4', auth });
43
+ const response = await sheets.spreadsheets.values.get({
44
+ spreadsheetId: process.env.GOOGLE_SHEET_ID,
45
+ range: 'Sheet1!A:H',
46
+ });
47
+ const rows = response.data.values;
48
+ if (!rows || rows.length === 0) {
49
+ console.log('No data found in spreadsheet.');
50
+ return;
51
+ }
52
+
53
+ rawSheetCache = rows;
54
+ const headers = rows[0];
55
+ const dataRows = rows.slice(1);
56
+ let currentLocation = '';
57
+ const allStaff = [];
58
+
59
+ dataRows.forEach((row, rowIndex) => {
60
+ const rowData = {};
61
+ headers.forEach((header, index) => rowData[header] = row[index] || '');
62
+
63
+ if (isValidName(rowData.Location)) {
64
+ currentLocation = rowData.Location;
65
+ }
66
+ rowData.Location = currentLocation;
67
+
68
+ const baseId = `r${rowIndex}`;
69
+
70
+ if (isValidName(rowData.Provider)) {
71
+ allStaff.push({ ...rowData, id: `${baseId}-p`, role: 'Provider' });
72
+ }
73
+ if (isValidName(rowData['Team Leads/Manager'])) {
74
+ allStaff.push({ ...rowData, id: `${baseId}-tl`, role: 'Team Lead / Manager', Extension: rowData['Ext/Phone#'] });
75
+ }
76
+ if (isValidName(rowData.MA)) {
77
+ allStaff.push({ ...rowData, id: `${baseId}-ma`, role: 'Medical Assistant', Extension: rowData.Extension });
78
+ }
79
+ if (isValidName(rowData.VA)) {
80
+ allStaff.push({ ...rowData, id: `${baseId}-va`, role: 'Virtual Assistant', Extension: rowData.Ext });
81
+ }
82
+ if (isValidName(rowData['Ext/Phone#']) && /^[a-zA-Z]+-\s*\d+$/.test(rowData['Ext/Phone#'])) {
83
+ const [name, ext] = rowData['Ext/Phone#'].split('-');
84
+ allStaff.push({ ...rowData, id: `${baseId}-other`, role: 'Other Staff', OtherStaffName: name.trim(), Extension: ext.trim() });
85
+ }
86
+ });
87
+
88
+ processedStaffCache = allStaff;
89
+ console.log('Cache updated. Processed records:', processedStaffCache.length, 'Raw rows:', rawSheetCache.length);
90
+ } catch (error) {
91
+ console.error('Error refreshing spreadsheet cache:', error.message);
92
+ }
93
+ }
94
+
95
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
96
+
97
+ async function generateWithRetry(model, prompt, maxRetries = 3) {
98
+ let attempt = 0;
99
+ while (attempt < maxRetries) {
100
+ try {
101
+ return await model.generateContentStream(prompt);
102
+ } catch (error) {
103
+ if (error.status === 503 && attempt < maxRetries - 1) {
104
+ const delayTime = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
105
+ console.log(`Attempt ${attempt + 1} failed. Retrying in ${Math.round(delayTime / 1000)}s...`);
106
+ await delay(delayTime);
107
+ attempt++;
108
+ } else {
109
+ throw error;
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ app.get('/api/data', (req, res) => {
116
+ if (!processedStaffCache) {
117
+ return res.status(503).json({ message: 'Data is being fetched.' });
118
+ }
119
+ res.json(processedStaffCache);
120
+ });
121
+
122
+ app.get('/api/raw-data', (req, res) => {
123
+ if (!rawSheetCache) {
124
+ return res.status(503).json({ message: 'Data is being fetched.' });
125
+ }
126
+ res.json(rawSheetCache);
127
+ });
128
+
129
+ app.get('/api/ask-luis', async (req, res) => {
130
+ const { question } = req.query;
131
+ if (!question || !processedStaffCache) {
132
+ return res.status(400).send('Bad request.');
133
+ }
134
+ res.setHeader('Content-Type', 'text/event-stream');
135
+ res.setHeader('Cache-Control', 'no-cache');
136
+ res.setHeader('Connection', 'keep-alive');
137
+ res.flushHeaders();
138
+ const prompt = `
139
+ You are "Luis", an AI data analyst for the Hillside Medical Group staff directory.
140
+ Your primary function is to answer questions about staff based ONLY on the JSON data provided.
141
+ **Core Instructions:**
142
+ 1. **Data Schema:** Each object in the JSON array represents ONE person. Their role is in the 'role' key.
143
+ 2. **Name Keys:** Use the correct key for the person's name based on their role: 'Provider' for Providers, 'Team Leads/Manager' for Managers, 'MA' for Medical Assistants, 'VA' for Virtual Assistants, and 'OtherStaffName' for Other Staff.
144
+ 3. **Structured Output:** For lists of people, ALWAYS use a Markdown table.
145
+ 4. **Ambiguity:** If a question is unclear, ask for the specific clinic location.
146
+ **Directory Data:**
147
+ ${JSON.stringify(processedStaffCache)}
148
+ **User's Question:**
149
+ "${question}"
150
+ `;
151
+ try {
152
+ const result = await generateWithRetry(model, prompt);
153
+ for await (const chunk of result.stream) {
154
+ if (chunk.text()) {
155
+ res.write(`data: ${JSON.stringify({ chunk: chunk.text() })}\n\n`);
156
+ }
157
+ }
158
+ } catch (error) {
159
+ const errorMessage = error.status === 503 ? "The service is busy. Please try again." : "I'm having trouble connecting.";
160
+ res.write(`data: ${JSON.stringify({ error: errorMessage })}\n\n`);
161
+ } finally {
162
+ res.write('data: [DONE]\n\n');
163
+ res.end();
164
+ }
165
+ });
166
+
167
+ app.listen(PORT, () => {
168
+ console.log(`Backend server is running on port ${PORT}`);
169
+ updateSheetCache();
170
+ setInterval(updateSheetCache, CACHE_DURATION_MS);
171
+ });