Spaces:
Sleeping
Sleeping
Commit ·
0c14067
1
Parent(s): bd7e728
Uploaded
Browse files- Dockerfile +20 -0
- google-credentials.json +13 -0
- package.json +18 -0
- 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 |
+
});
|