jsfe 1.4.1 → 1.5.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/README.md +25 -0
- package/dist/helpers/amount-to-speech.d.ts +6 -0
- package/dist/helpers/amount-to-speech.d.ts.map +1 -0
- package/dist/helpers/amount-to-speech.js +195 -0
- package/dist/helpers/answer.d.ts +50 -0
- package/dist/helpers/answer.d.ts.map +1 -0
- package/dist/helpers/answer.js +409 -0
- package/dist/helpers/capture.d.ts +5 -0
- package/dist/helpers/capture.d.ts.map +1 -0
- package/dist/helpers/capture.js +50 -0
- package/dist/helpers/card-checks.d.ts +8 -0
- package/dist/helpers/card-checks.d.ts.map +1 -0
- package/dist/helpers/card-checks.js +46 -0
- package/dist/helpers/contact.d.ts +50 -0
- package/dist/helpers/contact.d.ts.map +1 -0
- package/dist/helpers/contact.js +178 -0
- package/dist/helpers/date-words.d.ts +2 -0
- package/dist/helpers/date-words.d.ts.map +1 -0
- package/dist/helpers/date-words.js +29 -0
- package/dist/helpers/index.d.ts +72 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +34 -0
- package/dist/helpers/match-inputs.d.ts +28 -0
- package/dist/helpers/match-inputs.d.ts.map +1 -0
- package/dist/helpers/match-inputs.js +223 -0
- package/dist/helpers/matches-choice.d.ts +26 -0
- package/dist/helpers/matches-choice.d.ts.map +1 -0
- package/dist/helpers/matches-choice.js +61 -0
- package/dist/helpers/otp-check.d.ts +36 -0
- package/dist/helpers/otp-check.d.ts.map +1 -0
- package/dist/helpers/otp-check.js +75 -0
- package/dist/helpers/phrases.d.ts +12 -0
- package/dist/helpers/phrases.d.ts.map +1 -0
- package/dist/helpers/phrases.js +31 -0
- package/dist/helpers/url-speech.d.ts +8 -0
- package/dist/helpers/url-speech.d.ts.map +1 -0
- package/dist/helpers/url-speech.js +28 -0
- package/dist/helpers/validators.d.ts +13 -0
- package/dist/helpers/validators.d.ts.map +1 -0
- package/dist/helpers/validators.js +32 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -342,6 +342,31 @@ const body = buildAiRequest(
|
|
|
342
342
|
- `mergeTextParameters` adds a text model's free-text parameters only when the text model chose
|
|
343
343
|
the same flow. The classifier's enum and boolean values always win.
|
|
344
344
|
|
|
345
|
+
**Flow helpers — `jsfe/helpers`:**
|
|
346
|
+
The functions the bundled system flows (`ask-choice`, `collect-input`, `authenticate-user`…) call from
|
|
347
|
+
expressions, so every host runs the same ones. Pure: no credentials, no network. The engine does not
|
|
348
|
+
register them by itself; a host lists them among its approved functions:
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
import { STANDARD_FUNCTIONS } from 'jsfe/helpers';
|
|
352
|
+
|
|
353
|
+
const engine = new WorkflowEngine(logger, aiCallback, flows, tools,
|
|
354
|
+
{ ...STANDARD_FUNCTIONS, sendSMSOTP, sendEmail /* the host's own, credentialed functions */ });
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
- Reading answers: `matchesChoice`, `pickChoice`, `isAnswer`, `isAgentRequest`, `isOtherRequest`,
|
|
358
|
+
`isSpelledFragment`, `isAmountAnswer`, `amountInAnswer`, `readEmail`, `readPhone`,
|
|
359
|
+
`readInternationalPhone`, `readIdentifier`, `normalizeAndFindCapture`, and `matchInputs` (which of a
|
|
360
|
+
form's inputs an answer fills; ties between inputs of the same form are returned for the flow to ask).
|
|
361
|
+
- Saying values: `amountToSpeech`, `dateToWords`, `last4`, `maskEmail`, `urlToSpeech`,
|
|
362
|
+
`textWithUrlToSpeech`, `anythingElse`, `goodbye`.
|
|
363
|
+
- Checks: `validateDigits`, `validatePhone`, `validateEmail`, `validateCardNumber`,
|
|
364
|
+
`validateExpiration`, and `validateOTP(code, container)`, which accepts a code whose SHA-256 matches
|
|
365
|
+
`container.otpHash` within 10 minutes of `container.otpTimestamp` and leaves the reason for a refusal
|
|
366
|
+
in `container.otpFailure` (`missing`, `expired` or `mismatch`). Sending the code is the host's job.
|
|
367
|
+
- Session-aware helpers read only the cargo a caller passes: `callerTextNumber`, `textDestination`,
|
|
368
|
+
`callerIsInternational`.
|
|
369
|
+
|
|
345
370
|
**Alternative AI Services:**
|
|
346
371
|
You can integrate any AI service (Claude, Gemini, local LLMs, etc.) by implementing this same interface. The engine only requires a function that takes system instructions and user input, then returns an AI response.
|
|
347
372
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An amount for a reply: spoken in words on voice ("Fifty Dollars and Twenty Five Cents", "Cincuenta dólares…"), written
|
|
3
|
+
* as '$1,500.00' on text. amountToSpeech; pure, part of jsfe/helpers.
|
|
4
|
+
*/
|
|
5
|
+
export function amountToSpeech(amount: any, language?: string, voice?: boolean): string;
|
|
6
|
+
//# sourceMappingURL=amount-to-speech.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"amount-to-speech.d.ts","sourceRoot":"","sources":["../../src/helpers/amount-to-speech.js"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wFA4MC"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An amount for a reply: spoken in words on voice ("Fifty Dollars and Twenty Five Cents", "Cincuenta dólares…"), written
|
|
3
|
+
* as '$1,500.00' on text. amountToSpeech; pure, part of jsfe/helpers.
|
|
4
|
+
*/
|
|
5
|
+
export function amountToSpeech(amount, language = 'en', voice = true) {
|
|
6
|
+
try {
|
|
7
|
+
if (!voice) {
|
|
8
|
+
// Written: '$1,500.00' (it was the bare number, '1500.00', in every text reply).
|
|
9
|
+
const n = typeof amount === 'number' ? amount : parseFloat(String(amount).replace(/[^-0-9.]/g, ''));
|
|
10
|
+
return isNaN(n) ? String(amount) : (n < 0 ? '-' : '') + '$' + Math.abs(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
11
|
+
}
|
|
12
|
+
if (!language) {
|
|
13
|
+
language = 'en';
|
|
14
|
+
}
|
|
15
|
+
if (language === 'en') {
|
|
16
|
+
const units = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"];
|
|
17
|
+
const tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
|
|
18
|
+
const scales = ["", "Thousand", "Million", "Billion"];
|
|
19
|
+
function convertSmallNumber(num) {
|
|
20
|
+
let words = "";
|
|
21
|
+
if (num >= 100) {
|
|
22
|
+
words += units[Math.floor(num / 100)] + " Hundred ";
|
|
23
|
+
num %= 100;
|
|
24
|
+
}
|
|
25
|
+
if (num >= 20) {
|
|
26
|
+
words += tens[Math.floor(num / 10)] + (num % 10 !== 0 ? " " + units[num % 10] : "") + " ";
|
|
27
|
+
}
|
|
28
|
+
else if (num > 0) {
|
|
29
|
+
words += units[num] + " ";
|
|
30
|
+
}
|
|
31
|
+
return words;
|
|
32
|
+
}
|
|
33
|
+
// 0. Validate and normalize input
|
|
34
|
+
if (typeof amount === 'string') {
|
|
35
|
+
amount = amount.replace(/[^-0-9.]/g, ''); // Keep the minus sign!
|
|
36
|
+
}
|
|
37
|
+
const numAmount = parseFloat(amount);
|
|
38
|
+
if (isNaN(numAmount)) {
|
|
39
|
+
return "Invalid amount";
|
|
40
|
+
}
|
|
41
|
+
if (Math.abs(numAmount) >= 1000000000000) {
|
|
42
|
+
return "Amount exceeds 999 Billion limit";
|
|
43
|
+
}
|
|
44
|
+
// 1. Handle "Negative" prefix and work with absolute value
|
|
45
|
+
const isNegative = numAmount < 0;
|
|
46
|
+
let absoluteAmount = Math.abs(numAmount);
|
|
47
|
+
// 2. Split Dollars and Cents
|
|
48
|
+
const parts = absoluteAmount.toFixed(2).split(".");
|
|
49
|
+
let dollarVal = parseInt(parts[0]);
|
|
50
|
+
let centVal = parseInt(parts[1]);
|
|
51
|
+
// 3. Process Dollars
|
|
52
|
+
let dollarWords = "";
|
|
53
|
+
if (dollarVal === 0) {
|
|
54
|
+
dollarWords = "Zero";
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
let scaleIdx = 0;
|
|
58
|
+
while (dollarVal > 0) {
|
|
59
|
+
let chunk = dollarVal % 1000;
|
|
60
|
+
if (chunk > 0) {
|
|
61
|
+
dollarWords = convertSmallNumber(chunk) + scales[scaleIdx] + " " + dollarWords;
|
|
62
|
+
}
|
|
63
|
+
dollarVal = Math.floor(dollarVal / 1000);
|
|
64
|
+
scaleIdx++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 4. Formatting for TTS
|
|
68
|
+
const dLabel = (parseInt(parts[0]) === 1) ? "Dollar" : "Dollars";
|
|
69
|
+
const cLabel = (centVal === 1) ? "Cent" : "Cents";
|
|
70
|
+
let finalSpeech = (isNegative ? "Negative " : "") + dollarWords.trim() + " " + dLabel;
|
|
71
|
+
if (centVal > 0) {
|
|
72
|
+
const centWords = convertSmallNumber(centVal);
|
|
73
|
+
finalSpeech += " and " + centWords.trim() + " " + cLabel;
|
|
74
|
+
}
|
|
75
|
+
return finalSpeech.replace(/\s+/g, ' ');
|
|
76
|
+
}
|
|
77
|
+
else if (language === 'es') {
|
|
78
|
+
// Spanish implementation
|
|
79
|
+
const basics = ["", "Un", "Dos", "Tres", "Cuatro", "Cinco", "Seis", "Siete", "Ocho", "Nueve", "Diez", "Once", "Doce", "Trece", "Catorce", "Quince", "Dieciséis", "Diecisiete", "Dieciocho", "Diecinueve", "Veinte", "Veintiún", "Veintidós", "Veintitrés", "Veinticuatro", "Veinticinco", "Veintiséis", "Veintisiete", "Veintiocho", "Veintinueve"];
|
|
80
|
+
const tens = ["", "", "Veinte", "Treinta", "Cuarenta", "Cincuenta", "Sesenta", "Setenta", "Ochenta", "Noventa"];
|
|
81
|
+
const hundreds = ["", "Ciento", "Doscientos", "Trescientos", "Cuatrocientos", "Quinientos", "Seiscientos", "Setecientos", "Ochocientos", "Novecientos"];
|
|
82
|
+
function convertGroup(n) {
|
|
83
|
+
let output = "";
|
|
84
|
+
if (n === 100)
|
|
85
|
+
return "Cien";
|
|
86
|
+
if (n >= 100) {
|
|
87
|
+
output += hundreds[Math.floor(n / 100)] + " ";
|
|
88
|
+
n %= 100;
|
|
89
|
+
}
|
|
90
|
+
if (n === 0 && output.length > 0)
|
|
91
|
+
return output.trim();
|
|
92
|
+
if (n === 0)
|
|
93
|
+
return "";
|
|
94
|
+
if (n < 30) {
|
|
95
|
+
output += basics[n];
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
output += tens[Math.floor(n / 10)];
|
|
99
|
+
if ((n % 10) > 0) {
|
|
100
|
+
output += " y " + basics[n % 10];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return output.trim();
|
|
104
|
+
}
|
|
105
|
+
if (typeof amount === 'string') {
|
|
106
|
+
amount = amount.replace(/[^-0-9.]/g, '');
|
|
107
|
+
}
|
|
108
|
+
const numAmount = parseFloat(amount);
|
|
109
|
+
if (isNaN(numAmount)) {
|
|
110
|
+
return "Cantidad no válida";
|
|
111
|
+
}
|
|
112
|
+
if (Math.abs(numAmount) >= 1000000000000) {
|
|
113
|
+
return "Cantidad excede el límite de 999 billones";
|
|
114
|
+
}
|
|
115
|
+
const isNegative = numAmount < 0;
|
|
116
|
+
let absoluteAmount = Math.abs(numAmount);
|
|
117
|
+
const parts = absoluteAmount.toFixed(2).split(".");
|
|
118
|
+
let dollarVal = parseInt(parts[0]);
|
|
119
|
+
let centVal = parseInt(parts[1]);
|
|
120
|
+
let dollarWords = "";
|
|
121
|
+
if (dollarVal === 0) {
|
|
122
|
+
dollarWords = "Cero";
|
|
123
|
+
}
|
|
124
|
+
else if (dollarVal === 1) {
|
|
125
|
+
dollarWords = "Un";
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
let scaleIdx = 0;
|
|
129
|
+
let tempDollars = dollarVal;
|
|
130
|
+
let accumulated = "";
|
|
131
|
+
while (tempDollars > 0) {
|
|
132
|
+
let chunk = tempDollars % 1000;
|
|
133
|
+
if (chunk > 0) {
|
|
134
|
+
let chunkText = convertGroup(chunk);
|
|
135
|
+
if (scaleIdx === 0) {
|
|
136
|
+
accumulated = chunkText + " " + accumulated;
|
|
137
|
+
}
|
|
138
|
+
else if (scaleIdx === 1) {
|
|
139
|
+
// Thousands
|
|
140
|
+
if (chunk === 1) {
|
|
141
|
+
accumulated = "Mil " + accumulated;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
accumulated = chunkText + " Mil " + accumulated;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (scaleIdx === 2) {
|
|
148
|
+
// Millions
|
|
149
|
+
if (chunk === 1) {
|
|
150
|
+
accumulated = "Un Millón " + accumulated;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
accumulated = chunkText + " Millones " + accumulated;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (scaleIdx === 3) {
|
|
157
|
+
// Billions
|
|
158
|
+
if (chunk === 1) {
|
|
159
|
+
accumulated = "Un Billón " + accumulated;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
accumulated = chunkText + " Billones " + accumulated;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
tempDollars = Math.floor(tempDollars / 1000);
|
|
167
|
+
scaleIdx++;
|
|
168
|
+
}
|
|
169
|
+
dollarWords = accumulated.trim();
|
|
170
|
+
}
|
|
171
|
+
const dLabel = (parseInt(parts[0]) === 1) ? "Dólar" : "Dólares";
|
|
172
|
+
const cLabel = (centVal === 1) ? "Centavo" : "Centavos";
|
|
173
|
+
// Add "de" for exact millions/billions
|
|
174
|
+
if (dollarWords.endsWith("Millón") || dollarWords.endsWith("Millones") || dollarWords.endsWith("Billón") || dollarWords.endsWith("Billones")) {
|
|
175
|
+
dollarWords += " de";
|
|
176
|
+
}
|
|
177
|
+
let finalSpeech = (isNegative ? "Negativo " : "") + dollarWords + " " + dLabel;
|
|
178
|
+
if (centVal > 0) {
|
|
179
|
+
let centWords = "";
|
|
180
|
+
if (centVal === 1)
|
|
181
|
+
centWords = "Un";
|
|
182
|
+
else
|
|
183
|
+
centWords = convertGroup(centVal);
|
|
184
|
+
finalSpeech += " con " + centWords + " " + cLabel;
|
|
185
|
+
}
|
|
186
|
+
return finalSpeech.replace(/\s+/g, ' ').trim();
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
return `Language ${language} not supported for amount to speech conversion`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
return `Error converting amount to speech: ${error.message}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the answer is a `kind` answer ('yes' | 'no' | 'agent' | 'exit' | 'text'), with the caller's `extra` words.
|
|
3
|
+
* YES, AGENT and TEXT never match a negated answer ("not sure", "I don't want an agent"); 0 is AGENT, * is EXIT.
|
|
4
|
+
*/
|
|
5
|
+
export function isAnswer(input: any, kind: any, extra?: any[]): boolean;
|
|
6
|
+
/** Whether the WHOLE answer asks for a person ("agent", "I want to talk to a representative", "0") — for free-text prompts. */
|
|
7
|
+
export function isAgentRequest(input: any): boolean;
|
|
8
|
+
/** Whether the answer is (part of) a spelled address — letters said one at a time, or a spelled symbol. */
|
|
9
|
+
export function isSpelledFragment(input: any): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a free-text answer to a question that expects a `kind` ('number' | 'email' | 'contact') is the caller
|
|
12
|
+
* changing direction ("what's my balance?" to "how much would you like to pay?") rather than an attempt at the answer:
|
|
13
|
+
* three or more words, none of them the expected shape. Such an answer is DISPATCHed, never counted as a failed try.
|
|
14
|
+
* A short miss ("what?") is not: the asking flow re-asks it.
|
|
15
|
+
*/
|
|
16
|
+
export function isOtherRequest(input: any, kind: any): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the answer names an amount ("$1", "1 dollar", "un dólar", "50 cents", "12.50") rather than a keypad key.
|
|
19
|
+
* Such an answer never picks a digit choice (pickChoice): "$1" said at "press 1 to pay what is due" chose FULL and
|
|
20
|
+
* the caller was offered $235.90 (2026-10-14).
|
|
21
|
+
*/
|
|
22
|
+
export function isAmountAnswer(input: any): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The amount an answer names, or NaN when it names none or is ambiguous. Prod replay 2026-10-14: the first number in the
|
|
25
|
+
* answer was taken, so "pagar cuenta 50, ciento treinta y cuatro…" paid $50, "$10 con 18 centavos" paid $10.00 and an
|
|
26
|
+
* offer read back with "terminada en 2 4 8 6" paid $2. Numbers that name an account, card, code or ZIP, and digits spoken
|
|
27
|
+
* one by one, are not amounts; two different remaining numbers are ambiguous unless exactly one is marked as money ($,
|
|
28
|
+
* dollars, cents or decimals). "50 cents" alone is $0.50; "82 dollars and 50" and "82 con 50" are $82.50. Amounts said only in words are NaN (not parsed yet).
|
|
29
|
+
*/
|
|
30
|
+
export function amountInAnswer(input: any): number;
|
|
31
|
+
/**
|
|
32
|
+
* ask-choice's matcher: the key of the choice the answer picks, '__agent', '__exit', or '' (no choice).
|
|
33
|
+
* choices: [{ key, words, negative?, neutral?, exact? }]. A key (digit, * or #) and a YES word pick a choice only as the whole
|
|
34
|
+
* answer, courtesy aside (onlyAffirmative); an `exact` choice (one that charges or cancels) treats ALL its words that way. A choice keyed 'yes' also takes ANSWER_WORDS.yes (CONFIRM_WORDS when `exact`) and, when not `exact`, a bare thanks (THANKS), and the NO-type
|
|
35
|
+
* choice (key no / decline / done / cancel, or negative: true) also takes ANSWER_WORDS.no, so callers list only their
|
|
36
|
+
* extras. A word after a negation in its clause is negated: it picks the NO-type choice; a `neutral` choice still
|
|
37
|
+
* matches unless it follows its negation within two words; any other choice is blocked and NO answers. A deferral
|
|
38
|
+
* (later, mañana) answers NO; a reply led by a bare 'no' turns YES into NO. With acceptOther, AGENT only when the
|
|
39
|
+
* whole answer is an agent request and EXIT only on exit/quit/salir/abort (cancel is an answer there).
|
|
40
|
+
*/
|
|
41
|
+
export function pickChoice(answer: any, choices: any, acceptOther: any): any;
|
|
42
|
+
export namespace ANSWER_WORDS {
|
|
43
|
+
let yes: string[];
|
|
44
|
+
let no: string[];
|
|
45
|
+
let agent: string[];
|
|
46
|
+
let exit: string[];
|
|
47
|
+
let text: string[];
|
|
48
|
+
}
|
|
49
|
+
export const CONFIRM_WORDS: string[];
|
|
50
|
+
//# sourceMappingURL=answer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAyIA;;;GAGG;AACH,wEAgBC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAiBD;;;;;;GAMG;AACH,mDA6BC;AAUD;;;;;;;;;GASG;AACH,6EAyGC;;;;;;;;AAjWD,qCAKiD"}
|