@diegoaltoworks/talker 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/twilio.d.ts +13 -1
- package/dist/adapters/twilio.d.ts.map +1 -1
- package/dist/adapters/twilio.test.d.ts +7 -0
- package/dist/adapters/twilio.test.d.ts.map +1 -0
- package/dist/core/phrases.d.ts +5 -0
- package/dist/core/phrases.d.ts.map +1 -1
- package/dist/core/processing/prompts.d.ts +1 -1
- package/dist/core/processing/prompts.d.ts.map +1 -1
- package/dist/db/migrate.d.ts +1 -1
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +381 -97
- package/dist/index.mjs +368 -88
- package/dist/plugin.d.ts.map +1 -1
- package/dist/routes/call/handle-answer.test.d.ts +7 -0
- package/dist/routes/call/handle-answer.test.d.ts.map +1 -0
- package/dist/routes/call/handle-initial.test.d.ts +7 -0
- package/dist/routes/call/handle-initial.test.d.ts.map +1 -0
- package/dist/routes/call/handle-nospeech.test.d.ts +7 -0
- package/dist/routes/call/handle-nospeech.test.d.ts.map +1 -0
- package/dist/routes/call/handle-respond.test.d.ts +7 -0
- package/dist/routes/call/handle-respond.test.d.ts.map +1 -0
- package/dist/routes/call/handle-status.test.d.ts +7 -0
- package/dist/routes/call/handle-status.test.d.ts.map +1 -0
- package/dist/routes/call/pending.test.d.ts +7 -0
- package/dist/routes/call/pending.test.d.ts.map +1 -0
- package/dist/routes/sms/handle-incoming.test.d.ts +7 -0
- package/dist/routes/sms/handle-incoming.test.d.ts.map +1 -0
- package/dist/routes/whatsapp/handle-incoming.d.ts +12 -0
- package/dist/routes/whatsapp/handle-incoming.d.ts.map +1 -0
- package/dist/routes/whatsapp/handle-incoming.test.d.ts +7 -0
- package/dist/routes/whatsapp/handle-incoming.test.d.ts.map +1 -0
- package/dist/routes/whatsapp/index.d.ts +14 -0
- package/dist/routes/whatsapp/index.d.ts.map +1 -0
- package/dist/routes/whatsapp/processor.d.ts +14 -0
- package/dist/routes/whatsapp/processor.d.ts.map +1 -0
- package/dist/standalone.d.ts.map +1 -1
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/language/de.json +7 -0
- package/language/en.json +7 -0
- package/language/es.json +7 -0
- package/language/fr.json +7 -0
- package/language/nl.json +7 -0
- package/language/pt.json +7 -0
- package/package.json +1 -1
- package/prompts/outgoing.md +16 -1
package/dist/index.js
CHANGED
|
@@ -84,6 +84,7 @@ __export(index_exports, {
|
|
|
84
84
|
getPhrase: () => getPhrase,
|
|
85
85
|
getSmsPhrase: () => getSmsPhrase,
|
|
86
86
|
getVoiceConfig: () => getVoiceConfig,
|
|
87
|
+
getWhatsAppPhrase: () => getWhatsAppPhrase,
|
|
87
88
|
incrementNoSpeechRetries: () => incrementNoSpeechRetries,
|
|
88
89
|
initDbClient: () => initDbClient,
|
|
89
90
|
inputSanitizeMiddleware: () => inputSanitizeMiddleware,
|
|
@@ -102,6 +103,7 @@ __export(index_exports, {
|
|
|
102
103
|
runMigrations: () => runMigrations,
|
|
103
104
|
sayTwiml: () => sayTwiml,
|
|
104
105
|
sendSMS: () => sendSMS,
|
|
106
|
+
sendWhatsApp: () => sendWhatsApp,
|
|
105
107
|
setActiveFlow: () => setActiveFlow,
|
|
106
108
|
setDetectedLanguage: () => setDetectedLanguage,
|
|
107
109
|
setLastPrompt: () => setLastPrompt,
|
|
@@ -109,10 +111,12 @@ __export(index_exports, {
|
|
|
109
111
|
smsRoutes: () => smsRoutes,
|
|
110
112
|
startCleanup: () => startCleanup,
|
|
111
113
|
stopCleanup: () => stopCleanup,
|
|
114
|
+
stripWhatsAppPrefix: () => stripWhatsAppPrefix,
|
|
112
115
|
transferTwiml: () => transferTwiml,
|
|
113
116
|
twilioSignatureMiddleware: () => twilioSignatureMiddleware,
|
|
114
117
|
updateFlowParams: () => updateFlowParams,
|
|
115
|
-
validateTwilioSignature: () => validateTwilioSignature
|
|
118
|
+
validateTwilioSignature: () => validateTwilioSignature,
|
|
119
|
+
whatsappRoutes: () => whatsappRoutes
|
|
116
120
|
});
|
|
117
121
|
module.exports = __toCommonJS(index_exports);
|
|
118
122
|
|
|
@@ -306,8 +310,35 @@ async function closeDbClient() {
|
|
|
306
310
|
}
|
|
307
311
|
|
|
308
312
|
// src/db/migrate.ts
|
|
309
|
-
var
|
|
310
|
-
|
|
313
|
+
var SCHEMA_STATEMENTS = [
|
|
314
|
+
`CREATE TABLE IF NOT EXISTS talker_sessions (
|
|
315
|
+
id TEXT PRIMARY KEY,
|
|
316
|
+
phone_number TEXT NOT NULL,
|
|
317
|
+
channel TEXT NOT NULL CHECK(channel IN ('call', 'sms')),
|
|
318
|
+
reason TEXT NOT NULL CHECK(reason IN ('ended', 'redirected')),
|
|
319
|
+
language TEXT NOT NULL,
|
|
320
|
+
started_at INTEGER NOT NULL,
|
|
321
|
+
ended_at INTEGER NOT NULL,
|
|
322
|
+
duration_ms INTEGER NOT NULL,
|
|
323
|
+
transfer_reason TEXT,
|
|
324
|
+
conversation_id TEXT,
|
|
325
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
326
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
327
|
+
)`,
|
|
328
|
+
`CREATE TABLE IF NOT EXISTS talker_messages (
|
|
329
|
+
id TEXT PRIMARY KEY,
|
|
330
|
+
session_id TEXT NOT NULL,
|
|
331
|
+
role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
|
|
332
|
+
content TEXT NOT NULL,
|
|
333
|
+
timestamp INTEGER NOT NULL,
|
|
334
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
335
|
+
FOREIGN KEY (session_id) REFERENCES talker_sessions(id) ON DELETE CASCADE
|
|
336
|
+
)`,
|
|
337
|
+
"CREATE INDEX IF NOT EXISTS idx_talker_sessions_phone ON talker_sessions(phone_number)",
|
|
338
|
+
"CREATE INDEX IF NOT EXISTS idx_talker_sessions_created ON talker_sessions(created_at DESC)",
|
|
339
|
+
"CREATE INDEX IF NOT EXISTS idx_talker_messages_session ON talker_messages(session_id)",
|
|
340
|
+
"CREATE INDEX IF NOT EXISTS idx_talker_messages_ts ON talker_messages(timestamp)"
|
|
341
|
+
];
|
|
311
342
|
async function runMigrations() {
|
|
312
343
|
const client2 = getDbClient();
|
|
313
344
|
if (!client2) {
|
|
@@ -315,11 +346,8 @@ async function runMigrations() {
|
|
|
315
346
|
return;
|
|
316
347
|
}
|
|
317
348
|
try {
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
const statements = schema.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
321
|
-
logger.info("running database migrations", { statementCount: statements.length });
|
|
322
|
-
for (const statement of statements) {
|
|
349
|
+
logger.info("running database migrations", { statementCount: SCHEMA_STATEMENTS.length });
|
|
350
|
+
for (const statement of SCHEMA_STATEMENTS) {
|
|
323
351
|
await client2.execute(statement);
|
|
324
352
|
}
|
|
325
353
|
logger.info("database migrations completed");
|
|
@@ -332,7 +360,7 @@ async function runMigrations() {
|
|
|
332
360
|
}
|
|
333
361
|
|
|
334
362
|
// src/flows/registry.ts
|
|
335
|
-
var
|
|
363
|
+
var import_node_fs2 = require("node:fs");
|
|
336
364
|
|
|
337
365
|
// src/flows/intent.ts
|
|
338
366
|
var OPENAI_API_URL = "https://api.openai.com/v1/chat/completions";
|
|
@@ -421,15 +449,15 @@ Rules:
|
|
|
421
449
|
}
|
|
422
450
|
|
|
423
451
|
// src/flows/loader.ts
|
|
424
|
-
var
|
|
425
|
-
var
|
|
452
|
+
var import_node_fs = require("node:fs");
|
|
453
|
+
var import_node_path = require("node:path");
|
|
426
454
|
async function loadFlowsFromDirectory(flowsDir) {
|
|
427
455
|
const flows = /* @__PURE__ */ new Map();
|
|
428
|
-
if (!(0,
|
|
456
|
+
if (!(0, import_node_fs.existsSync)(flowsDir)) {
|
|
429
457
|
logger.warn("flows directory does not exist", { flowsDir });
|
|
430
458
|
return flows;
|
|
431
459
|
}
|
|
432
|
-
const flowDirs = (0,
|
|
460
|
+
const flowDirs = (0, import_node_fs.readdirSync)(flowsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).filter((dirent) => ["lib", "tests", "registry"].indexOf(dirent.name) === -1).map((dirent) => dirent.name);
|
|
433
461
|
logger.info("loading flows", { count: flowDirs.length, flowDirs });
|
|
434
462
|
for (const flowName of flowDirs) {
|
|
435
463
|
try {
|
|
@@ -447,20 +475,20 @@ async function loadFlowsFromDirectory(flowsDir) {
|
|
|
447
475
|
return flows;
|
|
448
476
|
}
|
|
449
477
|
async function loadFlow(flowsDir, flowName) {
|
|
450
|
-
const flowPath = (0,
|
|
451
|
-
const definitionPath = (0,
|
|
452
|
-
const instructionsPath = (0,
|
|
453
|
-
const handlerPath = (0,
|
|
454
|
-
if (!(0,
|
|
478
|
+
const flowPath = (0, import_node_path.join)(flowsDir, flowName);
|
|
479
|
+
const definitionPath = (0, import_node_path.join)(flowPath, "flow.json");
|
|
480
|
+
const instructionsPath = (0, import_node_path.join)(flowPath, "instructions.md");
|
|
481
|
+
const handlerPath = (0, import_node_path.join)(flowPath, "handler.ts");
|
|
482
|
+
if (!(0, import_node_fs.existsSync)(definitionPath)) {
|
|
455
483
|
throw new Error(`flow.json not found for ${flowName}`);
|
|
456
484
|
}
|
|
457
|
-
if (!(0,
|
|
485
|
+
if (!(0, import_node_fs.existsSync)(instructionsPath)) {
|
|
458
486
|
throw new Error(`instructions.md not found for ${flowName}`);
|
|
459
487
|
}
|
|
460
|
-
if (!(0,
|
|
488
|
+
if (!(0, import_node_fs.existsSync)(handlerPath)) {
|
|
461
489
|
throw new Error(`handler.ts not found for ${flowName}`);
|
|
462
490
|
}
|
|
463
|
-
const definitionContent = (0,
|
|
491
|
+
const definitionContent = (0, import_node_fs.readFileSync)(definitionPath, "utf-8");
|
|
464
492
|
const definition = JSON.parse(definitionContent);
|
|
465
493
|
if (definition.id !== flowName) {
|
|
466
494
|
throw new Error(`Flow id mismatch: ${definition.id} !== ${flowName}`);
|
|
@@ -476,9 +504,9 @@ async function loadFlow(flowsDir, flowName) {
|
|
|
476
504
|
if (typeof handler !== "function") {
|
|
477
505
|
throw new Error(`Flow ${flowName} handler.ts must export an 'execute' function`);
|
|
478
506
|
}
|
|
479
|
-
const prefillPath = (0,
|
|
507
|
+
const prefillPath = (0, import_node_path.join)(flowPath, "prefill.ts");
|
|
480
508
|
let prefill;
|
|
481
|
-
if ((0,
|
|
509
|
+
if ((0, import_node_fs.existsSync)(prefillPath)) {
|
|
482
510
|
const prefillModule = await import(prefillPath);
|
|
483
511
|
prefill = prefillModule.prefillFromContext;
|
|
484
512
|
if (typeof prefill !== "function") {
|
|
@@ -562,7 +590,7 @@ var FlowRegistry = class {
|
|
|
562
590
|
if (!flow) {
|
|
563
591
|
throw new Error(`Flow ${flowName} not found`);
|
|
564
592
|
}
|
|
565
|
-
return (0,
|
|
593
|
+
return (0, import_node_fs2.readFileSync)(flow.instructionsPath, "utf-8");
|
|
566
594
|
}
|
|
567
595
|
};
|
|
568
596
|
function testModeDetectIntent(message) {
|
|
@@ -722,20 +750,69 @@ function getErrorMessage(error) {
|
|
|
722
750
|
}
|
|
723
751
|
|
|
724
752
|
// src/core/phrases.ts
|
|
725
|
-
var
|
|
726
|
-
var
|
|
753
|
+
var import_node_fs3 = require("node:fs");
|
|
754
|
+
var import_node_path2 = require("node:path");
|
|
727
755
|
var phrasesCache = {};
|
|
756
|
+
var ENGLISH_FALLBACK = {
|
|
757
|
+
greeting: "Hello! I'm your voice assistant. How can I help you today?",
|
|
758
|
+
didNotCatch: "I didn't catch that. Could you please repeat?",
|
|
759
|
+
didNotHear: "I didn't hear anything. Goodbye.",
|
|
760
|
+
didNotHearRetry: "Sorry, I didn't catch that. Could you try again?",
|
|
761
|
+
didNotHearFinal: "I'm really sorry but I cannot hear what you're saying. Please call again. Bye for now.",
|
|
762
|
+
transfer: "Let me connect you with someone directly.",
|
|
763
|
+
acknowledgment: "One moment please...",
|
|
764
|
+
farewell: {
|
|
765
|
+
morning: "You're welcome! Have a wonderful day. Goodbye!",
|
|
766
|
+
afternoon: "You're welcome! Have a lovely afternoon. Goodbye!",
|
|
767
|
+
evening: "You're welcome! Have a good evening. Goodbye!"
|
|
768
|
+
},
|
|
769
|
+
error: "Sorry, I encountered an error. Please try again later. Goodbye.",
|
|
770
|
+
timeout: "Sorry, I took too long to respond. Please try again. Goodbye.",
|
|
771
|
+
lostQuestion: "I'm sorry, I lost track of your question. Could you please repeat?",
|
|
772
|
+
flow: {
|
|
773
|
+
cancelled: "No problem! I've cancelled that. What else would you like to know?"
|
|
774
|
+
},
|
|
775
|
+
sms: {
|
|
776
|
+
greeting: "Hi! I'm your voice assistant. Ask me anything!",
|
|
777
|
+
greetingShort: "Hi! Ask me anything!",
|
|
778
|
+
callForHelp: "For more complex questions, feel free to call back or reach us directly.",
|
|
779
|
+
processingError: "I'm having trouble processing that. Please try texting again or call back.",
|
|
780
|
+
genericError: "Sorry, something went wrong. Please try again."
|
|
781
|
+
},
|
|
782
|
+
whatsapp: {
|
|
783
|
+
greeting: "Hi! I'm your assistant. Send me a message and I'll help you out!",
|
|
784
|
+
greetingShort: "Hi! How can I help?",
|
|
785
|
+
callForHelp: "For more complex questions, feel free to call us directly or reply here with more details.",
|
|
786
|
+
processingError: "I'm having trouble processing that. Please try sending your message again.",
|
|
787
|
+
genericError: "Sorry, something went wrong. Please try again."
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
function resolveBuiltinLanguageDir() {
|
|
791
|
+
const candidates = [
|
|
792
|
+
(0, import_node_path2.join)(__dirname, "../../language"),
|
|
793
|
+
// source: src/core/ -> language/
|
|
794
|
+
(0, import_node_path2.join)(__dirname, "../language")
|
|
795
|
+
// dist: dist/ -> language/
|
|
796
|
+
];
|
|
797
|
+
for (const dir of candidates) {
|
|
798
|
+
if ((0, import_node_fs3.existsSync)(dir)) {
|
|
799
|
+
return dir;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return void 0;
|
|
803
|
+
}
|
|
728
804
|
function loadPhrases(language, languageDir) {
|
|
729
805
|
const cacheKey = `${languageDir || "default"}:${language}`;
|
|
730
806
|
if (phrasesCache[cacheKey]) {
|
|
731
807
|
return phrasesCache[cacheKey];
|
|
732
808
|
}
|
|
733
|
-
const
|
|
809
|
+
const builtinDir = resolveBuiltinLanguageDir();
|
|
810
|
+
const dirs = [languageDir, builtinDir].filter(Boolean);
|
|
734
811
|
for (const dir of dirs) {
|
|
735
|
-
const filePath = (0,
|
|
736
|
-
if ((0,
|
|
812
|
+
const filePath = (0, import_node_path2.join)(dir, `${language}.json`);
|
|
813
|
+
if ((0, import_node_fs3.existsSync)(filePath)) {
|
|
737
814
|
try {
|
|
738
|
-
const content = (0,
|
|
815
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
739
816
|
phrasesCache[cacheKey] = JSON.parse(content);
|
|
740
817
|
return phrasesCache[cacheKey];
|
|
741
818
|
} catch {
|
|
@@ -745,7 +822,8 @@ function loadPhrases(language, languageDir) {
|
|
|
745
822
|
if (language !== "en") {
|
|
746
823
|
return loadPhrases("en", languageDir);
|
|
747
824
|
}
|
|
748
|
-
|
|
825
|
+
phrasesCache[cacheKey] = ENGLISH_FALLBACK;
|
|
826
|
+
return ENGLISH_FALLBACK;
|
|
749
827
|
}
|
|
750
828
|
function getPhrase(language, key, languageDir) {
|
|
751
829
|
const phrases = loadPhrases(language, languageDir);
|
|
@@ -770,6 +848,13 @@ function getSmsPhrase(language, key, languageDir) {
|
|
|
770
848
|
const phrases = loadPhrases(language, languageDir);
|
|
771
849
|
return phrases.sms[key];
|
|
772
850
|
}
|
|
851
|
+
function getWhatsAppPhrase(language, key, languageDir) {
|
|
852
|
+
const phrases = loadPhrases(language, languageDir);
|
|
853
|
+
if (phrases.whatsapp) {
|
|
854
|
+
return phrases.whatsapp[key];
|
|
855
|
+
}
|
|
856
|
+
return phrases.sms[key];
|
|
857
|
+
}
|
|
773
858
|
|
|
774
859
|
// src/core/voice.ts
|
|
775
860
|
var DEFAULT_VOICES = {
|
|
@@ -1231,40 +1316,116 @@ async function callOpenAI(deps, systemPrompt, userMessage, context) {
|
|
|
1231
1316
|
}
|
|
1232
1317
|
|
|
1233
1318
|
// src/core/processing/prompts.ts
|
|
1234
|
-
var
|
|
1235
|
-
var
|
|
1319
|
+
var import_node_fs4 = require("node:fs");
|
|
1320
|
+
var import_node_path3 = require("node:path");
|
|
1236
1321
|
var incomingPrompt = null;
|
|
1237
1322
|
var outgoingPrompt = null;
|
|
1238
1323
|
function loadPromptFile(filename, customPath) {
|
|
1239
|
-
if (customPath && (0,
|
|
1240
|
-
return (0,
|
|
1241
|
-
}
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1324
|
+
if (customPath && (0, import_node_fs4.existsSync)(customPath)) {
|
|
1325
|
+
return (0, import_node_fs4.readFileSync)(customPath, "utf-8");
|
|
1326
|
+
}
|
|
1327
|
+
const candidates = [
|
|
1328
|
+
(0, import_node_path3.join)(__dirname, "../../../prompts", filename),
|
|
1329
|
+
// source: src/core/processing/ -> prompts/
|
|
1330
|
+
(0, import_node_path3.join)(__dirname, "../../prompts", filename),
|
|
1331
|
+
// dist: dist/ -> prompts/
|
|
1332
|
+
(0, import_node_path3.join)(__dirname, "../prompts", filename)
|
|
1333
|
+
// flat dist
|
|
1334
|
+
];
|
|
1335
|
+
for (const path of candidates) {
|
|
1336
|
+
if ((0, import_node_fs4.existsSync)(path)) {
|
|
1337
|
+
return (0, import_node_fs4.readFileSync)(path, "utf-8");
|
|
1338
|
+
}
|
|
1245
1339
|
}
|
|
1246
|
-
|
|
1340
|
+
return void 0;
|
|
1247
1341
|
}
|
|
1342
|
+
var DEFAULT_INCOMING_PROMPT = `# Incoming Message Processor
|
|
1343
|
+
|
|
1344
|
+
You are a pre-processor for a voice assistant. Your job is to analyze incoming caller speech and determine three things:
|
|
1345
|
+
|
|
1346
|
+
1. Should the caller be transferred to a human directly?
|
|
1347
|
+
2. What language is the caller speaking?
|
|
1348
|
+
3. What is the cleaned-up version of their message to send to the knowledge base?
|
|
1349
|
+
|
|
1350
|
+
## IMPORTANT: Conversation Context
|
|
1351
|
+
|
|
1352
|
+
You may receive conversation history along with the current message. **You MUST consider the context** when making decisions.
|
|
1353
|
+
|
|
1354
|
+
## Language Detection
|
|
1355
|
+
|
|
1356
|
+
Detect the language the caller is speaking. Supported: "en", "fr", "nl", "de", "es", "pt". Default: "en".
|
|
1357
|
+
|
|
1358
|
+
## Transfer Detection
|
|
1359
|
+
|
|
1360
|
+
Transfer if the caller expresses: "speak to someone", "talk to a person", "connect me", "real person", "human", frustration signals, or complex/personal matters.
|
|
1361
|
+
|
|
1362
|
+
## End Call Detection
|
|
1363
|
+
|
|
1364
|
+
End call if: "no thanks", "that's all", "goodbye", "bye", "I'm done", "nothing else". Only if clearly ending the conversation.
|
|
1365
|
+
|
|
1366
|
+
## Response Format
|
|
1367
|
+
|
|
1368
|
+
Respond with valid JSON:
|
|
1369
|
+
\`\`\`json
|
|
1370
|
+
{
|
|
1371
|
+
"shouldTransfer": true or false,
|
|
1372
|
+
"shouldEndCall": true or false,
|
|
1373
|
+
"detectedLanguage": "en" or "fr" or "nl" or "de" or "es" or "pt",
|
|
1374
|
+
"processedMessage": "the cleaned up message"
|
|
1375
|
+
}
|
|
1376
|
+
\`\`\`
|
|
1377
|
+
|
|
1378
|
+
## Message Cleaning Rules
|
|
1379
|
+
|
|
1380
|
+
- Fix obvious speech-to-text errors
|
|
1381
|
+
- Remove filler words (um, uh, like, you know)
|
|
1382
|
+
- Keep the core intent and original language intact`;
|
|
1383
|
+
var DEFAULT_OUTGOING_PROMPT = `# Outgoing Response Processor
|
|
1384
|
+
|
|
1385
|
+
You are a post-processor for a voice assistant. Transform knowledge base responses into channel-appropriate messages.
|
|
1386
|
+
|
|
1387
|
+
## Channel Type
|
|
1388
|
+
|
|
1389
|
+
You will be told the channel: "call" (phone), "sms" (text message), or "whatsapp".
|
|
1390
|
+
|
|
1391
|
+
**For CALL:** Spoken aloud by TTS. Convert numbers to words. Remove URLs. Max 2 sentences.
|
|
1392
|
+
**For SMS:** Read on phone screen. Keep digits. Max 160 chars. No markdown.
|
|
1393
|
+
**For WHATSAPP:** Read in chat. Keep digits and URLs. Can use *bold*, _italic_. Up to 500 chars.
|
|
1394
|
+
|
|
1395
|
+
## Language Requirement
|
|
1396
|
+
|
|
1397
|
+
You MUST respond in the specified language.
|
|
1398
|
+
|
|
1399
|
+
## Rules
|
|
1400
|
+
|
|
1401
|
+
1. Be concise - answer directly
|
|
1402
|
+
2. End with a follow-up question
|
|
1403
|
+
3. Always use third person
|
|
1404
|
+
4. Remove markdown, lists, technical jargon
|
|
1405
|
+
|
|
1406
|
+
Return ONLY the transformed text. No JSON, no explanations.`;
|
|
1248
1407
|
function getIncomingPrompt(deps) {
|
|
1249
1408
|
if (!incomingPrompt) {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
incomingPrompt =
|
|
1409
|
+
const loaded = loadPromptFile("incoming.md", deps.config.processing?.incomingPromptPath);
|
|
1410
|
+
if (loaded) {
|
|
1411
|
+
incomingPrompt = loaded;
|
|
1412
|
+
logger.info("incoming prompt loaded from file");
|
|
1413
|
+
} else {
|
|
1414
|
+
incomingPrompt = DEFAULT_INCOMING_PROMPT;
|
|
1415
|
+
logger.info("incoming prompt loaded (built-in default)");
|
|
1256
1416
|
}
|
|
1257
1417
|
}
|
|
1258
1418
|
return incomingPrompt;
|
|
1259
1419
|
}
|
|
1260
1420
|
function getOutgoingPrompt(deps) {
|
|
1261
1421
|
if (!outgoingPrompt) {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
outgoingPrompt =
|
|
1422
|
+
const loaded = loadPromptFile("outgoing.md", deps.config.processing?.outgoingPromptPath);
|
|
1423
|
+
if (loaded) {
|
|
1424
|
+
outgoingPrompt = loaded;
|
|
1425
|
+
logger.info("outgoing prompt loaded from file");
|
|
1426
|
+
} else {
|
|
1427
|
+
outgoingPrompt = DEFAULT_OUTGOING_PROMPT;
|
|
1428
|
+
logger.info("outgoing prompt loaded (built-in default)");
|
|
1268
1429
|
}
|
|
1269
1430
|
}
|
|
1270
1431
|
return outgoingPrompt;
|
|
@@ -1358,10 +1519,10 @@ Respond in: ${language}`;
|
|
|
1358
1519
|
}
|
|
1359
1520
|
|
|
1360
1521
|
// src/flows/params.ts
|
|
1361
|
-
var
|
|
1522
|
+
var import_node_fs5 = require("node:fs");
|
|
1362
1523
|
var OPENAI_API_URL3 = "https://api.openai.com/v1/chat/completions";
|
|
1363
1524
|
async function extractParameters(deps, flow, phoneNumber, userMessage, existingParams) {
|
|
1364
|
-
const instructions = (0,
|
|
1525
|
+
const instructions = (0, import_node_fs5.readFileSync)(flow.instructionsPath, "utf-8");
|
|
1365
1526
|
const schema = flow.definition.schema;
|
|
1366
1527
|
const properties = schema.properties || {};
|
|
1367
1528
|
const required = schema.required || [];
|
|
@@ -1825,6 +1986,163 @@ function smsRoutes(deps, registry) {
|
|
|
1825
1986
|
return app;
|
|
1826
1987
|
}
|
|
1827
1988
|
|
|
1989
|
+
// src/routes/whatsapp/index.ts
|
|
1990
|
+
var import_hono3 = require("hono");
|
|
1991
|
+
|
|
1992
|
+
// src/adapters/twilio.ts
|
|
1993
|
+
function stripWhatsAppPrefix(phoneNumber) {
|
|
1994
|
+
return phoneNumber.replace(/^whatsapp:/, "");
|
|
1995
|
+
}
|
|
1996
|
+
async function sendSMS(config, to, message) {
|
|
1997
|
+
if (!config.accountSid || !config.authToken || !config.phoneNumber) {
|
|
1998
|
+
logger.warn("Twilio credentials not configured, skipping SMS send", { to });
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
try {
|
|
2002
|
+
const auth = Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64");
|
|
2003
|
+
const response = await fetch(
|
|
2004
|
+
`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
2005
|
+
{
|
|
2006
|
+
method: "POST",
|
|
2007
|
+
headers: {
|
|
2008
|
+
Authorization: `Basic ${auth}`,
|
|
2009
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2010
|
+
},
|
|
2011
|
+
body: new URLSearchParams({
|
|
2012
|
+
From: config.phoneNumber,
|
|
2013
|
+
To: to,
|
|
2014
|
+
Body: message
|
|
2015
|
+
})
|
|
2016
|
+
}
|
|
2017
|
+
);
|
|
2018
|
+
if (!response.ok) {
|
|
2019
|
+
const error = await response.text();
|
|
2020
|
+
logger.error("SMS send failed", { to, status: response.status, error });
|
|
2021
|
+
return false;
|
|
2022
|
+
}
|
|
2023
|
+
const data = await response.json();
|
|
2024
|
+
logger.info("SMS sent successfully", { to, messageSid: data.sid });
|
|
2025
|
+
return true;
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
logger.error("SMS send error", {
|
|
2028
|
+
to,
|
|
2029
|
+
error: error instanceof Error ? error.message : "Unknown"
|
|
2030
|
+
});
|
|
2031
|
+
return false;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
async function sendWhatsApp(config, to, message) {
|
|
2035
|
+
if (!config.accountSid || !config.authToken || !config.phoneNumber) {
|
|
2036
|
+
logger.warn("Twilio credentials not configured, skipping WhatsApp send", { to });
|
|
2037
|
+
return false;
|
|
2038
|
+
}
|
|
2039
|
+
try {
|
|
2040
|
+
const auth = Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64");
|
|
2041
|
+
const whatsappTo = to.startsWith("whatsapp:") ? to : `whatsapp:${to}`;
|
|
2042
|
+
const whatsappFrom = config.phoneNumber.startsWith("whatsapp:") ? config.phoneNumber : `whatsapp:${config.phoneNumber}`;
|
|
2043
|
+
const response = await fetch(
|
|
2044
|
+
`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
2045
|
+
{
|
|
2046
|
+
method: "POST",
|
|
2047
|
+
headers: {
|
|
2048
|
+
Authorization: `Basic ${auth}`,
|
|
2049
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2050
|
+
},
|
|
2051
|
+
body: new URLSearchParams({
|
|
2052
|
+
From: whatsappFrom,
|
|
2053
|
+
To: whatsappTo,
|
|
2054
|
+
Body: message
|
|
2055
|
+
})
|
|
2056
|
+
}
|
|
2057
|
+
);
|
|
2058
|
+
if (!response.ok) {
|
|
2059
|
+
const error = await response.text();
|
|
2060
|
+
logger.error("WhatsApp send failed", { to, status: response.status, error });
|
|
2061
|
+
return false;
|
|
2062
|
+
}
|
|
2063
|
+
const data = await response.json();
|
|
2064
|
+
logger.info("WhatsApp message sent successfully", { to, messageSid: data.sid });
|
|
2065
|
+
return true;
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
logger.error("WhatsApp send error", {
|
|
2068
|
+
to,
|
|
2069
|
+
error: error instanceof Error ? error.message : "Unknown"
|
|
2070
|
+
});
|
|
2071
|
+
return false;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// src/routes/whatsapp/processor.ts
|
|
2076
|
+
async function processWhatsApp(deps, registry, phoneNumber, messageBody) {
|
|
2077
|
+
const incoming = await processIncoming(deps, phoneNumber, messageBody, "whatsapp");
|
|
2078
|
+
if (incoming.shouldTransfer) {
|
|
2079
|
+
const message = getWhatsAppPhrase(
|
|
2080
|
+
incoming.detectedLanguage,
|
|
2081
|
+
"callForHelp",
|
|
2082
|
+
deps.config.languageDir
|
|
2083
|
+
);
|
|
2084
|
+
return messageTwiml(message);
|
|
2085
|
+
}
|
|
2086
|
+
const flowResult = await processFlow(
|
|
2087
|
+
deps,
|
|
2088
|
+
registry,
|
|
2089
|
+
phoneNumber,
|
|
2090
|
+
incoming.processedMessage,
|
|
2091
|
+
"whatsapp"
|
|
2092
|
+
);
|
|
2093
|
+
if (flowResult.isFlowActive || flowResult.flowCompleted) {
|
|
2094
|
+
if (flowResult.flowCompleted && flowResult.flowSuccess === false) {
|
|
2095
|
+
const message = getWhatsAppPhrase(
|
|
2096
|
+
incoming.detectedLanguage,
|
|
2097
|
+
"processingError",
|
|
2098
|
+
deps.config.languageDir
|
|
2099
|
+
);
|
|
2100
|
+
return messageTwiml(message);
|
|
2101
|
+
}
|
|
2102
|
+
return messageTwiml(flowResult.response);
|
|
2103
|
+
}
|
|
2104
|
+
const botResponse = await chat(deps, phoneNumber, incoming.processedMessage);
|
|
2105
|
+
const whatsappResponse = await processOutgoing(deps, phoneNumber, botResponse, "whatsapp");
|
|
2106
|
+
return messageTwiml(whatsappResponse);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// src/routes/whatsapp/handle-incoming.ts
|
|
2110
|
+
async function handleIncomingWhatsApp(c, deps, registry) {
|
|
2111
|
+
const body = await c.req.parseBody();
|
|
2112
|
+
const rawFrom = (body.From || "unknown").trim();
|
|
2113
|
+
const phoneNumber = stripWhatsAppPrefix(rawFrom);
|
|
2114
|
+
const messageBody = body.Body || "";
|
|
2115
|
+
logger.info("whatsapp message received", { phoneNumber, messageBody });
|
|
2116
|
+
if (!messageBody.trim()) {
|
|
2117
|
+
return c.text(messageTwiml(getWhatsAppPhrase("en", "greeting", deps.config.languageDir)), 200, {
|
|
2118
|
+
"Content-Type": "text/xml"
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
try {
|
|
2122
|
+
const twiml = await processWhatsApp(deps, registry, phoneNumber, messageBody);
|
|
2123
|
+
persistSession(phoneNumber, "whatsapp");
|
|
2124
|
+
return c.text(twiml, 200, { "Content-Type": "text/xml" });
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
logger.error("whatsapp processing error", {
|
|
2127
|
+
phoneNumber,
|
|
2128
|
+
error: getErrorMessage(error)
|
|
2129
|
+
});
|
|
2130
|
+
return c.text(
|
|
2131
|
+
messageTwiml(getWhatsAppPhrase("en", "genericError", deps.config.languageDir)),
|
|
2132
|
+
200,
|
|
2133
|
+
{ "Content-Type": "text/xml" }
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// src/routes/whatsapp/index.ts
|
|
2139
|
+
function whatsappRoutes(deps, registry) {
|
|
2140
|
+
const app = new import_hono3.Hono();
|
|
2141
|
+
app.post("/whatsapp", (c) => handleIncomingWhatsApp(c, deps, registry));
|
|
2142
|
+
app.get("/whatsapp", (c) => c.text("WhatsApp endpoint active"));
|
|
2143
|
+
return app;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
1828
2146
|
// src/plugin.ts
|
|
1829
2147
|
var DEFAULT_CONTEXT_TTL_MS = 30 * 60 * 1e3;
|
|
1830
2148
|
var DEFAULT_CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -1858,6 +2176,7 @@ async function createTelephonyRoutes(app, chatterDeps, config) {
|
|
|
1858
2176
|
const prefix = config.routePrefix || "";
|
|
1859
2177
|
app.route(prefix, callRoutes(deps, registry));
|
|
1860
2178
|
app.route(prefix, smsRoutes(deps, registry));
|
|
2179
|
+
app.route(prefix, whatsappRoutes(deps, registry));
|
|
1861
2180
|
logger.info("telephony routes mounted", {
|
|
1862
2181
|
prefix: prefix || "/",
|
|
1863
2182
|
hasFlows: !!config.flowsDir,
|
|
@@ -1867,7 +2186,7 @@ async function createTelephonyRoutes(app, chatterDeps, config) {
|
|
|
1867
2186
|
}
|
|
1868
2187
|
|
|
1869
2188
|
// src/standalone.ts
|
|
1870
|
-
var
|
|
2189
|
+
var import_hono4 = require("hono");
|
|
1871
2190
|
var DEFAULT_CONTEXT_TTL_MS2 = 30 * 60 * 1e3;
|
|
1872
2191
|
var DEFAULT_CLEANUP_INTERVAL_MS2 = 5 * 60 * 1e3;
|
|
1873
2192
|
var DEFAULT_MODEL2 = "gpt-4o-mini";
|
|
@@ -1897,11 +2216,12 @@ async function createStandaloneServer(config) {
|
|
|
1897
2216
|
config.contextTtlMs ?? DEFAULT_CONTEXT_TTL_MS2,
|
|
1898
2217
|
config.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS2
|
|
1899
2218
|
);
|
|
1900
|
-
const app = new
|
|
2219
|
+
const app = new import_hono4.Hono();
|
|
1901
2220
|
app.get("/healthz", (c) => c.text("ok"));
|
|
1902
2221
|
const prefix = config.routePrefix || "";
|
|
1903
2222
|
app.route(prefix, callRoutes(deps, registry));
|
|
1904
2223
|
app.route(prefix, smsRoutes(deps, registry));
|
|
2224
|
+
app.route(prefix, whatsappRoutes(deps, registry));
|
|
1905
2225
|
logger.info("standalone talker server ready", {
|
|
1906
2226
|
prefix: prefix || "/",
|
|
1907
2227
|
hasFlows: !!config.flowsDir,
|
|
@@ -1911,46 +2231,6 @@ async function createStandaloneServer(config) {
|
|
|
1911
2231
|
});
|
|
1912
2232
|
return app;
|
|
1913
2233
|
}
|
|
1914
|
-
|
|
1915
|
-
// src/adapters/twilio.ts
|
|
1916
|
-
async function sendSMS(config, to, message) {
|
|
1917
|
-
if (!config.accountSid || !config.authToken || !config.phoneNumber) {
|
|
1918
|
-
logger.warn("Twilio credentials not configured, skipping SMS send", { to });
|
|
1919
|
-
return false;
|
|
1920
|
-
}
|
|
1921
|
-
try {
|
|
1922
|
-
const auth = Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64");
|
|
1923
|
-
const response = await fetch(
|
|
1924
|
-
`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
1925
|
-
{
|
|
1926
|
-
method: "POST",
|
|
1927
|
-
headers: {
|
|
1928
|
-
Authorization: `Basic ${auth}`,
|
|
1929
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
1930
|
-
},
|
|
1931
|
-
body: new URLSearchParams({
|
|
1932
|
-
From: config.phoneNumber,
|
|
1933
|
-
To: to,
|
|
1934
|
-
Body: message
|
|
1935
|
-
})
|
|
1936
|
-
}
|
|
1937
|
-
);
|
|
1938
|
-
if (!response.ok) {
|
|
1939
|
-
const error = await response.text();
|
|
1940
|
-
logger.error("SMS send failed", { to, status: response.status, error });
|
|
1941
|
-
return false;
|
|
1942
|
-
}
|
|
1943
|
-
const data = await response.json();
|
|
1944
|
-
logger.info("SMS sent successfully", { to, messageSid: data.sid });
|
|
1945
|
-
return true;
|
|
1946
|
-
} catch (error) {
|
|
1947
|
-
logger.error("SMS send error", {
|
|
1948
|
-
to,
|
|
1949
|
-
error: error instanceof Error ? error.message : "Unknown"
|
|
1950
|
-
});
|
|
1951
|
-
return false;
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
2234
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1955
2235
|
0 && (module.exports = {
|
|
1956
2236
|
FlowRegistry,
|
|
@@ -1980,6 +2260,7 @@ async function sendSMS(config, to, message) {
|
|
|
1980
2260
|
getPhrase,
|
|
1981
2261
|
getSmsPhrase,
|
|
1982
2262
|
getVoiceConfig,
|
|
2263
|
+
getWhatsAppPhrase,
|
|
1983
2264
|
incrementNoSpeechRetries,
|
|
1984
2265
|
initDbClient,
|
|
1985
2266
|
inputSanitizeMiddleware,
|
|
@@ -1998,6 +2279,7 @@ async function sendSMS(config, to, message) {
|
|
|
1998
2279
|
runMigrations,
|
|
1999
2280
|
sayTwiml,
|
|
2000
2281
|
sendSMS,
|
|
2282
|
+
sendWhatsApp,
|
|
2001
2283
|
setActiveFlow,
|
|
2002
2284
|
setDetectedLanguage,
|
|
2003
2285
|
setLastPrompt,
|
|
@@ -2005,8 +2287,10 @@ async function sendSMS(config, to, message) {
|
|
|
2005
2287
|
smsRoutes,
|
|
2006
2288
|
startCleanup,
|
|
2007
2289
|
stopCleanup,
|
|
2290
|
+
stripWhatsAppPrefix,
|
|
2008
2291
|
transferTwiml,
|
|
2009
2292
|
twilioSignatureMiddleware,
|
|
2010
2293
|
updateFlowParams,
|
|
2011
|
-
validateTwilioSignature
|
|
2294
|
+
validateTwilioSignature,
|
|
2295
|
+
whatsappRoutes
|
|
2012
2296
|
});
|