frontendt / server.js
ayushsinghal1510's picture
Added coaching and hotel
252a04f
Raw
History Blame Contribute Delete
10.5 kB
import 'dotenv/config'
import http from 'http'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const PORT = process.env.PORT || 7860
const WEBHOOK_URL = process.env.WEBHOOK_URL || `http://localhost:${PORT}`
const FROM_NUMBER = process.env.FROM_NUMBER || ''
const USER_API_KEY = process.env.USER_API_KEY || ''
const FLOW_API_KEY = process.env.FLOW_API_KEY || ''
// Session-aware SSE subscribers: Map<sessionId, Set<writer>>
const sessions = new Map()
function getSession(id) {
if (!sessions.has(id)) sessions.set(id, new Set())
return sessions.get(id)
}
function removeWriter(id, writer) {
const s = sessions.get(id)
if (!s) return
s.delete(writer)
if (s.size === 0) sessions.delete(id)
}
// Load call agent assets once at startup
const CALL_BASE = process.env.CALL_BASE || path.resolve(__dirname, 'assets')
let workflowTemplate, universityPrompt, realestatePrompt, coachingPrompt, medicalPrompt, hotelPrompt
try {
workflowTemplate = JSON.parse(fs.readFileSync(`${CALL_BASE}/jsons/restraunts/workflow.json`, 'utf8'))
universityPrompt = fs.readFileSync(`${CALL_BASE}/prompts/college/nims_admission_eng.md`, 'utf8')
realestatePrompt = fs.readFileSync(`${CALL_BASE}/prompts/realestate/apartment_sale_eng.md`, 'utf8')
coachingPrompt = fs.readFileSync(`${CALL_BASE}/prompts/coaching/coaching_admission_eng.md`, 'utf8')
medicalPrompt = fs.readFileSync(`${CALL_BASE}/prompts/medical/opd_reception_eng.md`, 'utf8')
hotelPrompt = fs.readFileSync(`${CALL_BASE}/prompts/hotel/concierge_prearrival_eng.md`, 'utf8')
console.log('call agent assets loaded')
} catch (e) {
console.warn('could not load call agent assets:', e.message)
}
// Each agent's prompt + greeting use a {{BRAND}} placeholder. The caller can
// override the brand from the UI; otherwise defaultBrand is substituted in.
const CALL_CONFIG = {
university: {
prompt: universityPrompt,
defaultBrand: 'NIMS University',
greeting: 'Hello! Thank you for calling {{BRAND}}, this is Sneha from the admissions team. May I know your name, and which course you are interested in?',
},
realestate: {
prompt: realestatePrompt,
defaultBrand: 'Skyline Greens',
greeting: 'Hello! Main Riya bol rahi hoon {{BRAND}} ki taraf se — aapne recently ek flat ke liye inquiry ki thi. Kya abhi thodi baat ho sakti hai?',
},
coaching: {
prompt: coachingPrompt,
defaultBrand: 'Bansal Classes',
greeting: 'Namaste! Main Aarohi baat kar rahi hoon {{BRAND}} se — aapne abhi coaching ke liye enquiry ki thi na? Kya do minute baat ho sakti hai?',
},
medical: {
prompt: medicalPrompt,
defaultBrand: 'Sanjeevani Hospital',
greeting: 'Namaste, {{BRAND}} me aapka swagat hai, main Meera baat kar rahi hoon. Aapki kya help kar sakti hoon?',
},
hotel: {
prompt: hotelPrompt,
defaultBrand: 'The Grand Rambagh',
greeting: 'Namaste! Main Kabir baat kar raha hoon {{BRAND}} se — aapki booking confirm hai, bas aapka welcome ready karne ke liye call kiya. Do minute baat ho sakti hai?',
},
}
// Static frontend assets
const distPath = path.resolve(__dirname, 'dist')
let indexHtml = null
try { indexHtml = fs.readFileSync(path.join(distPath, 'index.html'), 'utf8') } catch {}
const MIME = {
'.html': 'text/html',
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.css': 'text/css',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.ico': 'image/x-icon',
'.json': 'application/json',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
}
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', '*')
if (req.method === 'OPTIONS') {
res.writeHead(204)
res.end()
return
}
// Webhook receiver — VX backend POSTs here with session_id in payload
if (req.method === 'POST' && req.url === '/') {
let body = ''
for await (const chunk of req) body += chunk
try {
const data = JSON.parse(body)
const sessionId = data.session_id ?? data.responses?.find(r => r.session_id)?.session_id
const clients = sessionId ? sessions.get(sessionId) : null
console.log(`[webhook] resolved session=${sessionId} subscribers=${clients?.size ?? 0}`)
console.log('[webhook] payload keys:', Object.keys(data), '| responses count:', data.responses?.length ?? 'none')
if (data.responses?.length) {
console.log('[webhook] response nodes:', data.responses.map(r => `node=${r.node} streaming=${r.streaming} hasChunk=${!!r.chunk} hasOut=${!!r.out}`).join(' | '))
}
if (clients?.size) {
for (const write of clients) write(data)
}
} catch (e) {
console.error('[webhook] parse error:', e.message)
console.error('[webhook] raw body snippet:', body.slice(0, 300))
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ ok: true }))
return
}
// SSE stream — browser subscribes by session
if (req.method === 'GET' && req.url.startsWith('/events')) {
const url = new URL(req.url, 'http://localhost')
const sid = url.searchParams.get('session')
if (!sid) { res.writeHead(400); res.end('missing session param'); return }
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'X-Accel-Buffering':'no',
'Connection': 'keep-alive',
})
res.write(': connected\n\n')
const write = data => res.write(`data: ${JSON.stringify(data)}\n\n`)
const heartbeat = setInterval(() => res.write(': heartbeat\n\n'), 25000)
getSession(sid).add(write)
console.log(`[sse] client connected session=${sid} total=${sessions.get(sid)?.size}`)
req.on('close', () => {
removeWriter(sid, write)
clearInterval(heartbeat)
console.log(`[sse] client disconnected session=${sid}`)
})
return
}
// Outbound call proxy — frontend POSTs here to initiate a call agent session
if (req.method === 'POST' && req.url === '/call') {
let body = ''
for await (const chunk of req) body += chunk
try {
const { type, phone, session_id, voiceSettings, brand } = JSON.parse(body)
const cfg = CALL_CONFIG[type]
if (!cfg || !cfg.prompt) {
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: `unknown call type: ${type}` }))
return
}
// Substitute the {{BRAND}} placeholder with the caller-supplied brand,
// falling back to this agent's default.
const brandName = (typeof brand === 'string' && brand.trim()) ? brand.trim() : cfg.defaultBrand
const systemPrompt = cfg.prompt.split('{{BRAND}}').join(brandName)
const greetingText = cfg.greeting.split('{{BRAND}}').join(brandName)
// Deep-clone the workflow template and inject agent-specific fields
const customs = JSON.parse(JSON.stringify(workflowTemplate))
customs.agent_id.workflow.nodes.llm.parameters.system_prompt = systemPrompt
customs.agent_id.workflow.nodes.greeting.parameters.out_dict.speak = greetingText
customs.agent_id['webhook-url'] = WEBHOOK_URL
// Default STT / pre-fire / inactivity for all outbound calls
customs['stt_id'] = { service: 'soniox', language: ['hi', 'en'] }
customs['pre-fire'] = true
customs['pre-fire-config'] = { min: 10, max: 5000, current: 40 }
customs['inactivity'] = true
customs['inactivity-metadata'] = {
'time-period': 1000,
'max-times': 3,
'inactivity-type': 'static',
message: "Are you still there? No rush — take your time and answer whenever you're ready.",
'interruption-type': 'full',
'interruption-metadata': {},
}
if (voiceSettings) {
if (voiceSettings.tts_id) customs.tts_id = voiceSettings.tts_id
if (voiceSettings.stt_id) customs.stt_id = voiceSettings.stt_id
if (voiceSettings['grain-voice'] != null) customs['grain-voice'] = voiceSettings['grain-voice']
if (voiceSettings['grain-level'] != null) customs['grain-level'] = voiceSettings['grain-level']
if (voiceSettings['pre-fire'] != null) customs['pre-fire'] = voiceSettings['pre-fire']
if (voiceSettings['pre-fire-config']) customs['pre-fire-config'] = voiceSettings['pre-fire-config']
if (voiceSettings.inactivity != null) customs.inactivity = voiceSettings.inactivity
if (voiceSettings['inactivity-metadata']) customs['inactivity-metadata'] = voiceSettings['inactivity-metadata']
}
const payload = {
'from-number': FROM_NUMBER,
'to-number': phone,
session_id,
customs,
}
const upstream = await fetch('https://call.voxio.in/plivo/outbound', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
user_api_key: USER_API_KEY,
flow_api_key: FLOW_API_KEY,
},
body: JSON.stringify(payload),
})
const result = await upstream.json()
res.writeHead(upstream.status, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(result))
} catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: e.message }))
}
return
}
// Static files + SPA fallback
if (req.method === 'GET') {
const urlPath = req.url.split('?')[0]
const filePath = path.join(distPath, urlPath)
try {
const stat = fs.statSync(filePath)
if (stat.isFile()) {
const ext = path.extname(filePath).toLowerCase()
const mime = MIME[ext] || 'application/octet-stream'
res.writeHead(200, { 'Content-Type': mime })
fs.createReadStream(filePath).pipe(res)
return
}
} catch {}
if (indexHtml) {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(indexHtml)
return
}
}
res.writeHead(404)
res.end()
})
server.listen(PORT, '0.0.0.0', () =>
console.log(`webhook+sse server on :${PORT}`)
)