@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.mjs
CHANGED
|
@@ -233,8 +233,35 @@ async function closeDbClient() {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
// src/db/migrate.ts
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
var SCHEMA_STATEMENTS = [
|
|
237
|
+
`CREATE TABLE IF NOT EXISTS talker_sessions (
|
|
238
|
+
id TEXT PRIMARY KEY,
|
|
239
|
+
phone_number TEXT NOT NULL,
|
|
240
|
+
channel TEXT NOT NULL CHECK(channel IN ('call', 'sms')),
|
|
241
|
+
reason TEXT NOT NULL CHECK(reason IN ('ended', 'redirected')),
|
|
242
|
+
language TEXT NOT NULL,
|
|
243
|
+
started_at INTEGER NOT NULL,
|
|
244
|
+
ended_at INTEGER NOT NULL,
|
|
245
|
+
duration_ms INTEGER NOT NULL,
|
|
246
|
+
transfer_reason TEXT,
|
|
247
|
+
conversation_id TEXT,
|
|
248
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
249
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
250
|
+
)`,
|
|
251
|
+
`CREATE TABLE IF NOT EXISTS talker_messages (
|
|
252
|
+
id TEXT PRIMARY KEY,
|
|
253
|
+
session_id TEXT NOT NULL,
|
|
254
|
+
role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
|
|
255
|
+
content TEXT NOT NULL,
|
|
256
|
+
timestamp INTEGER NOT NULL,
|
|
257
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
258
|
+
FOREIGN KEY (session_id) REFERENCES talker_sessions(id) ON DELETE CASCADE
|
|
259
|
+
)`,
|
|
260
|
+
"CREATE INDEX IF NOT EXISTS idx_talker_sessions_phone ON talker_sessions(phone_number)",
|
|
261
|
+
"CREATE INDEX IF NOT EXISTS idx_talker_sessions_created ON talker_sessions(created_at DESC)",
|
|
262
|
+
"CREATE INDEX IF NOT EXISTS idx_talker_messages_session ON talker_messages(session_id)",
|
|
263
|
+
"CREATE INDEX IF NOT EXISTS idx_talker_messages_ts ON talker_messages(timestamp)"
|
|
264
|
+
];
|
|
238
265
|
async function runMigrations() {
|
|
239
266
|
const client2 = getDbClient();
|
|
240
267
|
if (!client2) {
|
|
@@ -242,11 +269,8 @@ async function runMigrations() {
|
|
|
242
269
|
return;
|
|
243
270
|
}
|
|
244
271
|
try {
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
const statements = schema.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
248
|
-
logger.info("running database migrations", { statementCount: statements.length });
|
|
249
|
-
for (const statement of statements) {
|
|
272
|
+
logger.info("running database migrations", { statementCount: SCHEMA_STATEMENTS.length });
|
|
273
|
+
for (const statement of SCHEMA_STATEMENTS) {
|
|
250
274
|
await client2.execute(statement);
|
|
251
275
|
}
|
|
252
276
|
logger.info("database migrations completed");
|
|
@@ -259,7 +283,7 @@ async function runMigrations() {
|
|
|
259
283
|
}
|
|
260
284
|
|
|
261
285
|
// src/flows/registry.ts
|
|
262
|
-
import { readFileSync as
|
|
286
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
263
287
|
|
|
264
288
|
// src/flows/intent.ts
|
|
265
289
|
var OPENAI_API_URL = "https://api.openai.com/v1/chat/completions";
|
|
@@ -348,8 +372,8 @@ Rules:
|
|
|
348
372
|
}
|
|
349
373
|
|
|
350
374
|
// src/flows/loader.ts
|
|
351
|
-
import { existsSync, readFileSync
|
|
352
|
-
import { join
|
|
375
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
376
|
+
import { join } from "node:path";
|
|
353
377
|
async function loadFlowsFromDirectory(flowsDir) {
|
|
354
378
|
const flows = /* @__PURE__ */ new Map();
|
|
355
379
|
if (!existsSync(flowsDir)) {
|
|
@@ -374,10 +398,10 @@ async function loadFlowsFromDirectory(flowsDir) {
|
|
|
374
398
|
return flows;
|
|
375
399
|
}
|
|
376
400
|
async function loadFlow(flowsDir, flowName) {
|
|
377
|
-
const flowPath =
|
|
378
|
-
const definitionPath =
|
|
379
|
-
const instructionsPath =
|
|
380
|
-
const handlerPath =
|
|
401
|
+
const flowPath = join(flowsDir, flowName);
|
|
402
|
+
const definitionPath = join(flowPath, "flow.json");
|
|
403
|
+
const instructionsPath = join(flowPath, "instructions.md");
|
|
404
|
+
const handlerPath = join(flowPath, "handler.ts");
|
|
381
405
|
if (!existsSync(definitionPath)) {
|
|
382
406
|
throw new Error(`flow.json not found for ${flowName}`);
|
|
383
407
|
}
|
|
@@ -387,7 +411,7 @@ async function loadFlow(flowsDir, flowName) {
|
|
|
387
411
|
if (!existsSync(handlerPath)) {
|
|
388
412
|
throw new Error(`handler.ts not found for ${flowName}`);
|
|
389
413
|
}
|
|
390
|
-
const definitionContent =
|
|
414
|
+
const definitionContent = readFileSync(definitionPath, "utf-8");
|
|
391
415
|
const definition = JSON.parse(definitionContent);
|
|
392
416
|
if (definition.id !== flowName) {
|
|
393
417
|
throw new Error(`Flow id mismatch: ${definition.id} !== ${flowName}`);
|
|
@@ -403,7 +427,7 @@ async function loadFlow(flowsDir, flowName) {
|
|
|
403
427
|
if (typeof handler !== "function") {
|
|
404
428
|
throw new Error(`Flow ${flowName} handler.ts must export an 'execute' function`);
|
|
405
429
|
}
|
|
406
|
-
const prefillPath =
|
|
430
|
+
const prefillPath = join(flowPath, "prefill.ts");
|
|
407
431
|
let prefill;
|
|
408
432
|
if (existsSync(prefillPath)) {
|
|
409
433
|
const prefillModule = await import(prefillPath);
|
|
@@ -489,7 +513,7 @@ var FlowRegistry = class {
|
|
|
489
513
|
if (!flow) {
|
|
490
514
|
throw new Error(`Flow ${flowName} not found`);
|
|
491
515
|
}
|
|
492
|
-
return
|
|
516
|
+
return readFileSync2(flow.instructionsPath, "utf-8");
|
|
493
517
|
}
|
|
494
518
|
};
|
|
495
519
|
function testModeDetectIntent(message) {
|
|
@@ -649,20 +673,69 @@ function getErrorMessage(error) {
|
|
|
649
673
|
}
|
|
650
674
|
|
|
651
675
|
// src/core/phrases.ts
|
|
652
|
-
import { existsSync as existsSync2, readFileSync as
|
|
653
|
-
import { join as
|
|
676
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
677
|
+
import { join as join2 } from "node:path";
|
|
654
678
|
var phrasesCache = {};
|
|
679
|
+
var ENGLISH_FALLBACK = {
|
|
680
|
+
greeting: "Hello! I'm your voice assistant. How can I help you today?",
|
|
681
|
+
didNotCatch: "I didn't catch that. Could you please repeat?",
|
|
682
|
+
didNotHear: "I didn't hear anything. Goodbye.",
|
|
683
|
+
didNotHearRetry: "Sorry, I didn't catch that. Could you try again?",
|
|
684
|
+
didNotHearFinal: "I'm really sorry but I cannot hear what you're saying. Please call again. Bye for now.",
|
|
685
|
+
transfer: "Let me connect you with someone directly.",
|
|
686
|
+
acknowledgment: "One moment please...",
|
|
687
|
+
farewell: {
|
|
688
|
+
morning: "You're welcome! Have a wonderful day. Goodbye!",
|
|
689
|
+
afternoon: "You're welcome! Have a lovely afternoon. Goodbye!",
|
|
690
|
+
evening: "You're welcome! Have a good evening. Goodbye!"
|
|
691
|
+
},
|
|
692
|
+
error: "Sorry, I encountered an error. Please try again later. Goodbye.",
|
|
693
|
+
timeout: "Sorry, I took too long to respond. Please try again. Goodbye.",
|
|
694
|
+
lostQuestion: "I'm sorry, I lost track of your question. Could you please repeat?",
|
|
695
|
+
flow: {
|
|
696
|
+
cancelled: "No problem! I've cancelled that. What else would you like to know?"
|
|
697
|
+
},
|
|
698
|
+
sms: {
|
|
699
|
+
greeting: "Hi! I'm your voice assistant. Ask me anything!",
|
|
700
|
+
greetingShort: "Hi! Ask me anything!",
|
|
701
|
+
callForHelp: "For more complex questions, feel free to call back or reach us directly.",
|
|
702
|
+
processingError: "I'm having trouble processing that. Please try texting again or call back.",
|
|
703
|
+
genericError: "Sorry, something went wrong. Please try again."
|
|
704
|
+
},
|
|
705
|
+
whatsapp: {
|
|
706
|
+
greeting: "Hi! I'm your assistant. Send me a message and I'll help you out!",
|
|
707
|
+
greetingShort: "Hi! How can I help?",
|
|
708
|
+
callForHelp: "For more complex questions, feel free to call us directly or reply here with more details.",
|
|
709
|
+
processingError: "I'm having trouble processing that. Please try sending your message again.",
|
|
710
|
+
genericError: "Sorry, something went wrong. Please try again."
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
function resolveBuiltinLanguageDir() {
|
|
714
|
+
const candidates = [
|
|
715
|
+
join2(__dirname, "../../language"),
|
|
716
|
+
// source: src/core/ -> language/
|
|
717
|
+
join2(__dirname, "../language")
|
|
718
|
+
// dist: dist/ -> language/
|
|
719
|
+
];
|
|
720
|
+
for (const dir of candidates) {
|
|
721
|
+
if (existsSync2(dir)) {
|
|
722
|
+
return dir;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return void 0;
|
|
726
|
+
}
|
|
655
727
|
function loadPhrases(language, languageDir) {
|
|
656
728
|
const cacheKey = `${languageDir || "default"}:${language}`;
|
|
657
729
|
if (phrasesCache[cacheKey]) {
|
|
658
730
|
return phrasesCache[cacheKey];
|
|
659
731
|
}
|
|
660
|
-
const
|
|
732
|
+
const builtinDir = resolveBuiltinLanguageDir();
|
|
733
|
+
const dirs = [languageDir, builtinDir].filter(Boolean);
|
|
661
734
|
for (const dir of dirs) {
|
|
662
|
-
const filePath =
|
|
735
|
+
const filePath = join2(dir, `${language}.json`);
|
|
663
736
|
if (existsSync2(filePath)) {
|
|
664
737
|
try {
|
|
665
|
-
const content =
|
|
738
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
666
739
|
phrasesCache[cacheKey] = JSON.parse(content);
|
|
667
740
|
return phrasesCache[cacheKey];
|
|
668
741
|
} catch {
|
|
@@ -672,7 +745,8 @@ function loadPhrases(language, languageDir) {
|
|
|
672
745
|
if (language !== "en") {
|
|
673
746
|
return loadPhrases("en", languageDir);
|
|
674
747
|
}
|
|
675
|
-
|
|
748
|
+
phrasesCache[cacheKey] = ENGLISH_FALLBACK;
|
|
749
|
+
return ENGLISH_FALLBACK;
|
|
676
750
|
}
|
|
677
751
|
function getPhrase(language, key, languageDir) {
|
|
678
752
|
const phrases = loadPhrases(language, languageDir);
|
|
@@ -697,6 +771,13 @@ function getSmsPhrase(language, key, languageDir) {
|
|
|
697
771
|
const phrases = loadPhrases(language, languageDir);
|
|
698
772
|
return phrases.sms[key];
|
|
699
773
|
}
|
|
774
|
+
function getWhatsAppPhrase(language, key, languageDir) {
|
|
775
|
+
const phrases = loadPhrases(language, languageDir);
|
|
776
|
+
if (phrases.whatsapp) {
|
|
777
|
+
return phrases.whatsapp[key];
|
|
778
|
+
}
|
|
779
|
+
return phrases.sms[key];
|
|
780
|
+
}
|
|
700
781
|
|
|
701
782
|
// src/core/voice.ts
|
|
702
783
|
var DEFAULT_VOICES = {
|
|
@@ -1158,40 +1239,116 @@ async function callOpenAI(deps, systemPrompt, userMessage, context) {
|
|
|
1158
1239
|
}
|
|
1159
1240
|
|
|
1160
1241
|
// src/core/processing/prompts.ts
|
|
1161
|
-
import { existsSync as existsSync3, readFileSync as
|
|
1162
|
-
import { join as
|
|
1242
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
1243
|
+
import { join as join3 } from "node:path";
|
|
1163
1244
|
var incomingPrompt = null;
|
|
1164
1245
|
var outgoingPrompt = null;
|
|
1165
1246
|
function loadPromptFile(filename, customPath) {
|
|
1166
1247
|
if (customPath && existsSync3(customPath)) {
|
|
1167
|
-
return
|
|
1168
|
-
}
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1248
|
+
return readFileSync4(customPath, "utf-8");
|
|
1249
|
+
}
|
|
1250
|
+
const candidates = [
|
|
1251
|
+
join3(__dirname, "../../../prompts", filename),
|
|
1252
|
+
// source: src/core/processing/ -> prompts/
|
|
1253
|
+
join3(__dirname, "../../prompts", filename),
|
|
1254
|
+
// dist: dist/ -> prompts/
|
|
1255
|
+
join3(__dirname, "../prompts", filename)
|
|
1256
|
+
// flat dist
|
|
1257
|
+
];
|
|
1258
|
+
for (const path of candidates) {
|
|
1259
|
+
if (existsSync3(path)) {
|
|
1260
|
+
return readFileSync4(path, "utf-8");
|
|
1261
|
+
}
|
|
1172
1262
|
}
|
|
1173
|
-
|
|
1263
|
+
return void 0;
|
|
1174
1264
|
}
|
|
1265
|
+
var DEFAULT_INCOMING_PROMPT = `# Incoming Message Processor
|
|
1266
|
+
|
|
1267
|
+
You are a pre-processor for a voice assistant. Your job is to analyze incoming caller speech and determine three things:
|
|
1268
|
+
|
|
1269
|
+
1. Should the caller be transferred to a human directly?
|
|
1270
|
+
2. What language is the caller speaking?
|
|
1271
|
+
3. What is the cleaned-up version of their message to send to the knowledge base?
|
|
1272
|
+
|
|
1273
|
+
## IMPORTANT: Conversation Context
|
|
1274
|
+
|
|
1275
|
+
You may receive conversation history along with the current message. **You MUST consider the context** when making decisions.
|
|
1276
|
+
|
|
1277
|
+
## Language Detection
|
|
1278
|
+
|
|
1279
|
+
Detect the language the caller is speaking. Supported: "en", "fr", "nl", "de", "es", "pt". Default: "en".
|
|
1280
|
+
|
|
1281
|
+
## Transfer Detection
|
|
1282
|
+
|
|
1283
|
+
Transfer if the caller expresses: "speak to someone", "talk to a person", "connect me", "real person", "human", frustration signals, or complex/personal matters.
|
|
1284
|
+
|
|
1285
|
+
## End Call Detection
|
|
1286
|
+
|
|
1287
|
+
End call if: "no thanks", "that's all", "goodbye", "bye", "I'm done", "nothing else". Only if clearly ending the conversation.
|
|
1288
|
+
|
|
1289
|
+
## Response Format
|
|
1290
|
+
|
|
1291
|
+
Respond with valid JSON:
|
|
1292
|
+
\`\`\`json
|
|
1293
|
+
{
|
|
1294
|
+
"shouldTransfer": true or false,
|
|
1295
|
+
"shouldEndCall": true or false,
|
|
1296
|
+
"detectedLanguage": "en" or "fr" or "nl" or "de" or "es" or "pt",
|
|
1297
|
+
"processedMessage": "the cleaned up message"
|
|
1298
|
+
}
|
|
1299
|
+
\`\`\`
|
|
1300
|
+
|
|
1301
|
+
## Message Cleaning Rules
|
|
1302
|
+
|
|
1303
|
+
- Fix obvious speech-to-text errors
|
|
1304
|
+
- Remove filler words (um, uh, like, you know)
|
|
1305
|
+
- Keep the core intent and original language intact`;
|
|
1306
|
+
var DEFAULT_OUTGOING_PROMPT = `# Outgoing Response Processor
|
|
1307
|
+
|
|
1308
|
+
You are a post-processor for a voice assistant. Transform knowledge base responses into channel-appropriate messages.
|
|
1309
|
+
|
|
1310
|
+
## Channel Type
|
|
1311
|
+
|
|
1312
|
+
You will be told the channel: "call" (phone), "sms" (text message), or "whatsapp".
|
|
1313
|
+
|
|
1314
|
+
**For CALL:** Spoken aloud by TTS. Convert numbers to words. Remove URLs. Max 2 sentences.
|
|
1315
|
+
**For SMS:** Read on phone screen. Keep digits. Max 160 chars. No markdown.
|
|
1316
|
+
**For WHATSAPP:** Read in chat. Keep digits and URLs. Can use *bold*, _italic_. Up to 500 chars.
|
|
1317
|
+
|
|
1318
|
+
## Language Requirement
|
|
1319
|
+
|
|
1320
|
+
You MUST respond in the specified language.
|
|
1321
|
+
|
|
1322
|
+
## Rules
|
|
1323
|
+
|
|
1324
|
+
1. Be concise - answer directly
|
|
1325
|
+
2. End with a follow-up question
|
|
1326
|
+
3. Always use third person
|
|
1327
|
+
4. Remove markdown, lists, technical jargon
|
|
1328
|
+
|
|
1329
|
+
Return ONLY the transformed text. No JSON, no explanations.`;
|
|
1175
1330
|
function getIncomingPrompt(deps) {
|
|
1176
1331
|
if (!incomingPrompt) {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
incomingPrompt =
|
|
1332
|
+
const loaded = loadPromptFile("incoming.md", deps.config.processing?.incomingPromptPath);
|
|
1333
|
+
if (loaded) {
|
|
1334
|
+
incomingPrompt = loaded;
|
|
1335
|
+
logger.info("incoming prompt loaded from file");
|
|
1336
|
+
} else {
|
|
1337
|
+
incomingPrompt = DEFAULT_INCOMING_PROMPT;
|
|
1338
|
+
logger.info("incoming prompt loaded (built-in default)");
|
|
1183
1339
|
}
|
|
1184
1340
|
}
|
|
1185
1341
|
return incomingPrompt;
|
|
1186
1342
|
}
|
|
1187
1343
|
function getOutgoingPrompt(deps) {
|
|
1188
1344
|
if (!outgoingPrompt) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
outgoingPrompt =
|
|
1345
|
+
const loaded = loadPromptFile("outgoing.md", deps.config.processing?.outgoingPromptPath);
|
|
1346
|
+
if (loaded) {
|
|
1347
|
+
outgoingPrompt = loaded;
|
|
1348
|
+
logger.info("outgoing prompt loaded from file");
|
|
1349
|
+
} else {
|
|
1350
|
+
outgoingPrompt = DEFAULT_OUTGOING_PROMPT;
|
|
1351
|
+
logger.info("outgoing prompt loaded (built-in default)");
|
|
1195
1352
|
}
|
|
1196
1353
|
}
|
|
1197
1354
|
return outgoingPrompt;
|
|
@@ -1285,10 +1442,10 @@ Respond in: ${language}`;
|
|
|
1285
1442
|
}
|
|
1286
1443
|
|
|
1287
1444
|
// src/flows/params.ts
|
|
1288
|
-
import { readFileSync as
|
|
1445
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1289
1446
|
var OPENAI_API_URL3 = "https://api.openai.com/v1/chat/completions";
|
|
1290
1447
|
async function extractParameters(deps, flow, phoneNumber, userMessage, existingParams) {
|
|
1291
|
-
const instructions =
|
|
1448
|
+
const instructions = readFileSync5(flow.instructionsPath, "utf-8");
|
|
1292
1449
|
const schema = flow.definition.schema;
|
|
1293
1450
|
const properties = schema.properties || {};
|
|
1294
1451
|
const required = schema.required || [];
|
|
@@ -1752,6 +1909,163 @@ function smsRoutes(deps, registry) {
|
|
|
1752
1909
|
return app;
|
|
1753
1910
|
}
|
|
1754
1911
|
|
|
1912
|
+
// src/routes/whatsapp/index.ts
|
|
1913
|
+
import { Hono as Hono3 } from "hono";
|
|
1914
|
+
|
|
1915
|
+
// src/adapters/twilio.ts
|
|
1916
|
+
function stripWhatsAppPrefix(phoneNumber) {
|
|
1917
|
+
return phoneNumber.replace(/^whatsapp:/, "");
|
|
1918
|
+
}
|
|
1919
|
+
async function sendSMS(config, to, message) {
|
|
1920
|
+
if (!config.accountSid || !config.authToken || !config.phoneNumber) {
|
|
1921
|
+
logger.warn("Twilio credentials not configured, skipping SMS send", { to });
|
|
1922
|
+
return false;
|
|
1923
|
+
}
|
|
1924
|
+
try {
|
|
1925
|
+
const auth = Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64");
|
|
1926
|
+
const response = await fetch(
|
|
1927
|
+
`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
1928
|
+
{
|
|
1929
|
+
method: "POST",
|
|
1930
|
+
headers: {
|
|
1931
|
+
Authorization: `Basic ${auth}`,
|
|
1932
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1933
|
+
},
|
|
1934
|
+
body: new URLSearchParams({
|
|
1935
|
+
From: config.phoneNumber,
|
|
1936
|
+
To: to,
|
|
1937
|
+
Body: message
|
|
1938
|
+
})
|
|
1939
|
+
}
|
|
1940
|
+
);
|
|
1941
|
+
if (!response.ok) {
|
|
1942
|
+
const error = await response.text();
|
|
1943
|
+
logger.error("SMS send failed", { to, status: response.status, error });
|
|
1944
|
+
return false;
|
|
1945
|
+
}
|
|
1946
|
+
const data = await response.json();
|
|
1947
|
+
logger.info("SMS sent successfully", { to, messageSid: data.sid });
|
|
1948
|
+
return true;
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
logger.error("SMS send error", {
|
|
1951
|
+
to,
|
|
1952
|
+
error: error instanceof Error ? error.message : "Unknown"
|
|
1953
|
+
});
|
|
1954
|
+
return false;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
async function sendWhatsApp(config, to, message) {
|
|
1958
|
+
if (!config.accountSid || !config.authToken || !config.phoneNumber) {
|
|
1959
|
+
logger.warn("Twilio credentials not configured, skipping WhatsApp send", { to });
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
try {
|
|
1963
|
+
const auth = Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64");
|
|
1964
|
+
const whatsappTo = to.startsWith("whatsapp:") ? to : `whatsapp:${to}`;
|
|
1965
|
+
const whatsappFrom = config.phoneNumber.startsWith("whatsapp:") ? config.phoneNumber : `whatsapp:${config.phoneNumber}`;
|
|
1966
|
+
const response = await fetch(
|
|
1967
|
+
`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
1968
|
+
{
|
|
1969
|
+
method: "POST",
|
|
1970
|
+
headers: {
|
|
1971
|
+
Authorization: `Basic ${auth}`,
|
|
1972
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1973
|
+
},
|
|
1974
|
+
body: new URLSearchParams({
|
|
1975
|
+
From: whatsappFrom,
|
|
1976
|
+
To: whatsappTo,
|
|
1977
|
+
Body: message
|
|
1978
|
+
})
|
|
1979
|
+
}
|
|
1980
|
+
);
|
|
1981
|
+
if (!response.ok) {
|
|
1982
|
+
const error = await response.text();
|
|
1983
|
+
logger.error("WhatsApp send failed", { to, status: response.status, error });
|
|
1984
|
+
return false;
|
|
1985
|
+
}
|
|
1986
|
+
const data = await response.json();
|
|
1987
|
+
logger.info("WhatsApp message sent successfully", { to, messageSid: data.sid });
|
|
1988
|
+
return true;
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
logger.error("WhatsApp send error", {
|
|
1991
|
+
to,
|
|
1992
|
+
error: error instanceof Error ? error.message : "Unknown"
|
|
1993
|
+
});
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/routes/whatsapp/processor.ts
|
|
1999
|
+
async function processWhatsApp(deps, registry, phoneNumber, messageBody) {
|
|
2000
|
+
const incoming = await processIncoming(deps, phoneNumber, messageBody, "whatsapp");
|
|
2001
|
+
if (incoming.shouldTransfer) {
|
|
2002
|
+
const message = getWhatsAppPhrase(
|
|
2003
|
+
incoming.detectedLanguage,
|
|
2004
|
+
"callForHelp",
|
|
2005
|
+
deps.config.languageDir
|
|
2006
|
+
);
|
|
2007
|
+
return messageTwiml(message);
|
|
2008
|
+
}
|
|
2009
|
+
const flowResult = await processFlow(
|
|
2010
|
+
deps,
|
|
2011
|
+
registry,
|
|
2012
|
+
phoneNumber,
|
|
2013
|
+
incoming.processedMessage,
|
|
2014
|
+
"whatsapp"
|
|
2015
|
+
);
|
|
2016
|
+
if (flowResult.isFlowActive || flowResult.flowCompleted) {
|
|
2017
|
+
if (flowResult.flowCompleted && flowResult.flowSuccess === false) {
|
|
2018
|
+
const message = getWhatsAppPhrase(
|
|
2019
|
+
incoming.detectedLanguage,
|
|
2020
|
+
"processingError",
|
|
2021
|
+
deps.config.languageDir
|
|
2022
|
+
);
|
|
2023
|
+
return messageTwiml(message);
|
|
2024
|
+
}
|
|
2025
|
+
return messageTwiml(flowResult.response);
|
|
2026
|
+
}
|
|
2027
|
+
const botResponse = await chat(deps, phoneNumber, incoming.processedMessage);
|
|
2028
|
+
const whatsappResponse = await processOutgoing(deps, phoneNumber, botResponse, "whatsapp");
|
|
2029
|
+
return messageTwiml(whatsappResponse);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// src/routes/whatsapp/handle-incoming.ts
|
|
2033
|
+
async function handleIncomingWhatsApp(c, deps, registry) {
|
|
2034
|
+
const body = await c.req.parseBody();
|
|
2035
|
+
const rawFrom = (body.From || "unknown").trim();
|
|
2036
|
+
const phoneNumber = stripWhatsAppPrefix(rawFrom);
|
|
2037
|
+
const messageBody = body.Body || "";
|
|
2038
|
+
logger.info("whatsapp message received", { phoneNumber, messageBody });
|
|
2039
|
+
if (!messageBody.trim()) {
|
|
2040
|
+
return c.text(messageTwiml(getWhatsAppPhrase("en", "greeting", deps.config.languageDir)), 200, {
|
|
2041
|
+
"Content-Type": "text/xml"
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
try {
|
|
2045
|
+
const twiml = await processWhatsApp(deps, registry, phoneNumber, messageBody);
|
|
2046
|
+
persistSession(phoneNumber, "whatsapp");
|
|
2047
|
+
return c.text(twiml, 200, { "Content-Type": "text/xml" });
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
logger.error("whatsapp processing error", {
|
|
2050
|
+
phoneNumber,
|
|
2051
|
+
error: getErrorMessage(error)
|
|
2052
|
+
});
|
|
2053
|
+
return c.text(
|
|
2054
|
+
messageTwiml(getWhatsAppPhrase("en", "genericError", deps.config.languageDir)),
|
|
2055
|
+
200,
|
|
2056
|
+
{ "Content-Type": "text/xml" }
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// src/routes/whatsapp/index.ts
|
|
2062
|
+
function whatsappRoutes(deps, registry) {
|
|
2063
|
+
const app = new Hono3();
|
|
2064
|
+
app.post("/whatsapp", (c) => handleIncomingWhatsApp(c, deps, registry));
|
|
2065
|
+
app.get("/whatsapp", (c) => c.text("WhatsApp endpoint active"));
|
|
2066
|
+
return app;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1755
2069
|
// src/plugin.ts
|
|
1756
2070
|
var DEFAULT_CONTEXT_TTL_MS = 30 * 60 * 1e3;
|
|
1757
2071
|
var DEFAULT_CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -1785,6 +2099,7 @@ async function createTelephonyRoutes(app, chatterDeps, config) {
|
|
|
1785
2099
|
const prefix = config.routePrefix || "";
|
|
1786
2100
|
app.route(prefix, callRoutes(deps, registry));
|
|
1787
2101
|
app.route(prefix, smsRoutes(deps, registry));
|
|
2102
|
+
app.route(prefix, whatsappRoutes(deps, registry));
|
|
1788
2103
|
logger.info("telephony routes mounted", {
|
|
1789
2104
|
prefix: prefix || "/",
|
|
1790
2105
|
hasFlows: !!config.flowsDir,
|
|
@@ -1794,7 +2109,7 @@ async function createTelephonyRoutes(app, chatterDeps, config) {
|
|
|
1794
2109
|
}
|
|
1795
2110
|
|
|
1796
2111
|
// src/standalone.ts
|
|
1797
|
-
import { Hono as
|
|
2112
|
+
import { Hono as Hono4 } from "hono";
|
|
1798
2113
|
var DEFAULT_CONTEXT_TTL_MS2 = 30 * 60 * 1e3;
|
|
1799
2114
|
var DEFAULT_CLEANUP_INTERVAL_MS2 = 5 * 60 * 1e3;
|
|
1800
2115
|
var DEFAULT_MODEL2 = "gpt-4o-mini";
|
|
@@ -1824,11 +2139,12 @@ async function createStandaloneServer(config) {
|
|
|
1824
2139
|
config.contextTtlMs ?? DEFAULT_CONTEXT_TTL_MS2,
|
|
1825
2140
|
config.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS2
|
|
1826
2141
|
);
|
|
1827
|
-
const app = new
|
|
2142
|
+
const app = new Hono4();
|
|
1828
2143
|
app.get("/healthz", (c) => c.text("ok"));
|
|
1829
2144
|
const prefix = config.routePrefix || "";
|
|
1830
2145
|
app.route(prefix, callRoutes(deps, registry));
|
|
1831
2146
|
app.route(prefix, smsRoutes(deps, registry));
|
|
2147
|
+
app.route(prefix, whatsappRoutes(deps, registry));
|
|
1832
2148
|
logger.info("standalone talker server ready", {
|
|
1833
2149
|
prefix: prefix || "/",
|
|
1834
2150
|
hasFlows: !!config.flowsDir,
|
|
@@ -1838,46 +2154,6 @@ async function createStandaloneServer(config) {
|
|
|
1838
2154
|
});
|
|
1839
2155
|
return app;
|
|
1840
2156
|
}
|
|
1841
|
-
|
|
1842
|
-
// src/adapters/twilio.ts
|
|
1843
|
-
async function sendSMS(config, to, message) {
|
|
1844
|
-
if (!config.accountSid || !config.authToken || !config.phoneNumber) {
|
|
1845
|
-
logger.warn("Twilio credentials not configured, skipping SMS send", { to });
|
|
1846
|
-
return false;
|
|
1847
|
-
}
|
|
1848
|
-
try {
|
|
1849
|
-
const auth = Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64");
|
|
1850
|
-
const response = await fetch(
|
|
1851
|
-
`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
1852
|
-
{
|
|
1853
|
-
method: "POST",
|
|
1854
|
-
headers: {
|
|
1855
|
-
Authorization: `Basic ${auth}`,
|
|
1856
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
1857
|
-
},
|
|
1858
|
-
body: new URLSearchParams({
|
|
1859
|
-
From: config.phoneNumber,
|
|
1860
|
-
To: to,
|
|
1861
|
-
Body: message
|
|
1862
|
-
})
|
|
1863
|
-
}
|
|
1864
|
-
);
|
|
1865
|
-
if (!response.ok) {
|
|
1866
|
-
const error = await response.text();
|
|
1867
|
-
logger.error("SMS send failed", { to, status: response.status, error });
|
|
1868
|
-
return false;
|
|
1869
|
-
}
|
|
1870
|
-
const data = await response.json();
|
|
1871
|
-
logger.info("SMS sent successfully", { to, messageSid: data.sid });
|
|
1872
|
-
return true;
|
|
1873
|
-
} catch (error) {
|
|
1874
|
-
logger.error("SMS send error", {
|
|
1875
|
-
to,
|
|
1876
|
-
error: error instanceof Error ? error.message : "Unknown"
|
|
1877
|
-
});
|
|
1878
|
-
return false;
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
2157
|
export {
|
|
1882
2158
|
FlowRegistry,
|
|
1883
2159
|
acknowledgmentTwiml,
|
|
@@ -1906,6 +2182,7 @@ export {
|
|
|
1906
2182
|
getPhrase,
|
|
1907
2183
|
getSmsPhrase,
|
|
1908
2184
|
getVoiceConfig,
|
|
2185
|
+
getWhatsAppPhrase,
|
|
1909
2186
|
incrementNoSpeechRetries,
|
|
1910
2187
|
initDbClient,
|
|
1911
2188
|
inputSanitizeMiddleware,
|
|
@@ -1924,6 +2201,7 @@ export {
|
|
|
1924
2201
|
runMigrations,
|
|
1925
2202
|
sayTwiml,
|
|
1926
2203
|
sendSMS,
|
|
2204
|
+
sendWhatsApp,
|
|
1927
2205
|
setActiveFlow,
|
|
1928
2206
|
setDetectedLanguage,
|
|
1929
2207
|
setLastPrompt,
|
|
@@ -1931,8 +2209,10 @@ export {
|
|
|
1931
2209
|
smsRoutes,
|
|
1932
2210
|
startCleanup,
|
|
1933
2211
|
stopCleanup,
|
|
2212
|
+
stripWhatsAppPrefix,
|
|
1934
2213
|
transferTwiml,
|
|
1935
2214
|
twilioSignatureMiddleware,
|
|
1936
2215
|
updateFlowParams,
|
|
1937
|
-
validateTwilioSignature
|
|
2216
|
+
validateTwilioSignature,
|
|
2217
|
+
whatsappRoutes
|
|
1938
2218
|
};
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AASjC,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,SAAS,CAAC;AAMhE;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,IAAI,EACT,WAAW,EAAE,kBAAkB,EAC/B,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CA4Cf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle-answer.test.d.ts","sourceRoot":"","sources":["../../../src/routes/call/handle-answer.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle-initial.test.d.ts","sourceRoot":"","sources":["../../../src/routes/call/handle-initial.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle-nospeech.test.d.ts","sourceRoot":"","sources":["../../../src/routes/call/handle-nospeech.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle-respond.test.d.ts","sourceRoot":"","sources":["../../../src/routes/call/handle-respond.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|