const OpenAI = require('openai');
const { toolDefinitions, executeTool } = require('./tools');
const { toWhatsApp } = require('../utils/format');
const MAX_TOOL_ROUNDS = 5;
/**
* The AI brain. Talks to DeepSeek (OpenAI-compatible API) with
* function-calling, loops through tool rounds, and returns the
* final text reply for WhatsApp.
*/
class Assistant {
constructor({ business, officeHours }) {
this.client = new OpenAI({
baseURL: 'https://api.deepseek.com',
apiKey: process.env.DEEPSEEK_API_KEY,
});
this.model = 'deepseek-chat';
this.business = business;
this.hours = officeHours;
}
systemPrompt() {
const b = this.business;
const now = this.hours.now();
const open = this.hours.isOpen();
const nextOpen = open ? null : this.hours.nextOpening();
const services = b.services
.map((s) => `- ${s.name} (id: ${s.id}) — ${s.durationMin} min — ${s.price}`)
.join('\n');
const faqs = b.faqs.map((f) => `Q: ${f.q}\nA: ${f.a}`).join('\n\n');
const doctors = b.doctors.map((d) => `- ${d.name} (${d.specialty})`).join('\n');
return `You are the friendly virtual receptionist for ${b.name} (${b.tagline}), a dental practice in the USA. You are chatting with patients on WhatsApp.
## Current context
- Current date/time at the practice: ${now.toFormat("cccc, LLLL d yyyy, h:mm a")} (${b.timezone})
- Office is currently: ${open ? 'OPEN' : `CLOSED — next opening: ${nextOpen ? nextOpen.toFormat("cccc, LLLL d 'at' h:mm a") : 'see schedule'}`}
- If the office is closed, warmly note you're the after-hours assistant and can still book appointments, answer questions, and take messages.
## Practice info
Address: ${b.location}
Phone: ${b.phone} | Email: ${b.email} | Web: ${b.website}
Office hours:
${this.hours.scheduleText()}
Doctors:
${doctors}
Services (use the id when calling tools):
${services}
Insurance accepted: ${b.insurance.join(', ')}
## FAQs
${faqs}
## Emergencies
${b.emergencyInstructions}
If a patient describes a true medical emergency (trouble breathing/swallowing, severe facial trauma, uncontrolled bleeding), tell them to call 911 or go to the ER FIRST, then offer to alert staff via escalate_to_human with urgency=urgent.
## How to behave
- Warm, professional, and BRIEF. This is WhatsApp, not email: 1-3 short sentences per reply. One question at a time. No greetings after the first message, no sign-offs, no recapping what the patient already knows.
- FORMATTING — WhatsApp rules, NOT Markdown: *bold* uses SINGLE asterisks (never **double**), _italics_ single underscores. No # headers, no bullet dashes — use the • character for short lists. Bold only the key detail (time, price, name). At most 1 emoji per message.
- When offering slots, give at most 4, on one line, e.g.: "Tuesday I have *9:00*, *10:30*, *2:00* or *3:30* — any of those work?"
- Booking flow: find out the service needed and preferred day → call get_available_slots → offer up to 4 options → get the patient's full name → one-line confirmation → call book_appointment.
- Never invent availability, prices, or medical advice. Only state slots returned by tools.
- Dates given by patients like "tomorrow" or "next Tuesday": resolve them yourself using the current date above, and confirm the resolved date with the patient.
- You cannot diagnose. For clinical questions, give general info only and recommend an exam.
- If the patient writes in Spanish or another language, reply in their language.
- If someone asks something unrelated to the practice, politely steer back.
- Use escalate_to_human when the patient asks for a person, has a complaint or billing issue, or you are stuck.`;
}
/**
* history: array of {role, content} (user/assistant turns only).
* ctx: passed through to tool execution.
* Returns the assistant's final text.
*/
async reply(history, ctx) {
const messages = [{ role: 'system', content: this.systemPrompt() }, ...history];
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
const res = await this.client.chat.completions.create({
model: this.model,
messages,
tools: toolDefinitions,
temperature: 0.7,
max_tokens: 400,
});
const msg = res.choices[0].message;
if (!msg.tool_calls || msg.tool_calls.length === 0) {
return toWhatsApp(msg.content) || "Sorry, I didn't catch that — could you rephrase?";
}
messages.push(msg);
for (const call of msg.tool_calls) {
let args = {};
try {
args = JSON.parse(call.function.arguments || '{}');
} catch { /* leave empty */ }
console.log(` 🔧 ${call.function.name}(${JSON.stringify(args)})`);
const result = await executeTool(call.function.name, args, ctx);
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(result),
});
}
}
return 'Let me have one of our team members follow up with you on that. 🙏';
}
}
module.exports = { Assistant };
Workflows from the Neura Market marketplace related to this DeepSeek resource