Skip to content

feat: implement Sarah AI sales bot with inbound/outbound support#104

Open
ZyosxD wants to merge 1 commit into
voice-bot-projectfrom
feat/sarah-sales-bot-3410138576930795993
Open

feat: implement Sarah AI sales bot with inbound/outbound support#104
ZyosxD wants to merge 1 commit into
voice-bot-projectfrom
feat/sarah-sales-bot-3410138576930795993

Conversation

@ZyosxD
Copy link
Copy Markdown
Owner

@ZyosxD ZyosxD commented Apr 2, 2026

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

- 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>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +258 to 271
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);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Comment on lines +37 to +63
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);
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

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.

Suggested change
fastify.post('/status', async (request, reply) => {
fastify.post('/status', { preHandler: [validateTwilioRequest] }, async (request, reply) => {

Comment on lines +5 to +15
const createTransporter = () => {
return nodemailer.createTransport({
host: config.email.smtpHost,
port: config.email.smtpPort,
secure: false,
auth: {
user: config.email.smtpUser,
pass: config.email.smtpPass
}
});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Creating a new transporter instance for every email is inefficient as it initializes a new connection pool each time. It is recommended to create a single transporter instance at the module level and reuse it across all email sending functions.

twilioClient = twilio(config.twilio.accountSid, config.twilio.authToken);
}

const publicUrl = config.server.publicUrl || `http://localhost:${config.server.port}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Falling back to localhost for the publicUrl will cause outbound calls to fail, as Twilio cannot reach a local address to fetch the TwiML. It is better to validate that config.server.publicUrl is correctly configured before attempting to initiate a call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant