@superpanel-sdk/runtime 0.2.2 → 0.2.3
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/builder/compile/compile-agenda.js +9 -4
- package/dist/cli/serve/index.js +60 -12
- package/dist/compute-host/index.d.ts +2 -0
- package/dist/compute-host/index.js +1 -0
- package/dist/compute-host/read-only-cache.d.ts +20 -0
- package/dist/compute-host/read-only-cache.js +61 -0
- package/dist/compute-host/source-compute-host.js +7 -2
- package/dist/core/context/types.d.ts +19 -1
- package/dist/core/functions/define/custom.d.ts +1 -1
- package/dist/core/functions/types.d.ts +4 -4
- package/dist/core/slot/index.d.ts +1 -1
- package/dist/core/slot/index.js +1 -1
- package/dist/core/slot/types.d.ts +2 -0
- package/dist/core/slot/write.d.ts +8 -1
- package/dist/core/slot/write.js +145 -38
- package/dist/core/stage/builders.d.ts +23 -5
- package/dist/core/stage/builders.js +3 -0
- package/dist/core/stage/drive.d.ts +1 -1
- package/dist/core/stage/drive.js +9 -8
- package/dist/core/stage/flatten.js +13 -11
- package/dist/core/stage/index.d.ts +8 -4
- package/dist/core/stage/index.js +5 -2
- package/dist/core/stage/interrupts.d.ts +28 -0
- package/dist/core/stage/interrupts.js +120 -0
- package/dist/core/stage/ivr-fields.d.ts +9 -0
- package/dist/core/stage/ivr-fields.js +75 -0
- package/dist/core/stage/transfer-fields.d.ts +6 -1
- package/dist/core/stage/transfer-fields.js +44 -3
- package/dist/core/stage/transfer-spec.d.ts +48 -0
- package/dist/core/stage/transfer-spec.js +113 -0
- package/dist/core/stage/types.d.ts +45 -14
- package/dist/core/stage/validate.js +105 -1
- package/dist/core/stage/walk.d.ts +5 -0
- package/dist/core/stage/walk.js +33 -10
- package/dist/fn-context/rpc/trpc-client.js +3 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/voice-service/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { idField } from '../slot/schema.js';
|
|
2
|
+
import { isResolved } from '../slot/field.js';
|
|
3
|
+
import { getSingleField } from '../slot/snapshot.js';
|
|
4
|
+
import { getCompiledTable } from '../slot/compile-schema.js';
|
|
5
|
+
import { formatFieldPath } from '../slot/path.js';
|
|
6
|
+
import { transferFieldKey } from './transfer-fields.js';
|
|
7
|
+
const RESULT_ENUM = ['collected', 'failed'];
|
|
8
|
+
function ivrFieldCore(label) {
|
|
9
|
+
return {
|
|
10
|
+
derived: false,
|
|
11
|
+
label,
|
|
12
|
+
dataType: 'string',
|
|
13
|
+
source: 'system',
|
|
14
|
+
extraction: 'Platform IVR result',
|
|
15
|
+
enum: [...RESULT_ENUM],
|
|
16
|
+
nullable: true,
|
|
17
|
+
identifier: null,
|
|
18
|
+
ref: null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function ivrResultField(nodeId) {
|
|
22
|
+
return formatFieldPath('session', `ivr_${transferFieldKey(nodeId)}_result`);
|
|
23
|
+
}
|
|
24
|
+
export function ivrAttempted(item, snapshot) {
|
|
25
|
+
return isResolved(getSingleField(snapshot, item.resultField));
|
|
26
|
+
}
|
|
27
|
+
export function collectIvrBlocks(blocks, nodes = []) {
|
|
28
|
+
const out = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const block of Object.values(blocks)) {
|
|
31
|
+
if (block.kind === 'ivr' && !seen.has(block.id)) {
|
|
32
|
+
seen.add(block.id);
|
|
33
|
+
out.push(block);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
visit(nodes);
|
|
37
|
+
return out;
|
|
38
|
+
function visit(list) {
|
|
39
|
+
for (const node of list) {
|
|
40
|
+
if (node.kind === 'ivr' && !seen.has(node.id)) {
|
|
41
|
+
seen.add(node.id);
|
|
42
|
+
out.push(node);
|
|
43
|
+
}
|
|
44
|
+
else if (node.kind === 'section' || node.kind === 'branch') {
|
|
45
|
+
visit(node.children);
|
|
46
|
+
}
|
|
47
|
+
else if (node.kind === 'switchOn') {
|
|
48
|
+
for (const children of Object.values(node.cases))
|
|
49
|
+
visit(children);
|
|
50
|
+
if (node.otherwise)
|
|
51
|
+
visit(node.otherwise);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function registerIvrSessionFields(schema, ivrIds) {
|
|
57
|
+
const ids = [...new Set(ivrIds)];
|
|
58
|
+
if (ids.length === 0)
|
|
59
|
+
return schema;
|
|
60
|
+
const session = getCompiledTable(schema, 'session');
|
|
61
|
+
if (!session)
|
|
62
|
+
return schema;
|
|
63
|
+
const fields = { ...session.fields };
|
|
64
|
+
if (!fields.id)
|
|
65
|
+
fields.id = { ...idField, ref: null };
|
|
66
|
+
for (const id of ids) {
|
|
67
|
+
const name = `ivr_${transferFieldKey(id)}_result`;
|
|
68
|
+
if (!fields[name]) {
|
|
69
|
+
fields[name] = ivrFieldCore(`IVR ${id} result`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const nextSession = { ...session, fields };
|
|
73
|
+
return { ...schema, session: nextSession };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=ivr-fields.js.map
|
|
@@ -5,10 +5,15 @@ import type { AgendaBlock, AuthoringNode, TransferBlock } from './types.js';
|
|
|
5
5
|
export declare function transferFieldKey(nodeId: string): string;
|
|
6
6
|
export declare function transferResultField(nodeId: string): FieldPath;
|
|
7
7
|
export declare const LAST_TRANSFER_RESULT_FIELD: `${string}.${string}`;
|
|
8
|
+
export declare const CASE_TRANSFER_ATTEMPT_COUNT: `${string}.${string}`;
|
|
9
|
+
export declare const CASE_LAST_TRANSFER_RESULT: `${string}.${string}`;
|
|
10
|
+
export declare const CASE_LAST_TRANSFER_AT: `${string}.${string}`;
|
|
8
11
|
export declare function collectTransferBlocks(blocks: Record<string, AgendaBlock>, nodes?: AuthoringNode[]): TransferBlock[];
|
|
9
12
|
/**
|
|
10
13
|
* Register per-node `session.transfer_<id>_result` and `session.last_transfer_result`.
|
|
11
14
|
* Existing session fields with the same name are left untouched.
|
|
12
15
|
*/
|
|
13
|
-
export declare function registerTransferSessionFields(schema: CompiledSlotSchema, transferIds: Iterable<string
|
|
16
|
+
export declare function registerTransferSessionFields(schema: CompiledSlotSchema, transferIds: Iterable<string>, options?: {
|
|
17
|
+
lastResult?: boolean;
|
|
18
|
+
}): CompiledSlotSchema;
|
|
14
19
|
//# sourceMappingURL=transfer-fields.d.ts.map
|
|
@@ -24,6 +24,9 @@ export function transferResultField(nodeId) {
|
|
|
24
24
|
return formatFieldPath('session', `transfer_${transferFieldKey(nodeId)}_result`);
|
|
25
25
|
}
|
|
26
26
|
export const LAST_TRANSFER_RESULT_FIELD = formatFieldPath('session', 'last_transfer_result');
|
|
27
|
+
export const CASE_TRANSFER_ATTEMPT_COUNT = formatFieldPath('case_file', 'transfer_attempt_count');
|
|
28
|
+
export const CASE_LAST_TRANSFER_RESULT = formatFieldPath('case_file', 'last_transfer_result');
|
|
29
|
+
export const CASE_LAST_TRANSFER_AT = formatFieldPath('case_file', 'last_transfer_at');
|
|
27
30
|
export function collectTransferBlocks(blocks, nodes = []) {
|
|
28
31
|
const out = [];
|
|
29
32
|
const seen = new Set();
|
|
@@ -57,9 +60,9 @@ export function collectTransferBlocks(blocks, nodes = []) {
|
|
|
57
60
|
* Register per-node `session.transfer_<id>_result` and `session.last_transfer_result`.
|
|
58
61
|
* Existing session fields with the same name are left untouched.
|
|
59
62
|
*/
|
|
60
|
-
export function registerTransferSessionFields(schema, transferIds) {
|
|
63
|
+
export function registerTransferSessionFields(schema, transferIds, options) {
|
|
61
64
|
const ids = [...new Set(transferIds)];
|
|
62
|
-
if (ids.length === 0)
|
|
65
|
+
if (ids.length === 0 && !options?.lastResult)
|
|
63
66
|
return schema;
|
|
64
67
|
const session = getCompiledTable(schema, 'session');
|
|
65
68
|
if (!session)
|
|
@@ -73,16 +76,54 @@ export function registerTransferSessionFields(schema, transferIds) {
|
|
|
73
76
|
fields[name] = transferFieldCore(`Transfer ${id} result`);
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
|
-
if (!fields.last_transfer_result) {
|
|
79
|
+
if ((ids.length > 0 || options?.lastResult) && !fields.last_transfer_result) {
|
|
77
80
|
fields.last_transfer_result = transferFieldCore('Last transfer result');
|
|
78
81
|
}
|
|
79
82
|
const nextSession = {
|
|
80
83
|
...session,
|
|
81
84
|
fields,
|
|
82
85
|
};
|
|
86
|
+
const caseFile = getCompiledTable(schema, 'case_file');
|
|
87
|
+
let nextCase = caseFile;
|
|
88
|
+
if (caseFile && (ids.length > 0 || options?.lastResult)) {
|
|
89
|
+
const caseFields = { ...caseFile.fields };
|
|
90
|
+
if (!caseFields.id)
|
|
91
|
+
caseFields.id = { ...idField, ref: null };
|
|
92
|
+
if (!caseFields.transfer_attempt_count) {
|
|
93
|
+
caseFields.transfer_attempt_count = {
|
|
94
|
+
derived: false,
|
|
95
|
+
label: 'Transfer attempt count',
|
|
96
|
+
dataType: 'number',
|
|
97
|
+
source: 'system',
|
|
98
|
+
extraction: 'Platform transfer attempt count',
|
|
99
|
+
enum: null,
|
|
100
|
+
nullable: true,
|
|
101
|
+
identifier: null,
|
|
102
|
+
ref: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (!caseFields.last_transfer_result) {
|
|
106
|
+
caseFields.last_transfer_result = transferFieldCore('Case last transfer result');
|
|
107
|
+
}
|
|
108
|
+
if (!caseFields.last_transfer_at) {
|
|
109
|
+
caseFields.last_transfer_at = {
|
|
110
|
+
derived: false,
|
|
111
|
+
label: 'Last transfer at',
|
|
112
|
+
dataType: 'datetime',
|
|
113
|
+
source: 'system',
|
|
114
|
+
extraction: 'Platform last transfer time',
|
|
115
|
+
enum: null,
|
|
116
|
+
nullable: true,
|
|
117
|
+
identifier: null,
|
|
118
|
+
ref: null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
nextCase = { ...caseFile, fields: caseFields };
|
|
122
|
+
}
|
|
83
123
|
return {
|
|
84
124
|
...schema,
|
|
85
125
|
session: nextSession,
|
|
126
|
+
...(nextCase ? { case_file: nextCase } : {}),
|
|
86
127
|
};
|
|
87
128
|
}
|
|
88
129
|
//# sourceMappingURL=transfer-fields.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FieldPath } from '../slot/path.js';
|
|
2
|
+
import type { RuntimeSlotsSnapshot, ValidationIssue } from '../slot/types.js';
|
|
3
|
+
export type TransferMode = 'cold' | 'warm' | 'async-warm';
|
|
4
|
+
export type TransferCallerIdShown = 'client' | 'service';
|
|
5
|
+
export type TransferConfirmMethod = 'keypad' | 'llm';
|
|
6
|
+
export type TransferConfirmKey = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '*' | '#';
|
|
7
|
+
export type TransferSay = string | Record<string, string>;
|
|
8
|
+
export type TransferFallbackAction = 'end-call' | 'restore-normal-call-state';
|
|
9
|
+
/** mars-voice-service `TransferCallArgs`. String fields may contain `{{table.field}}`. */
|
|
10
|
+
export type TransferArguments = {
|
|
11
|
+
sendDigits?: string;
|
|
12
|
+
sipUrl?: string;
|
|
13
|
+
sipUsername?: string;
|
|
14
|
+
sipPassword?: string;
|
|
15
|
+
transferCallerIdShown?: TransferCallerIdShown;
|
|
16
|
+
};
|
|
17
|
+
/** Shared dial config for agenda transfer nodes and interrupt transfer actions. */
|
|
18
|
+
export type TransferDial = {
|
|
19
|
+
numbers: string[];
|
|
20
|
+
mode: TransferMode;
|
|
21
|
+
say?: TransferSay | null;
|
|
22
|
+
unsuccessfulSay?: TransferSay | null;
|
|
23
|
+
fallbackAction?: TransferFallbackAction | null;
|
|
24
|
+
arguments?: TransferArguments | null;
|
|
25
|
+
warmConfirmMode?: TransferConfirmMethod[] | null;
|
|
26
|
+
warmConfirmKeypad?: TransferConfirmKey | null;
|
|
27
|
+
warmConfirmTimeoutMs?: number | null;
|
|
28
|
+
warmLoopSay?: string | null;
|
|
29
|
+
warmBriefingPrompt?: string | null;
|
|
30
|
+
asyncWarmBridge?: {
|
|
31
|
+
targetSayTemplate?: string;
|
|
32
|
+
customerSayTemplate?: string;
|
|
33
|
+
} | null;
|
|
34
|
+
};
|
|
35
|
+
export declare const E164_NUMBER: RegExp;
|
|
36
|
+
export declare const SIP_URI: RegExp;
|
|
37
|
+
export declare function isTransferDestinationLiteral(value: string): boolean;
|
|
38
|
+
export declare function interpolateTemplate(text: string, read: (path: FieldPath) => string | number | boolean | null | undefined): string;
|
|
39
|
+
export declare function interpolateFromSnapshot(text: string, snapshot: RuntimeSlotsSnapshot): string;
|
|
40
|
+
export declare function resolveTransferDestinations(numbers: string[], snapshot: RuntimeSlotsSnapshot): string[];
|
|
41
|
+
export declare function resolveTransferSay(say: TransferSay | null | undefined, language: string | null | undefined, fallback: string): string;
|
|
42
|
+
export declare function resolveTransferArguments(dial: TransferDial, snapshot: RuntimeSlotsSnapshot): TransferArguments;
|
|
43
|
+
export declare function transferArgumentsJson(dial: TransferDial, snapshot: RuntimeSlotsSnapshot): string;
|
|
44
|
+
export declare function validateTransferDestinations(numbers: string[], options: {
|
|
45
|
+
path: string;
|
|
46
|
+
code: string;
|
|
47
|
+
}): ValidationIssue[];
|
|
48
|
+
//# sourceMappingURL=transfer-spec.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { parseFieldPath } from '../slot/path.js';
|
|
2
|
+
import { getRowField } from '../slot/row.js';
|
|
3
|
+
import { getSnapshotTable } from '../slot/snapshot.js';
|
|
4
|
+
import { extractTemplateFields } from './condition.js';
|
|
5
|
+
export const E164_NUMBER = /^\+[1-9]\d{7,14}$/;
|
|
6
|
+
export const SIP_URI = /^sips?:[^@\s]+@[^@\s]+$/i;
|
|
7
|
+
const TEMPLATE_RE = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
8
|
+
export function isTransferDestinationLiteral(value) {
|
|
9
|
+
return E164_NUMBER.test(value) || SIP_URI.test(value);
|
|
10
|
+
}
|
|
11
|
+
export function interpolateTemplate(text, read) {
|
|
12
|
+
return text.replace(TEMPLATE_RE, (_all, path) => {
|
|
13
|
+
const value = read(path);
|
|
14
|
+
return value == null ? '' : String(value);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function boundRowId(tableName, current) {
|
|
18
|
+
if (!current)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (tableName === 'contact')
|
|
21
|
+
return current.contactId;
|
|
22
|
+
if (tableName === 'case_contact')
|
|
23
|
+
return current.caseContactId;
|
|
24
|
+
if (tableName === 'case_file')
|
|
25
|
+
return current.caseFileId;
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export function interpolateFromSnapshot(text, snapshot) {
|
|
29
|
+
return interpolateTemplate(text, (path) => {
|
|
30
|
+
const parsed = parseFieldPath(path);
|
|
31
|
+
if (!parsed)
|
|
32
|
+
return '';
|
|
33
|
+
const data = getSnapshotTable(snapshot, parsed.table);
|
|
34
|
+
if (!data)
|
|
35
|
+
return '';
|
|
36
|
+
if (!Array.isArray(data))
|
|
37
|
+
return getRowField(data, parsed.field)?.value ?? '';
|
|
38
|
+
const id = boundRowId(parsed.table, snapshot.current);
|
|
39
|
+
if (!id)
|
|
40
|
+
return '';
|
|
41
|
+
const row = data.find((entry) => entry.id === id);
|
|
42
|
+
return row ? (getRowField(row, parsed.field)?.value ?? '') : '';
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function resolveTransferDestinations(numbers, snapshot) {
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const raw of numbers) {
|
|
48
|
+
const resolved = interpolateFromSnapshot(raw, snapshot).trim();
|
|
49
|
+
if (resolved)
|
|
50
|
+
out.push(resolved);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
export function resolveTransferSay(say, language, fallback) {
|
|
55
|
+
if (say == null)
|
|
56
|
+
return fallback;
|
|
57
|
+
if (typeof say === 'string')
|
|
58
|
+
return say.trim() || fallback;
|
|
59
|
+
const key = language?.trim().toLowerCase() || 'english';
|
|
60
|
+
const picked = say[key] ?? say.english ?? Object.values(say).find((value) => value.trim().length > 0);
|
|
61
|
+
return picked?.trim() || fallback;
|
|
62
|
+
}
|
|
63
|
+
function interpolatedArg(value, snapshot) {
|
|
64
|
+
return value ? interpolateFromSnapshot(value, snapshot).trim() : '';
|
|
65
|
+
}
|
|
66
|
+
export function resolveTransferArguments(dial, snapshot) {
|
|
67
|
+
const src = dial.arguments;
|
|
68
|
+
if (!src)
|
|
69
|
+
return {};
|
|
70
|
+
const out = {};
|
|
71
|
+
const sendDigits = interpolatedArg(src.sendDigits, snapshot);
|
|
72
|
+
if (sendDigits)
|
|
73
|
+
out.sendDigits = sendDigits;
|
|
74
|
+
const sipUrl = interpolatedArg(src.sipUrl, snapshot);
|
|
75
|
+
if (sipUrl)
|
|
76
|
+
out.sipUrl = sipUrl;
|
|
77
|
+
const sipUsername = interpolatedArg(src.sipUsername, snapshot);
|
|
78
|
+
if (sipUsername)
|
|
79
|
+
out.sipUsername = sipUsername;
|
|
80
|
+
const sipPassword = interpolatedArg(src.sipPassword, snapshot);
|
|
81
|
+
if (sipPassword)
|
|
82
|
+
out.sipPassword = sipPassword;
|
|
83
|
+
if (src.transferCallerIdShown)
|
|
84
|
+
out.transferCallerIdShown = src.transferCallerIdShown;
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
export function transferArgumentsJson(dial, snapshot) {
|
|
88
|
+
return JSON.stringify(resolveTransferArguments(dial, snapshot));
|
|
89
|
+
}
|
|
90
|
+
export function validateTransferDestinations(numbers, options) {
|
|
91
|
+
const issues = [];
|
|
92
|
+
if (numbers.length === 0) {
|
|
93
|
+
issues.push({
|
|
94
|
+
code: options.code,
|
|
95
|
+
message: 'transfer numbers must not be empty',
|
|
96
|
+
path: options.path,
|
|
97
|
+
});
|
|
98
|
+
return issues;
|
|
99
|
+
}
|
|
100
|
+
for (const number of numbers) {
|
|
101
|
+
if (extractTemplateFields(number).length > 0)
|
|
102
|
+
continue;
|
|
103
|
+
if (!isTransferDestinationLiteral(number)) {
|
|
104
|
+
issues.push({
|
|
105
|
+
code: options.code,
|
|
106
|
+
message: `Transfer target must be E.164 or sip/sips URI: ${number}`,
|
|
107
|
+
path: options.path,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return issues;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=transfer-spec.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TopLevelCondition } from 'json-rules-engine';
|
|
2
2
|
import type { FuncRef } from '../functions/func-ref.js';
|
|
3
3
|
import type { FieldPath } from '../slot/path.js';
|
|
4
|
+
import type { TransferDial, TransferFallbackAction } from './transfer-spec.js';
|
|
4
5
|
export type Channel = 'call' | 'sms' | 'email' | 'webchat';
|
|
5
6
|
/** Nested condition from the engine (`all` members): leaf or `TopLevelCondition`. */
|
|
6
7
|
export type Condition = Extract<TopLevelCondition, {
|
|
@@ -40,6 +41,11 @@ export type FirstMessage = {
|
|
|
40
41
|
kind: 'fn';
|
|
41
42
|
fn: string;
|
|
42
43
|
};
|
|
44
|
+
export type TransferInterruptAction = {
|
|
45
|
+
type: 'transfer';
|
|
46
|
+
escalateCriteria?: string[];
|
|
47
|
+
noEscalationCriteria?: string[];
|
|
48
|
+
} & TransferDial;
|
|
43
49
|
export type Interrupt = {
|
|
44
50
|
id: string;
|
|
45
51
|
label: string;
|
|
@@ -47,11 +53,7 @@ export type Interrupt = {
|
|
|
47
53
|
name: string;
|
|
48
54
|
description: string;
|
|
49
55
|
};
|
|
50
|
-
action: {
|
|
51
|
-
type: 'transfer';
|
|
52
|
-
numbers: string[];
|
|
53
|
-
mode: 'cold' | 'warm';
|
|
54
|
-
} | {
|
|
56
|
+
action: TransferInterruptAction | {
|
|
55
57
|
type: 'switch-language';
|
|
56
58
|
languages: string[];
|
|
57
59
|
};
|
|
@@ -86,12 +88,32 @@ export type TransferBlock = {
|
|
|
86
88
|
kind: 'transfer';
|
|
87
89
|
id: string;
|
|
88
90
|
label: string;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
} & TransferDial;
|
|
92
|
+
export type IvrDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '*' | '#';
|
|
93
|
+
export type IvrFallbackAction = TransferFallbackAction;
|
|
94
|
+
export type DtmfResult = {
|
|
95
|
+
result: IvrDigit[];
|
|
96
|
+
value: string;
|
|
97
|
+
};
|
|
98
|
+
export type IvrBlock = {
|
|
99
|
+
kind: 'ivr';
|
|
100
|
+
id: string;
|
|
101
|
+
label: string;
|
|
102
|
+
onDtmfResult: FuncRef;
|
|
103
|
+
finishWhen: FinishWhen;
|
|
104
|
+
fallbackAction: IvrFallbackAction;
|
|
105
|
+
confirmTimeoutMs: number;
|
|
106
|
+
pauseAfterSayMs: number;
|
|
107
|
+
loopSay: string;
|
|
108
|
+
enterSay: string | null;
|
|
109
|
+
succeedSay: string | null;
|
|
110
|
+
unsuccessSay: string | null;
|
|
111
|
+
finishOnKey?: IvrDigit | null;
|
|
112
|
+
minDigits?: number | null;
|
|
113
|
+
maxDigits?: number | null;
|
|
114
|
+
validateDigits?: IvrDigit[] | null;
|
|
115
|
+
};
|
|
116
|
+
export type AgendaBlock = TopicBlock | ActionBlock | TransferBlock | IvrBlock;
|
|
95
117
|
export type CompiledTopicBlock = Omit<TopicBlock, 'fields' | 'finishWhen'> & {
|
|
96
118
|
fields: FieldPath[];
|
|
97
119
|
finishWhen: CompiledWhen;
|
|
@@ -104,7 +126,12 @@ export type CompiledTransferBlock = TransferBlock & {
|
|
|
104
126
|
finishWhen: CompiledWhen;
|
|
105
127
|
resultField: FieldPath;
|
|
106
128
|
};
|
|
107
|
-
export type
|
|
129
|
+
export type CompiledIvrBlock = Omit<IvrBlock, 'onDtmfResult' | 'finishWhen'> & {
|
|
130
|
+
onDtmfResult: string;
|
|
131
|
+
finishWhen: CompiledWhen;
|
|
132
|
+
resultField: FieldPath;
|
|
133
|
+
};
|
|
134
|
+
export type CompiledAgendaBlock = CompiledTopicBlock | CompiledActionBlock | CompiledTransferBlock | CompiledIvrBlock;
|
|
108
135
|
export type SectionPlacement = {
|
|
109
136
|
kind: 'section';
|
|
110
137
|
placementId: string;
|
|
@@ -136,7 +163,8 @@ export type FlatNodePlacement = {
|
|
|
136
163
|
export type FlatTopicItem = CompiledTopicBlock & FlatNodePlacement;
|
|
137
164
|
export type FlatActionItem = CompiledActionBlock & FlatNodePlacement;
|
|
138
165
|
export type FlatTransferItem = CompiledTransferBlock & FlatNodePlacement;
|
|
139
|
-
export type
|
|
166
|
+
export type FlatIvrItem = CompiledIvrBlock & FlatNodePlacement;
|
|
167
|
+
export type FlatAgendaItem = FlatTopicItem | FlatActionItem | FlatTransferItem | FlatIvrItem;
|
|
140
168
|
export type CompiledAgenda = {
|
|
141
169
|
header: AgendaHeader | null;
|
|
142
170
|
flat: FlatAgendaItem[];
|
|
@@ -150,6 +178,9 @@ export type ActionNode = ActionBlock & {
|
|
|
150
178
|
export type TransferNode = TransferBlock & {
|
|
151
179
|
applyWhen: AuthoringWhen;
|
|
152
180
|
};
|
|
181
|
+
export type IvrNode = IvrBlock & {
|
|
182
|
+
applyWhen: AuthoringWhen;
|
|
183
|
+
};
|
|
153
184
|
export type SectionNode = {
|
|
154
185
|
kind: 'section';
|
|
155
186
|
id: string;
|
|
@@ -167,7 +198,7 @@ export type SwitchOnNode = {
|
|
|
167
198
|
cases: Record<string, AuthoringNode[]>;
|
|
168
199
|
otherwise?: AuthoringNode[];
|
|
169
200
|
};
|
|
170
|
-
export type AuthoringNode = TopicNode | ActionNode | TransferNode | SectionNode | BranchNode | SwitchOnNode;
|
|
201
|
+
export type AuthoringNode = TopicNode | ActionNode | TransferNode | IvrNode | SectionNode | BranchNode | SwitchOnNode;
|
|
171
202
|
export type AgendaTree = {
|
|
172
203
|
kind: 'agenda';
|
|
173
204
|
id: string;
|
|
@@ -2,7 +2,9 @@ import { getCompiledTable } from '../slot/compile-schema.js';
|
|
|
2
2
|
import { parseFieldPath } from '../slot/path.js';
|
|
3
3
|
import { extractTemplateFields } from './condition.js';
|
|
4
4
|
import { whenFields } from './finish-when.js';
|
|
5
|
+
import { headerHasTransferInterrupt, validateInterrupts } from './interrupts.js';
|
|
5
6
|
import { LAST_TRANSFER_RESULT_FIELD } from './transfer-fields.js';
|
|
7
|
+
import { validateTransferDestinations } from './transfer-spec.js';
|
|
6
8
|
function fieldExists(schema, path) {
|
|
7
9
|
const parsed = parseFieldPath(path);
|
|
8
10
|
if (!parsed)
|
|
@@ -28,6 +30,10 @@ function earlierCollected(items, index) {
|
|
|
28
30
|
for (const field of item.fields)
|
|
29
31
|
collected.add(field);
|
|
30
32
|
}
|
|
33
|
+
if (item.kind === 'ivr') {
|
|
34
|
+
for (const field of whenFields(item.finishWhen))
|
|
35
|
+
collected.add(field);
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
38
|
return collected;
|
|
33
39
|
}
|
|
@@ -74,6 +80,13 @@ export function validateCompiledAgenda(compiled, options) {
|
|
|
74
80
|
const seenPlacement = new Set();
|
|
75
81
|
const transferByResult = transferIndexByResultField(items);
|
|
76
82
|
const firstTransfer = firstTransferIndex(items);
|
|
83
|
+
const transferInterrupt = headerHasTransferInterrupt(compiled.header);
|
|
84
|
+
if (compiled.header) {
|
|
85
|
+
issues.push(...validateInterrupts(compiled.header.interrupts, {
|
|
86
|
+
schema,
|
|
87
|
+
agendaId: prefix,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
77
90
|
if (compiled.header?.channel && compiled.header.channel !== 'call') {
|
|
78
91
|
for (const item of items) {
|
|
79
92
|
if (item.kind === 'transfer') {
|
|
@@ -83,6 +96,13 @@ export function validateCompiledAgenda(compiled, options) {
|
|
|
83
96
|
path: `${prefix}/${item.placementId}`,
|
|
84
97
|
});
|
|
85
98
|
}
|
|
99
|
+
if (item.kind === 'ivr') {
|
|
100
|
+
issues.push({
|
|
101
|
+
code: 'ivr-channel',
|
|
102
|
+
message: `IVR ${item.id} is only valid on call agendas`,
|
|
103
|
+
path: `${prefix}/${item.placementId}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
if (compiled.header?.firstMessage?.kind === 'template') {
|
|
@@ -128,7 +148,7 @@ export function validateCompiledAgenda(compiled, options) {
|
|
|
128
148
|
continue;
|
|
129
149
|
}
|
|
130
150
|
if (field === LAST_TRANSFER_RESULT_FIELD) {
|
|
131
|
-
if (firstTransfer < 0 || firstTransfer >= index) {
|
|
151
|
+
if (!transferInterrupt && (firstTransfer < 0 || firstTransfer >= index)) {
|
|
132
152
|
issues.push({
|
|
133
153
|
code: 'transfer-result-before-node',
|
|
134
154
|
message: `applyWhen reads ${LAST_TRANSFER_RESULT_FIELD} before any transfer`,
|
|
@@ -152,6 +172,56 @@ export function validateCompiledAgenda(compiled, options) {
|
|
|
152
172
|
}
|
|
153
173
|
}
|
|
154
174
|
requireBoolWhen(item.applyWhen, functions, issues, `${path}/applyWhen`, 'applyWhen');
|
|
175
|
+
if (item.kind === 'ivr') {
|
|
176
|
+
requireBoolWhen(item.finishWhen, functions, issues, `${path}/finishWhen`, 'finishWhen');
|
|
177
|
+
if (item.finishWhen.kind === 'rules') {
|
|
178
|
+
requireFields(schema, whenFields(item.finishWhen), issues, `${path}/finishWhen`);
|
|
179
|
+
}
|
|
180
|
+
const run = functions?.[item.onDtmfResult];
|
|
181
|
+
if (functions && !run) {
|
|
182
|
+
issues.push({
|
|
183
|
+
code: 'unknown-function',
|
|
184
|
+
message: `IVR '${item.id}' onDtmfResult unknown function '${item.onDtmfResult}'. Custom functions: ${listedCustomFunctions(functions)}`,
|
|
185
|
+
path: `${path}/onDtmfResult`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else if (run && run.kind !== 'custom') {
|
|
189
|
+
issues.push({
|
|
190
|
+
code: 'ivr-run-not-custom',
|
|
191
|
+
message: `IVR '${item.id}' onDtmfResult '${item.onDtmfResult}' must be defineCustomFunc`,
|
|
192
|
+
path: `${path}/onDtmfResult`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (!item.loopSay.trim()) {
|
|
196
|
+
issues.push({
|
|
197
|
+
code: 'ivr-say-empty',
|
|
198
|
+
message: 'ivr loopSay must not be empty',
|
|
199
|
+
path: `${path}/loopSay`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (!(item.confirmTimeoutMs > 0) || !(item.pauseAfterSayMs >= 0)) {
|
|
203
|
+
issues.push({
|
|
204
|
+
code: 'ivr-timing',
|
|
205
|
+
message: 'ivr confirmTimeoutMs must be > 0 and pauseAfterSayMs must be >= 0',
|
|
206
|
+
path: `${path}/timing`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (item.fallbackAction !== 'end-call' && item.fallbackAction !== 'restore-normal-call-state') {
|
|
210
|
+
issues.push({
|
|
211
|
+
code: 'ivr-fallback',
|
|
212
|
+
message: 'ivr fallbackAction must be end-call or restore-normal-call-state',
|
|
213
|
+
path: `${path}/fallbackAction`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const digits = (item.validateDigits ?? []).map((digit) => digit.trim()).filter(Boolean);
|
|
217
|
+
if (!item.finishOnKey && digits.length === 0) {
|
|
218
|
+
issues.push({
|
|
219
|
+
code: 'ivr-digits-empty',
|
|
220
|
+
message: 'ivr needs validateDigits when finishOnKey is not set',
|
|
221
|
+
path: `${path}/validateDigits`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
155
225
|
if (item.kind === 'topic') {
|
|
156
226
|
requireFields(schema, item.fields, issues, `${path}/fields`);
|
|
157
227
|
const referenced = whenFields(item.finishWhen);
|
|
@@ -194,6 +264,26 @@ export function validateCompiledAgenda(compiled, options) {
|
|
|
194
264
|
for (const number of item.numbers) {
|
|
195
265
|
requireFields(schema, extractTemplateFields(number), issues, `${path}/numbers`);
|
|
196
266
|
}
|
|
267
|
+
if (item.arguments) {
|
|
268
|
+
for (const key of ['sendDigits', 'sipUrl', 'sipUsername', 'sipPassword']) {
|
|
269
|
+
const text = item.arguments[key];
|
|
270
|
+
if (text) {
|
|
271
|
+
requireFields(schema, extractTemplateFields(text), issues, `${path}/arguments/${key}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (typeof item.say === 'string') {
|
|
276
|
+
requireFields(schema, extractTemplateFields(item.say), issues, `${path}/say`);
|
|
277
|
+
}
|
|
278
|
+
else if (item.say) {
|
|
279
|
+
for (const text of Object.values(item.say)) {
|
|
280
|
+
requireFields(schema, extractTemplateFields(text), issues, `${path}/say`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
issues.push(...validateTransferDestinations(item.numbers, {
|
|
284
|
+
path: `${path}/numbers`,
|
|
285
|
+
code: 'transfer-target',
|
|
286
|
+
}));
|
|
197
287
|
requireFields(schema, [item.resultField, LAST_TRANSFER_RESULT_FIELD], issues, `${path}/result`);
|
|
198
288
|
}
|
|
199
289
|
}
|
|
@@ -215,10 +305,24 @@ const FATAL_AGENDA_CODES = new Set([
|
|
|
215
305
|
'branch-no-explicit',
|
|
216
306
|
'otherwise-unexpanded',
|
|
217
307
|
'transfer-channel',
|
|
308
|
+
'ivr-channel',
|
|
309
|
+
'ivr-run-not-custom',
|
|
310
|
+
'ivr-fallback',
|
|
311
|
+
'ivr-timing',
|
|
312
|
+
'ivr-say-empty',
|
|
313
|
+
'ivr-digits-empty',
|
|
218
314
|
'transfer-result-before-node',
|
|
219
315
|
'action-run-not-custom',
|
|
220
316
|
'duplicate-placement',
|
|
221
317
|
'block-kind-mismatch',
|
|
318
|
+
'interrupt-tool-reserved',
|
|
319
|
+
'interrupt-tool-duplicate',
|
|
320
|
+
'interrupt-priority-tie',
|
|
321
|
+
'interrupt-languages-empty',
|
|
322
|
+
'interrupt-language-field',
|
|
323
|
+
'interrupt-transfer-number',
|
|
324
|
+
'interrupt-escalation-criteria',
|
|
325
|
+
'transfer-target',
|
|
222
326
|
]);
|
|
223
327
|
export function fatalAgendaIssues(issues) {
|
|
224
328
|
return issues.filter((issue) => FATAL_AGENDA_CODES.has(issue.code));
|
|
@@ -9,11 +9,16 @@ export type WalkSkip = {
|
|
|
9
9
|
reason: WalkSkipReason;
|
|
10
10
|
};
|
|
11
11
|
export type WalkHitReason = 'sticky' | 'scan';
|
|
12
|
+
export type WalkStats = {
|
|
13
|
+
durationMs: number;
|
|
14
|
+
evals: number;
|
|
15
|
+
};
|
|
12
16
|
export type WalkResult = {
|
|
13
17
|
hit: FlatAgendaItem | null;
|
|
14
18
|
reason: WalkHitReason | 'complete';
|
|
15
19
|
skipped: WalkSkip[];
|
|
16
20
|
decisions: WalkDecision[];
|
|
21
|
+
stats: WalkStats;
|
|
17
22
|
};
|
|
18
23
|
export type WalkDecision = {
|
|
19
24
|
placementId: string;
|