jsfe 1.14.0 → 1.14.2
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/forms/choice-flow.js +2 -2
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +38 -19
- package/dist/forms/identity-flows.js +2 -2
- package/dist/helpers/contact.d.ts.map +1 -1
- package/dist/helpers/contact.js +4 -1
- package/dist/host/reply-rank.d.ts +11 -4
- package/dist/host/reply-rank.d.ts.map +1 -1
- package/dist/host/reply-rank.js +28 -6
- package/dist/runtime-identity.js +1 -1
- package/package.json +1 -1
|
@@ -452,7 +452,7 @@ export const FLOWS = [
|
|
|
452
452
|
"id": "ac_switch_to_text_key",
|
|
453
453
|
"type": "SET",
|
|
454
454
|
"variable": "ac_key",
|
|
455
|
-
"value": "ac_key === '' && !ac_other && !ac_amount && cargo.voice && String(ac_answer || '').trim() === '9' && !!callerTextNumber(cargo) ? '__text' : ac_key"
|
|
455
|
+
"value": "ac_key === '' && !ac_other && !ac_amount && cargo.voice && String(ac_answer || '').trim().replace(/[.,!?;:]+$/, '') === '9' && !!callerTextNumber(cargo) ? '__text' : ac_key"
|
|
456
456
|
},
|
|
457
457
|
{
|
|
458
458
|
"id": "ac_keypad_again",
|
|
@@ -629,7 +629,7 @@ export const FLOWS = [
|
|
|
629
629
|
"id": "ac_switch_to_text_key_again",
|
|
630
630
|
"type": "SET",
|
|
631
631
|
"variable": "ac_key",
|
|
632
|
-
"value": "ac_key === '' && !ac_other && !ac_amount && cargo.voice && String(ac_answer || '').trim() === '9' && !!callerTextNumber(cargo) ? '__text' : ac_key"
|
|
632
|
+
"value": "ac_key === '' && !ac_other && !ac_amount && cargo.voice && String(ac_answer || '').trim().replace(/[.,!?;:]+$/, '') === '9' && !!callerTextNumber(cargo) ? '__text' : ac_key"
|
|
633
633
|
},
|
|
634
634
|
{
|
|
635
635
|
"id": "ac_keypad_done",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;
|
|
1
|
+
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAueD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAnqBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA41B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -269,7 +269,9 @@ const collectInput = {
|
|
|
269
269
|
SET('ci_question', 'ci_q', "formInputPrompt(cargo,form,current,ci_spec,ci_d,'en')"),
|
|
270
270
|
SET('ci_question_es', 'ci_q_es', "formInputPrompt(cargo,form,current,ci_spec,ci_d,'es')"),
|
|
271
271
|
SIDE('ci_feedback_consumed', 'consumeFormFeedback(cargo,form,current)'),
|
|
272
|
-
|
|
272
|
+
// A grouped input (any_of: a plan option, an amount...) offers the ranker every choice its members declare (prod 2026-10-28:
|
|
273
|
+
// the pay offer offered none, so a caller's own words for a listed option were not understood).
|
|
274
|
+
SET('ci_own_choices', 'ci_choices', "ci_spec.choices_from && Array.isArray(ci_d[ci_spec.choices_from]) ? ci_d[ci_spec.choices_from] : (ci_spec.profile.kind === 'choice' && Array.isArray(ci_spec.profile.choices) ? ci_spec.profile.choices : (Array.isArray(ci_spec.any_of) ? ci_spec.any_of.reduce(function(all,m){return all.concat(m && m.profile && m.profile.kind === 'choice' && Array.isArray(m.profile.choices) ? m.profile.choices.filter(function(c,i,own){return !all.some(function(a){return String(a.key)===String(c.key);}) && own.findIndex(function(o){return String(o.key)===String(c.key);})===i;}) : []);},[]) : []))"),
|
|
273
275
|
SET('ci_keypad', 'ci_digits', 'ci_spec.digits || null'),
|
|
274
276
|
SET('ci_is_amount', 'ci_amount', "ci_spec.amount === 'marked' ? 'marked' : (ci_spec.amount === true || ci_spec.profile.kind === 'amount')"),
|
|
275
277
|
// FORM profile readers own normalization and compound matching. Never discard other
|
|
@@ -383,15 +385,24 @@ const readFormResponse = {
|
|
|
383
385
|
};
|
|
384
386
|
// An answer the field's reader could not read goes to the reply ranker, never straight to the host (owner 2026-10-26: a form
|
|
385
387
|
// gives up only for a real new intent). The ranker is offered the question's declared choices and the two escapes every
|
|
386
|
-
// question supports (owner 2026-10-28): a person, and on a call with a textable number, text.
|
|
387
|
-
// exact
|
|
388
|
+
// question supports (owner 2026-10-28): a person, and on a call with a textable number, text. Every declared choice is
|
|
389
|
+
// offered, `exact` ones too (exact is how WORDS match a choice, not a confirmation); only the YES of a firm (hard)
|
|
390
|
+
// confirmation never is. A choice without a label is described by its key and its worded phrases (keys are digits).
|
|
388
391
|
// `answers` (picks none): asked again ("Sorry, I didn't catch that."), counted as an attempt so max_attempts still bounds
|
|
389
392
|
// it; `abort` ends every flow; `new_request` goes to intent detection.
|
|
390
393
|
const rankedMiss = (id) => SIDE(id, "(cargo.forms[form].attempts=cargo.forms[form].attempts || {},cargo.forms[form].attempts[input]=Number(cargo.forms[form].attempts[input] || 0)+1,cargo.forms[form].feedback=cargo.forms[form].feedback || {},cargo.forms[form].feedback[input]='miss',true)");
|
|
391
|
-
|
|
394
|
+
// The ranker is offered only a person. Text is never offered: its label matched "continuar", "texto" and "enlace", and a
|
|
395
|
+
// call moved to text hangs up (prod 2026-10-28: 7 of 7 such picks were wrong). TEXTO and keypad 9 are read before it.
|
|
396
|
+
const ESCAPES_JS = "[{key:'__agent',label:'speak with a person (a live agent)'}]";
|
|
392
397
|
const rankedChoices = (id, from) => SET(id, 'ranked', `(${from}).concat(${ESCAPES_JS})`);
|
|
393
|
-
|
|
394
|
-
|
|
398
|
+
// A choice as the ranker is offered it: its key, and its label or its worded phrases.
|
|
399
|
+
const RANK_LABEL_JS = "function(c){var said=(Array.isArray(c.phrases) ? c.phrases : []).map(String).filter(function(p){return /[a-zà-ÿ]/i.test(p) && p!==String(c.key);}).slice(0,6);return String(c.label || (said.length ? c.key+' ('+said.join(', ')+')' : c.key));}";
|
|
400
|
+
// The choices of this form's OTHER inputs that the answer could also set (the word reader reads them all: 'LO VENCIDO' at
|
|
401
|
+
// the pay confirmation): the ranker is offered every choice the reader would accept (owner 2026-10-28). A confirmation
|
|
402
|
+
// input is answered only when asked, and a key already offered is not offered twice. Each carries its input, so a pick is
|
|
403
|
+
// read as that input's answer.
|
|
404
|
+
const otherChoicesJS = (asked, taken) => `(function(){var seen=(${taken}).map(function(c){return String(c.key);}).concat(['yes','no']);return (chain || []).filter(function(i){return i.form===form && i.name!==${asked} && !i.confirmation && i.profile && i.profile.kind==='choice' && Array.isArray(i.profile.choices);}).reduce(function(all,i){return all.concat(i.profile.choices.filter(function(c){var k=String(c.key);if(seen.indexOf(k)>=0)return false;seen.push(k);return true;}).map(function(c){return {key:String(c.key),label:(${RANK_LABEL_JS})(c),owner:{form:i.form,name:i.name}};}));},[]);})()`;
|
|
405
|
+
const escapeRoutes = (id) => [WHEN(`${id}_agent`, "reply && reply.key === '__agent'", FLOW(`${id}_agent_flow`, 'live-agent-requested', 'reboot'))];
|
|
395
406
|
const rankedDispatch = (id, reason) => ({ ...yieldDispatch(id, 'form', `'${reason}'`), onResponse: 'continue', responseContext: 'Current question: {{asked}}', responseChoices: 'ranked', responseVariable: 'reply' });
|
|
396
407
|
const readFormReply = {
|
|
397
408
|
id: 'read-form-reply', name: 'ReadFormReply', version: '1.1.0', scope: 'isolated', description: 'Rank an unreadable answer against its question: a request for a person or for text is taken; an answer is asked again; a withdrawal or a new request leaves with saved form progress.',
|
|
@@ -404,37 +415,45 @@ const readFormReply = {
|
|
|
404
415
|
rankedMiss('reply_miss')]
|
|
405
416
|
};
|
|
406
417
|
const readFormChoice = {
|
|
407
|
-
id: 'read-form-choice', name: 'ReadFormChoice', version: '1.
|
|
418
|
+
id: 'read-form-choice', name: 'ReadFormChoice', version: '1.3.0', scope: 'isolated', description: 'Rank an unreadable answer to a choice question and take the offered option it picks, or a request for a person or for text; otherwise ask again. A withdrawal or a new request leaves with saved form progress.',
|
|
408
419
|
parameters: [{ name: 'form', type: 'string' }, { name: 'input', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }, { name: 'choices', type: 'array' }, { name: 'firm', type: 'string' }],
|
|
409
|
-
variables: vars({ question: '', question_es: '', choices: [], firm: '', asked: '', ranked: [], reply: null, picked: '', chain: null, reading: null }),
|
|
420
|
+
variables: vars({ question: '', question_es: '', choices: [], firm: '', asked: '', ranked: [], reply: null, picked: '', chain: null, reading: null, others: [], owner: null }),
|
|
410
421
|
steps: [SET('choice_asked', 'asked', "language === 'es' && question_es ? question_es : question"),
|
|
411
|
-
|
|
422
|
+
SET('choice_chain', 'chain', 'formInputChain(cargo.forms, form, cargo)'),
|
|
423
|
+
SET('choice_others', 'others', otherChoicesJS('input', 'choices || []')),
|
|
424
|
+
rankedChoices('choice_ranked', `(choices || []).filter(function(c,i,all){return !(firm==='firm' && String(c.key)==='yes') && all.findIndex(function(o){return String(o.key)===String(c.key);})===i;}).map(function(c){return {key:String(c.key),label:(${RANK_LABEL_JS})(c)};}).concat(others.map(function(c){return {key:c.key,label:c.label};}))`),
|
|
412
425
|
rankedDispatch('choice_dispatch', 'input_not_matched'),
|
|
413
426
|
...escapeRoutes('choice_escape'),
|
|
414
|
-
SET('choice_picked', 'picked', "(function(){var k=String(reply && reply.key || '');var c=(choices || []).filter(function(c){return String(c.key)===k;})[0];return c ? String(c.key) : '';})()"),
|
|
427
|
+
SET('choice_picked', 'picked', "(function(){var k=String(reply && reply.key || '');var c=(choices || []).filter(function(c){return String(c.key)===k;})[0] || others.filter(function(c){return c.key===k;})[0];return c ? String(c.key) : '';})()"),
|
|
428
|
+
SET('choice_owner', 'owner', "(others.filter(function(c){return c.key===picked && !(choices || []).some(function(own){return String(own.key)===picked;});})[0] || {}).owner || null"),
|
|
415
429
|
WHEN('choice_none', '!picked', rankedMiss('choice_miss')),
|
|
416
430
|
WHEN('choice_none_end', '!picked', END('choice_none_done')),
|
|
417
431
|
SIDE('choice_take', "(cargo.choice=picked,cargo.choiceText=picked,true)"),
|
|
418
|
-
SET('
|
|
419
|
-
SET('choice_read', 'reading', "enrichFormMatches(readFormAnswer(picked, chain, { form: form, name: input }, picked, []), chain, form, [input], input)"),
|
|
432
|
+
SET('choice_read', 'reading', "owner ? enrichFormMatches(readFormAnswer(picked, chain, owner, picked, []), chain, form, [owner.name], owner.name) : enrichFormMatches(readFormAnswer(picked, chain, { form: form, name: input }, picked, []), chain, form, [input], input)"),
|
|
420
433
|
WHEN('choice_unread', '!reading.hits.length', rankedMiss('choice_unread_miss')),
|
|
421
434
|
WHEN('choice_unread_end', '!reading.hits.length', END('choice_unread_done')),
|
|
422
435
|
SIDE('choice_clear', 'clearFormMisses(cargo,form,input)'),
|
|
423
436
|
FLOW('choice_accept', 'collect-input-accept', 'call', { hits: '{{reading.hits}}' })]
|
|
424
437
|
};
|
|
425
438
|
// A confirmation's unread answer goes to the reply ranker with the confirmation question (owner 2026-10-27), never
|
|
426
|
-
// straight to the host. A form's confirmation is exact (it authorizes the form's action), so the ranker is offered NO
|
|
427
|
-
// and
|
|
428
|
-
// flow; `new_request` goes to intent detection.
|
|
439
|
+
// straight to the host. A form's confirmation is exact (it authorizes the form's action), so the ranker is offered NO, the
|
|
440
|
+
// form's other choices (a correction) and a person, never YES. Result: cargo.confirmRanked = 'no' | 'changed' | 'none'
|
|
441
|
+
// (ConfirmForm reads it); `abort` ends every flow; `new_request` goes to intent detection.
|
|
429
442
|
const readConfirmReply = {
|
|
430
|
-
id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.
|
|
443
|
+
id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.2.0', scope: 'isolated', description: 'Rank an unreadable answer to a confirmation: it declines (NO), picks another of the form\'s choices (a correction, read as that input\'s answer), asks for a person, or is asked again; a withdrawal or a new request leaves with saved form progress. Never confirms.',
|
|
431
444
|
parameters: [{ name: 'form', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }],
|
|
432
|
-
variables: vars({ question: '', question_es: '', asked: '', ranked: [], reply: null }),
|
|
445
|
+
variables: vars({ question: '', question_es: '', asked: '', ranked: [], reply: null, chain: null, others: [], picked: null, reading: null }),
|
|
433
446
|
steps: [SET('confirm_asked', 'asked', "language === 'es' && question_es ? question_es : question"),
|
|
434
|
-
|
|
447
|
+
SET('confirm_chain', 'chain', 'formInputChain(cargo.forms, form, cargo)'),
|
|
448
|
+
SET('confirm_others', 'others', otherChoicesJS("''", '[]')),
|
|
449
|
+
rankedChoices('confirm_ranked_choices', "[{key:'no',label:'no: declines or refuses what the question asks to confirm'}].concat(others.map(function(c){return {key:c.key,label:c.label};}))"),
|
|
435
450
|
rankedDispatch('confirm_dispatch', 'confirmation_not_matched'),
|
|
436
451
|
...escapeRoutes('confirm_escape'),
|
|
437
|
-
|
|
452
|
+
// a correction picked by the ranker is read as its input's answer: the form changes and is confirmed again, never confirmed
|
|
453
|
+
SET('confirm_picked', 'picked', "others.filter(function(c){return reply && c.key===reply.key;})[0] || null"),
|
|
454
|
+
SET('confirm_read', 'reading', "picked ? enrichFormMatches(readFormAnswer(picked.key, chain, picked.owner, picked.key, []), chain, form, [picked.owner.name], picked.owner.name) : null"),
|
|
455
|
+
WHEN('confirm_change', 'reading && reading.hits.length', FLOW('confirm_change_accept', 'collect-input-accept', 'call', { hits: '{{reading.hits}}' })),
|
|
456
|
+
SIDE('confirm_ranked', "(cargo.confirmRanked=reading && reading.hits.length ? 'changed' : reply && reply.key==='no' ? 'no' : 'none',true)")]
|
|
438
457
|
};
|
|
439
458
|
const collectContextualAnswer = formFlow({ id: 'collect-contextual-answer', name: 'CollectContextualAnswer', description: 'Collect one open-ended answer using contextual DISPATCH and preserve the caller wording.',
|
|
440
459
|
parameters: [...['question', 'question_es', 'description', 'context', 'context_es'].map(name => ({ name, type: 'string' })), { name: 'choices', type: 'array' }, { name: 'digits', type: 'object' }],
|
|
@@ -257,12 +257,12 @@ const retry = worker('generic-retry-with-options', 'GenericRetryWithOptions', 'C
|
|
|
257
257
|
set('retry_question', 'retry_question', "error_message+' '+(question || 'Would you like to try again?')+' To retry '+(cargo.voice?'Press 1 or ':'')+cargo.verb+' YES. '+(callerTextNumber(cargo)?'To switch to text press 9 or '+cargo.verb+' TEXT. ':'')+'To abort '+(cargo.voice?'press the asterisk key or ':'')+cargo.verb+' EXIT.'"),
|
|
258
258
|
set('retry_question_es', 'retry_question_es', "error_message_es+' '+(question_es || '¿Le gustaría intentar nuevamente?')+' Para reintentar '+(cargo.voice?'Presione 1 o ':'')+cargo.verb_es+' SÍ. '+(callerTextNumber(cargo)?'Para cambiar a texto presione 9 o '+cargo.verb_es+' TEXTO. ':'')+'Para salir '+(cargo.voice?'presione la tecla de asterisco o ':'')+cargo.verb_es+' SALIR.'"),
|
|
259
259
|
call('retry_collect', 'collect-service-answer', { profiles: '{{retry_profiles}}', question: '{{retry_question}}', question_es: '{{retry_question_es}}', keypad: { min: 1, max: 1 }, parent_form: '{{parent_form}}' }),
|
|
260
|
-
set('retry_answer', 'user_choice', 'String(cargo.serviceAnswer.raw || "")'),
|
|
260
|
+
set('retry_answer', 'user_choice', 'String(cargo.serviceAnswer.raw || "").trim().replace(/[.,!?;:]+$/,"")'),
|
|
261
261
|
set('retry_default_cancel', 'cancel_flow', "cancel_flow || 'contact-support'"),
|
|
262
262
|
when('retry_unknown_key_question', '/^\\s*[2-8]\\s*$/.test(user_choice)', set('retry_unknown_question', 'retry_question', "'Sorry, that is not an option. To retry '+(cargo.voice?'Press 1 or ':'')+cargo.verb+' YES. '+(callerTextNumber(cargo)?'To switch to text press 9 or '+cargo.verb+' TEXT. ':'')+(cargo.voice?'For an agent press 0. ':'')+'To abort '+(cargo.voice?'press the asterisk key or ':'')+cargo.verb+' EXIT.'")),
|
|
263
263
|
when('retry_unknown_key_question_es', '/^\\s*[2-8]\\s*$/.test(user_choice)', set('retry_unknown_question_es', 'retry_question_es', "'Lo siento, esa no es una opción. Para reintentar '+(cargo.voice?'Presione 1 o ':'')+cargo.verb_es+' SÍ. '+(callerTextNumber(cargo)?'Para cambiar a texto presione 9 o '+cargo.verb_es+' TEXTO. ':'')+(cargo.voice?'Para un agente presione 0. ':'')+'Para salir '+(cargo.voice?'presione la tecla de asterisco o ':'')+cargo.verb_es+' SALIR.'")),
|
|
264
264
|
when('retry_unknown_key', '/^\\s*[2-8]\\s*$/.test(user_choice)', call('retry_bad_key_collect', 'collect-service-answer', { profiles: '{{retry_profiles}}', question: '{{retry_question}}', question_es: '{{retry_question_es}}', keypad: { min: 1, max: 1 }, parent_form: '{{parent_form}}' })),
|
|
265
|
-
set('retry_latest_answer', 'user_choice', 'String(cargo.serviceAnswer.raw || "")'), set('retry_capture', 'smart_capture_result', 'capture_patterns && capture_patterns.length ? normalizeAndFindCapture(user_choice,capture_patterns) : null'),
|
|
265
|
+
set('retry_latest_answer', 'user_choice', 'String(cargo.serviceAnswer.raw || "").trim().replace(/[.,!?;:]+$/,"")'), set('retry_capture', 'smart_capture_result', 'capture_patterns && capture_patterns.length ? normalizeAndFindCapture(user_choice,capture_patterns) : null'),
|
|
266
266
|
cases('retry_capture_route', { 'condition: smart_capture_result && retry_call_type === "replace"': call('retry_captured_replace', '{{retry_flow}}', { cell_number: '', email: '', '{{smart_capture_result.variable}}': '{{smart_capture_result.value}}', retry_flow: '{{retry_flow}}', cancel_flow: '{{cancel_flow}}', retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'replace'), 'condition: smart_capture_result': call('retry_captured_reboot', '{{retry_flow}}', { '{{smart_capture_result.variable}}': '{{smart_capture_result.value}}', retry_flow: '{{retry_flow}}', cancel_flow: '{{cancel_flow}}', retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'reboot'), default: effect('retry_no_capture', 'true') }),
|
|
267
267
|
set('retry_typed_code', 'normalized_otp_code', "allow_code_entry && user_choice.replace(/\\D/g,'').length === 6 ? user_choice.replace(/\\D/g,'') : ''"), when('retry_validate_code', '!!normalized_otp_code', call('retry_code_validate', 'validate-otp-code')),
|
|
268
268
|
cases('retry_decision', { 'condition: normalized_otp_code && cargo.otpVerified === true': end('retry_verified'), "condition: (user_choice.trim() === '1' || isAnswer(user_choice,'yes',retry_yes_words)) && retry_call_type === 'replace'": call('retry_yes_replace', '{{retry_flow}}', { retrying: true, cell_number: '', email: '', contact_options: null, parent_form: '{{parent_form}}' }, 'replace'), "condition: user_choice.trim() === '1' || isAnswer(user_choice,'yes',retry_yes_words)": call('retry_yes_reboot', '{{retry_flow}}', { retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'reboot'), "condition: user_choice.trim() === '9' || isAnswer(user_choice,'text')": call('retry_text', 'switch-to-text', undefined, 'reboot'), "condition: isAgentRequest(user_choice)": call('retry_agent', 'live-agent-requested', undefined, 'reboot'), "condition: !user_choice.trim() || /^\\d$/.test(user_choice.trim()) || isExitRequest(user_choice)": call('retry_cancel', '{{cancel_flow}}', undefined, 'reboot'), default: { id: 'retry_dispatch', type: 'DISPATCH', outcome: '{{caller_outcome}}', reason: '{{caller_reason}}' } })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contact.d.ts","sourceRoot":"","sources":["../../src/helpers/contact.js"],"names":[],"mappings":"AAiBA;;;;;;;;GAQG;AACH;;
|
|
1
|
+
{"version":3,"file":"contact.d.ts","sourceRoot":"","sources":["../../src/helpers/contact.js"],"names":[],"mappings":"AAiBA;;;;;;;;GAQG;AACH;;WAiCC;AAID;;;;;GAKG;AACH,0CAWC;AAED;;;;;GAKG;AACH,0DAgBC;AAED,qHAAqH;AACrH,2DAIC;AAED;;;;GAIG;AACH;;;SAmBC;AAED;;;GAGG;AACH,sDAOC;AAED;;;;;GAKG;AACH,yEAUC;AAQD;;;;;GAKG;AACH,qDAIC;AAED,mHAAmH;AACnH,oDAIC"}
|
package/dist/helpers/contact.js
CHANGED
|
@@ -40,7 +40,10 @@ export function readEmail(text, { literal = false } = {}) {
|
|
|
40
40
|
return '';
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
43
|
-
|
|
43
|
+
// a spelled domain arrives in pieces ("gm a i l.com", "y a h o o . c o m"): join them up to its top-level domain
|
|
44
|
+
const after = spoken.slice(at + 1).trim();
|
|
45
|
+
const spelled = after.match(/^[a-z0-9-]+(?:\s+[a-z0-9-]+){1,12}?(?:\s*\.\s*(?:[a-z]{2,}|[a-z](?:\s+[a-z]){1,2})\b){1,2}/i);
|
|
46
|
+
const domain = (spelled ? spelled[0].replace(/\s+/g, '') : (after.match(/^[^\s@]+/) || [''])[0]).replace(/[.,;:!?]+$/, '');
|
|
44
47
|
const pieces = spoken.slice(0, at).replace(/@/g, ' ').normalize('NFD').replace(/[\u0300-\u036f]/g, '').split(/[\s,;:!?]+/).map((w) => w.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, '')).filter(Boolean);
|
|
45
48
|
const local = [];
|
|
46
49
|
for (let k = pieces.length - 1; k >= 0 && local.join('').length < 64; k--) {
|
|
@@ -26,6 +26,11 @@ export declare const CHOICE_PREFIX = "choice:";
|
|
|
26
26
|
export declare const REPLY_DEFAULT: ReplyOption;
|
|
27
27
|
/** A noul at or above this is a yes. */
|
|
28
28
|
export declare const REPLY_YES_THRESHOLD = 0.5;
|
|
29
|
+
/**
|
|
30
|
+
* The cut an option must clear, by what a wrong pick costs: an option not listed here takes REPLY_YES_THRESHOLD. Set
|
|
31
|
+
* from labelled prod replies (owner 2026-10-28), never guessed.
|
|
32
|
+
*/
|
|
33
|
+
export declare const REPLY_CUTS: Partial<Record<ReplyOption, number>>;
|
|
29
34
|
export interface ReplyTurn {
|
|
30
35
|
role: 'user' | 'assistant';
|
|
31
36
|
content: string;
|
|
@@ -60,13 +65,15 @@ export declare function decodeReplyRankLlm(text: string, request: ReplyRankReque
|
|
|
60
65
|
option: ReplyOption;
|
|
61
66
|
};
|
|
62
67
|
/**
|
|
63
|
-
* System One's answers as the canonical reply: the highest-rated option
|
|
64
|
-
*
|
|
68
|
+
* System One's answers as the canonical reply: the highest-rated option that clears its own cut (REPLY_CUTS, else the
|
|
69
|
+
* threshold), a cleared choice before `answers` and `new_request`; when none does, the default (`answers`: asked again at a choice question, the answer at an open one), with
|
|
70
|
+
* every option's probability (for logs). Throws if any question is unanswered.
|
|
65
71
|
*/
|
|
66
72
|
export declare function decodeReplyRankSystemOne(answers: Record<string, {
|
|
67
73
|
noul?: unknown;
|
|
68
|
-
} | undefined> | undefined, request: ReplyRankRequest, { threshold }?: {
|
|
69
|
-
threshold?: number
|
|
74
|
+
} | undefined> | undefined, request: ReplyRankRequest, { threshold, cuts }?: {
|
|
75
|
+
threshold?: number;
|
|
76
|
+
cuts?: Partial<Record<ReplyOption, number>>;
|
|
70
77
|
}): {
|
|
71
78
|
option: ReplyOption;
|
|
72
79
|
scores: Record<string, number>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reply-rank.d.ts","sourceRoot":"","sources":["../../src/host/reply-rank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,aAAa,8CAA+C,CAAC;AAC1E,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,OAAO,aAAa,CAAC,MAAM,CAAC,GAAG,UAAU,MAAM,EAAE,CAAC;AAC5E,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAC3D,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,gGAAgG;AAChG,eAAO,MAAM,aAAa,EAAE,WAAuB,CAAC;AACpD,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"reply-rank.d.ts","sourceRoot":"","sources":["../../src/host/reply-rank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,aAAa,8CAA+C,CAAC;AAC1E,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,OAAO,aAAa,CAAC,MAAM,CAAC,GAAG,UAAU,MAAM,EAAE,CAAC;AAC5E,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAC3D,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,gGAAgG;AAChG,eAAO,MAAM,aAAa,EAAE,WAAuB,CAAC;AACpD,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAO3D,CAAC;AAEF,MAAM,WAAW,SAAS;IAAG,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,SAAS,EAAE,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IACvE,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,WAAW,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;CACjI;AAwCD,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,WAAW,EAAO,GAAG,gBAAgB,CA+CxI;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,CAMnG;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAAC,GAAG,SAAS,EACnE,OAAO,EAAE,gBAAgB,EACzB,EAAE,SAA+B,EAAE,IAAiB,EAAE,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;CAAO,GAC/H;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAiB5E;AAED,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,gBAAgB,CAGhF"}
|
package/dist/host/reply-rank.js
CHANGED
|
@@ -19,6 +19,18 @@ export const CHOICE_PREFIX = 'choice:';
|
|
|
19
19
|
export const REPLY_DEFAULT = 'answers';
|
|
20
20
|
/** A noul at or above this is a yes. */
|
|
21
21
|
export const REPLY_YES_THRESHOLD = 0.5;
|
|
22
|
+
/**
|
|
23
|
+
* The cut an option must clear, by what a wrong pick costs: an option not listed here takes REPLY_YES_THRESHOLD. Set
|
|
24
|
+
* from labelled prod replies (owner 2026-10-28), never guessed.
|
|
25
|
+
*/
|
|
26
|
+
export const REPLY_CUTS = {
|
|
27
|
+
// a NO cancels what the caller was confirming. Measured 2026-10-28: replies that are not a NO ("imposible", "no es un")
|
|
28
|
+
// rated 0.51-0.63, real NOs ("No me interesa", "No de verdad", "mejor no") 0.67-0.95
|
|
29
|
+
'choice:no': 0.65,
|
|
30
|
+
// a new request leaves the question unanswered. Unclear replies at a question ("la curazán" at a confirmation, "Es que
|
|
31
|
+
// mi tarjeta se cambió" at the pay menu) rated 0.47-0.58 and vary ±0.04 between calls; they are asked again
|
|
32
|
+
new_request: 0.6,
|
|
33
|
+
};
|
|
22
34
|
// ── the criteria: written once, rendered for both executors ─────────────────────────────────────────
|
|
23
35
|
const CRITERIA = {
|
|
24
36
|
answers: {
|
|
@@ -81,7 +93,12 @@ export function buildReplyRank(input, conversation, question, choices = []) {
|
|
|
81
93
|
].join('\n');
|
|
82
94
|
const questions = {};
|
|
83
95
|
const index = [];
|
|
84
|
-
|
|
96
|
+
// With declared choices, `answers` ("picks none of them") is not a question System One answers: it competes with the
|
|
97
|
+
// choices it excludes. It is what is left when no option clears its cut (owner 2026-10-28). A text model still has it.
|
|
98
|
+
// An escape (`__agent`) is not the question's own choice: a question that offers only escapes is an open question,
|
|
99
|
+
// where `answers` is the reply (prod 2026-10-28: a city named at "check nearby stores" went to intent detection).
|
|
100
|
+
const ownChoices = choices.some(c => !c.key.startsWith('__'));
|
|
101
|
+
options.filter(o => !(o === 'answers' && ownChoices)).forEach((o, i) => {
|
|
85
102
|
const id = `r${i}`;
|
|
86
103
|
questions[id] = { type: 'noul', criteria: criteriaOf(o, choices), instructions: { option: o, question: `Does the latest message do what \`${o}\` describes?` } };
|
|
87
104
|
index.push({ id, option: o });
|
|
@@ -110,10 +127,11 @@ export function decodeReplyRankLlm(text, request) {
|
|
|
110
127
|
return { option };
|
|
111
128
|
}
|
|
112
129
|
/**
|
|
113
|
-
* System One's answers as the canonical reply: the highest-rated option
|
|
114
|
-
*
|
|
130
|
+
* System One's answers as the canonical reply: the highest-rated option that clears its own cut (REPLY_CUTS, else the
|
|
131
|
+
* threshold), a cleared choice before `answers` and `new_request`; when none does, the default (`answers`: asked again at a choice question, the answer at an open one), with
|
|
132
|
+
* every option's probability (for logs). Throws if any question is unanswered.
|
|
115
133
|
*/
|
|
116
|
-
export function decodeReplyRankSystemOne(answers, request, { threshold = REPLY_YES_THRESHOLD } = {}) {
|
|
134
|
+
export function decodeReplyRankSystemOne(answers, request, { threshold = REPLY_YES_THRESHOLD, cuts = REPLY_CUTS } = {}) {
|
|
117
135
|
const scores = {};
|
|
118
136
|
for (const q of request.systemOne.index) {
|
|
119
137
|
const noul = Number(answers?.[q.id]?.noul);
|
|
@@ -121,8 +139,12 @@ export function decodeReplyRankSystemOne(answers, request, { threshold = REPLY_Y
|
|
|
121
139
|
throw new Error(`System One: question ${q.id} unanswered`);
|
|
122
140
|
scores[q.option] = noul;
|
|
123
141
|
}
|
|
124
|
-
const
|
|
125
|
-
|
|
142
|
+
const cleared = request.systemOne.index.map(q => q.option).filter(o => scores[o] >= (cuts[o] ?? threshold)).sort((a, b) => scores[b] - scores[a]);
|
|
143
|
+
// A cleared choice is taken over `answers` and `new_request` (owner 2026-10-28: a request that names an offered option
|
|
144
|
+
// is that option, "Representative." is the agent); only `abort` keeps its rank.
|
|
145
|
+
const choice = cleared.find(o => o.startsWith(CHOICE_PREFIX));
|
|
146
|
+
const best = choice && (cleared[0] === 'answers' || cleared[0] === 'new_request') ? choice : cleared[0];
|
|
147
|
+
if (best) {
|
|
126
148
|
return { option: best, scores, fallback: false };
|
|
127
149
|
}
|
|
128
150
|
else {
|
package/dist/runtime-identity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const JSFE_RUNTIME_FINGERPRINT = "sha256:
|
|
1
|
+
export const JSFE_RUNTIME_FINGERPRINT = "sha256:9fca8fe719c5610d1260ece2cc393107d549667095f71bcaa62427a894f0ba7b";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsfe",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
4
4
|
"description": "TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ron Pinkas",
|