feat: implement Sarah AI sales bot with inbound/outbound support#104
feat: implement Sarah AI sales bot with inbound/outbound support#104ZyosxD wants to merge 1 commit into
Conversation
- Switched from Express to Fastify for better low-RAM performance. - Implemented dual persona (SARAH_INBOUND and SARAH_OUTBOUND) focusing 100% on sales, avoiding "chat", and gathering "The Trifecta". - Added a Smart Drip engine (`dripService.js`) that automatically dials clients in specific MT operating hours using Twilio and Dayjs, locking concurrent calls correctly. - Enhanced OpenAI Realtime API (`openaiRealtime.js`) with structured tools (`schedule_appointment`, `report_interaction`, `end_call`), robust error handling, a 10s delay on hangup, and "coral" voice implementation. - Implemented Nodemailer integration to send automated green/orange outcome reports based on call tools. - Refactored routes and controllers to natively support Fastify and handle correct param extractions. - Updated documentation (README.md) with full Master Specifications. Co-authored-by: ZyosxD <35784430+ZyosxD@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
There was a problem hiding this comment.
Code Review
This pull request transitions the voice bot from Express to Fastify and rebrands the system as 'Sarah,' a specialized AI sales assistant. Key architectural changes include the implementation of a 'Smart Drip' engine for automated outbound calling, persona-specific prompts for inbound and outbound scenarios, and a new email notification service for lead tracking. Feedback highlights critical performance issues related to synchronous file I/O that could cause audio latency, a security vulnerability in the unvalidated status callback endpoint, and opportunities to optimize email transporter management and public URL validation.
| saveData(filename, data) { | ||
| try { | ||
| logger.info(`Initiating transfer for call ${this.callSid}`); | ||
| // Get TwiML for transfer | ||
| const twiml = transferToHuman(this.callSid); | ||
|
|
||
| // Update the call using Twilio REST API | ||
| const client = twilio(config.twilio.accountSid, config.twilio.authToken); | ||
| await client.calls(this.callSid).update({ | ||
| twiml: twiml | ||
| }); | ||
|
|
||
| // Close websocket as the call is being transferred | ||
| if (this.ws && this.ws.readyState === WebSocket.OPEN) { | ||
| this.ws.close(); | ||
| } | ||
| if (this.openaiWs && this.openaiWs.readyState === WebSocket.OPEN) { | ||
| this.openaiWs.close(); | ||
| const filePath = path.join(process.cwd(), 'src', 'data', filename); | ||
| let currentData = []; | ||
| if (fs.existsSync(filePath)) { | ||
| const fileContent = fs.readFileSync(filePath, 'utf8'); | ||
| currentData = JSON.parse(fileContent); | ||
| } | ||
|
|
||
| currentData.push(data); | ||
| fs.writeFileSync(filePath, JSON.stringify(currentData, null, 2)); | ||
| } catch (error) { | ||
| logger.error('Error handling transfer:', error); | ||
| logger.error(`Error saving data to ${filename}:`, error); | ||
| } | ||
| } |
There was a problem hiding this comment.
The saveData method uses synchronous file system operations (fs.readFileSync and fs.writeFileSync). In a Node.js environment handling real-time audio streams, blocking the event loop with synchronous I/O can cause audio jitter and latency. Additionally, this read-modify-write pattern is not atomic and can lead to data corruption if multiple calls attempt to save data simultaneously. Consider using fs.promises and a more robust data persistence strategy.
| const getPendingClient = () => { | ||
| try { | ||
| if (!fs.existsSync(CLIENTS_FILE)) return null; | ||
| const data = fs.readFileSync(CLIENTS_FILE, 'utf8'); | ||
| const clients = JSON.parse(data); | ||
| return clients.find(c => c.status === 'PENDING'); | ||
| } catch (error) { | ||
| logger.error('Error reading clients.json:', error); | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| const markClientCalled = (phoneNumber) => { | ||
| try { | ||
| const data = fs.readFileSync(CLIENTS_FILE, 'utf8'); | ||
| const clients = JSON.parse(data); | ||
| const updatedClients = clients.map(c => { | ||
| if (c.phone === phoneNumber) { | ||
| return { ...c, status: 'CALLED' }; | ||
| } | ||
| return c; | ||
| }); | ||
| fs.writeFileSync(CLIENTS_FILE, JSON.stringify(updatedClients, null, 2)); | ||
| } catch (error) { | ||
| logger.error('Error updating clients.json:', error); | ||
| } | ||
| }; |
There was a problem hiding this comment.
The functions getPendingClient and markClientCalled perform synchronous file I/O. Since the drip engine runs on a frequent interval, these blocking operations will periodically stall the event loop, which can impact the performance of concurrent voice streams. Consider using fs.promises for asynchronous file access.
|
|
||
| export default router; | ||
| // Status Callback for Outbound Calls | ||
| fastify.post('/status', async (request, reply) => { |
There was a problem hiding this comment.
The /status endpoint is missing Twilio request validation. Since this endpoint is responsible for releasing the isCallActive lock in the drip engine, it should be protected to ensure that only legitimate requests from Twilio can trigger state changes.
| fastify.post('/status', async (request, reply) => { | |
| fastify.post('/status', { preHandler: [validateTwilioRequest] }, async (request, reply) => { |
| const createTransporter = () => { | ||
| return nodemailer.createTransport({ | ||
| host: config.email.smtpHost, | ||
| port: config.email.smtpPort, | ||
| secure: false, | ||
| auth: { | ||
| user: config.email.smtpUser, | ||
| pass: config.email.smtpPass | ||
| } | ||
| }); | ||
| }; |
| twilioClient = twilio(config.twilio.accountSid, config.twilio.authToken); | ||
| } | ||
|
|
||
| const publicUrl = config.server.publicUrl || `http://localhost:${config.server.port}`; |
Transform the existing voice bot into "Sarah (1Wire Assistant)," a dual-persona AI bot acting as an inbound receptionist and outbound cold caller, strictly focused on sales for Internet, VoIP, and IT services. Replaced Express with Fastify, added a Smart Drip engine, integrated a mailer for reporting, and hardened the Realtime API implementation.
PR created automatically by Jules for task 3410138576930795993 started by @ZyosxD