@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
package/dist/core/slot/write.js
CHANGED
|
@@ -4,6 +4,70 @@ import { formatFieldPath, parseFieldPath, requireFieldPath } from './path.js';
|
|
|
4
4
|
import { getRowField, isReservedTableName, isRowIdField, mintRowId, slotFieldNames } from './row.js';
|
|
5
5
|
import { cloneSnapshot, getSnapshotTable } from './snapshot.js';
|
|
6
6
|
import { validateBareValue, validateExtractedField } from './value.js';
|
|
7
|
+
export function isRowWrite(value) {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
9
|
+
return false;
|
|
10
|
+
const o = value;
|
|
11
|
+
return (typeof o.table === 'string' &&
|
|
12
|
+
typeof o.rowId === 'string' &&
|
|
13
|
+
o.values !== null &&
|
|
14
|
+
typeof o.values === 'object' &&
|
|
15
|
+
!Array.isArray(o.values));
|
|
16
|
+
}
|
|
17
|
+
export function isStructuredWrites(value) {
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || isRowWrite(value))
|
|
19
|
+
return false;
|
|
20
|
+
const keys = Object.keys(value);
|
|
21
|
+
return keys.length > 0 && keys.every((key) => key === 'fields' || key === 'rows');
|
|
22
|
+
}
|
|
23
|
+
export function normalizeWrites(writes) {
|
|
24
|
+
if (writes == null)
|
|
25
|
+
return null;
|
|
26
|
+
if (isRowWrite(writes)) {
|
|
27
|
+
return { fields: {}, rows: [writes] };
|
|
28
|
+
}
|
|
29
|
+
if (isStructuredWrites(writes)) {
|
|
30
|
+
const fields = writes.fields && typeof writes.fields === 'object' && !Array.isArray(writes.fields) ? writes.fields : {};
|
|
31
|
+
const rows = Array.isArray(writes.rows) ? writes.rows.filter(isRowWrite) : [];
|
|
32
|
+
if (Object.keys(fields).length === 0 && rows.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
return { fields, rows };
|
|
35
|
+
}
|
|
36
|
+
if (Object.keys(writes).length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
return { fields: writes, rows: [] };
|
|
39
|
+
}
|
|
40
|
+
function boundRowId(tableName, current) {
|
|
41
|
+
if (!current)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (tableName === 'contact')
|
|
44
|
+
return current.contactId;
|
|
45
|
+
if (tableName === 'case_contact')
|
|
46
|
+
return current.caseContactId;
|
|
47
|
+
if (tableName === 'case_file')
|
|
48
|
+
return current.caseFileId;
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function writeTargetRow(schema, snapshot, tableName, rowId) {
|
|
52
|
+
const table = getCompiledTable(schema, tableName);
|
|
53
|
+
if (!table)
|
|
54
|
+
return null;
|
|
55
|
+
const data = getSnapshotTable(snapshot, tableName);
|
|
56
|
+
if (!data)
|
|
57
|
+
return null;
|
|
58
|
+
if (isSnapshotSingleRow(table) && !Array.isArray(data)) {
|
|
59
|
+
if (rowId && data.id && data.id !== rowId)
|
|
60
|
+
return null;
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(data)) {
|
|
64
|
+
const id = rowId ?? boundRowId(tableName, snapshot.current);
|
|
65
|
+
if (!id)
|
|
66
|
+
return null;
|
|
67
|
+
return data.find((row) => row.id === id) ?? null;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
7
71
|
function extractedToFieldValue(extracted, updatedBy, now) {
|
|
8
72
|
return makeFieldValue({
|
|
9
73
|
reason: extracted.reason,
|
|
@@ -13,7 +77,7 @@ function extractedToFieldValue(extracted, updatedBy, now) {
|
|
|
13
77
|
updatedAt: now,
|
|
14
78
|
});
|
|
15
79
|
}
|
|
16
|
-
function assertWritableNormal(schema, path, options) {
|
|
80
|
+
function assertWritableNormal(schema, snapshot, path, options, rowId) {
|
|
17
81
|
const { table, field: fieldName } = requireFieldPath(path);
|
|
18
82
|
const tableDef = getCompiledTable(schema, table);
|
|
19
83
|
const field = tableDef?.fields[fieldName];
|
|
@@ -49,23 +113,75 @@ function assertWritableNormal(schema, path, options) {
|
|
|
49
113
|
},
|
|
50
114
|
];
|
|
51
115
|
}
|
|
52
|
-
if (!isSnapshotSingleRow(tableDef)) {
|
|
116
|
+
if (!isSnapshotSingleRow(tableDef) && !writeTargetRow(schema, snapshot, table, rowId)) {
|
|
53
117
|
return [
|
|
54
118
|
{
|
|
55
|
-
code: 'multi-path-write',
|
|
56
|
-
message:
|
|
119
|
+
code: rowId ? 'row-not-found' : 'multi-path-write',
|
|
120
|
+
message: rowId
|
|
121
|
+
? `Row ${rowId} not found in ${table}`
|
|
122
|
+
: `Use Slot Filling or { table, rowId, values } to write multi-row field ${path}`,
|
|
57
123
|
path,
|
|
58
124
|
},
|
|
59
125
|
];
|
|
60
126
|
}
|
|
61
127
|
return [];
|
|
62
128
|
}
|
|
129
|
+
function applyBareFieldWrite(schema, original, next, path, value, rowId, options, now, errors, changed, history) {
|
|
130
|
+
const writeIssues = assertWritableNormal(schema, original, path, options, rowId);
|
|
131
|
+
if (writeIssues.length > 0) {
|
|
132
|
+
errors.push(...writeIssues);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const parsed = parseFieldPath(path);
|
|
136
|
+
if (!parsed) {
|
|
137
|
+
errors.push({ code: 'unknown-field', message: `Invalid field path: ${path}`, path });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const field = getCompiledTable(schema, parsed.table)?.fields[parsed.field];
|
|
141
|
+
if (!field) {
|
|
142
|
+
errors.push({ code: 'unknown-field', message: `Unknown field: ${path}`, path });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const valueIssues = validateBareValue(field, value, path, {
|
|
146
|
+
allowNull: !field.derived && field.nullable,
|
|
147
|
+
});
|
|
148
|
+
if (valueIssues.length > 0) {
|
|
149
|
+
errors.push(...valueIssues);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const tableData = writeTargetRow(schema, next, parsed.table, rowId);
|
|
153
|
+
if (!tableData)
|
|
154
|
+
return;
|
|
155
|
+
const prev = getRowField(tableData, parsed.field);
|
|
156
|
+
if (!prev)
|
|
157
|
+
return;
|
|
158
|
+
const committed = commitFieldWrite(prev, makeFieldValue({
|
|
159
|
+
value: (value ?? null),
|
|
160
|
+
state: 'filled',
|
|
161
|
+
reason: null,
|
|
162
|
+
updatedBy: options.source,
|
|
163
|
+
updatedAt: now,
|
|
164
|
+
}));
|
|
165
|
+
tableData[parsed.field] = committed.field;
|
|
166
|
+
if (committed.changed) {
|
|
167
|
+
changed.push(parsed.path);
|
|
168
|
+
if (committed.historyEntry) {
|
|
169
|
+
history.push({
|
|
170
|
+
path: parsed.path,
|
|
171
|
+
entry: committed.historyEntry,
|
|
172
|
+
rowId: typeof tableData.id === 'string' && tableData.id ? tableData.id : undefined,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
63
177
|
/**
|
|
64
178
|
* Apply bare Writes (function / human / import / platform) onto a snapshot.
|
|
65
179
|
* Validates type/enum and write permissions. Detects C5 changes and appends history.
|
|
180
|
+
* Multi-row tables: pass `{ table, rowId, values }` (or `rows` on StructuredWrites).
|
|
66
181
|
*/
|
|
67
182
|
export function applyWrites(schema, snapshot, writes, options) {
|
|
68
|
-
|
|
183
|
+
const normalized = normalizeWrites(writes);
|
|
184
|
+
if (!normalized) {
|
|
69
185
|
return { snapshot, changed: [], history: [] };
|
|
70
186
|
}
|
|
71
187
|
const now = options.now ?? Date.now();
|
|
@@ -73,38 +189,21 @@ export function applyWrites(schema, snapshot, writes, options) {
|
|
|
73
189
|
const changed = [];
|
|
74
190
|
const history = [];
|
|
75
191
|
const errors = [];
|
|
76
|
-
for (const [path, value] of Object.entries(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
192
|
+
for (const [path, value] of Object.entries(normalized.fields)) {
|
|
193
|
+
applyBareFieldWrite(schema, snapshot, next, path, value, undefined, options, now, errors, changed, history);
|
|
194
|
+
}
|
|
195
|
+
for (const row of normalized.rows) {
|
|
196
|
+
if (!row.table || !row.rowId) {
|
|
197
|
+
errors.push({
|
|
198
|
+
code: 'row-not-found',
|
|
199
|
+
message: 'Row write requires table and rowId',
|
|
200
|
+
path: row.table || undefined,
|
|
201
|
+
});
|
|
80
202
|
continue;
|
|
81
203
|
}
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
allowNull: !field.derived && field.nullable,
|
|
86
|
-
});
|
|
87
|
-
if (valueIssues.length > 0) {
|
|
88
|
-
errors.push(...valueIssues);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
const tableData = getSnapshotTable(next, parsed.table);
|
|
92
|
-
const prev = getRowField(tableData, parsed.field);
|
|
93
|
-
if (!prev)
|
|
94
|
-
continue;
|
|
95
|
-
const committed = commitFieldWrite(prev, makeFieldValue({
|
|
96
|
-
value: (value ?? null),
|
|
97
|
-
state: 'filled',
|
|
98
|
-
reason: null,
|
|
99
|
-
updatedBy: options.source,
|
|
100
|
-
updatedAt: now,
|
|
101
|
-
}));
|
|
102
|
-
tableData[parsed.field] = committed.field;
|
|
103
|
-
if (committed.changed) {
|
|
104
|
-
changed.push(parsed.path);
|
|
105
|
-
if (committed.historyEntry) {
|
|
106
|
-
history.push({ path: parsed.path, entry: committed.historyEntry });
|
|
107
|
-
}
|
|
204
|
+
for (const [fieldName, value] of Object.entries(row.values)) {
|
|
205
|
+
const path = formatFieldPath(row.table, fieldName);
|
|
206
|
+
applyBareFieldWrite(schema, snapshot, next, path, value, row.rowId, options, now, errors, changed, history);
|
|
108
207
|
}
|
|
109
208
|
}
|
|
110
209
|
if (errors.length > 0) {
|
|
@@ -201,7 +300,11 @@ export function applySlotFilling(schema, snapshot, output, options = {}) {
|
|
|
201
300
|
if (committed.changed) {
|
|
202
301
|
changed.add(path);
|
|
203
302
|
if (committed.historyEntry) {
|
|
204
|
-
history.push({
|
|
303
|
+
history.push({
|
|
304
|
+
path,
|
|
305
|
+
entry: committed.historyEntry,
|
|
306
|
+
rowId: typeof tableData.id === 'string' && tableData.id ? tableData.id : undefined,
|
|
307
|
+
});
|
|
205
308
|
}
|
|
206
309
|
}
|
|
207
310
|
}
|
|
@@ -241,7 +344,7 @@ export function applySlotFilling(schema, snapshot, output, options = {}) {
|
|
|
241
344
|
if (committed.changed) {
|
|
242
345
|
changed.add(path);
|
|
243
346
|
if (committed.historyEntry) {
|
|
244
|
-
history.push({ path, entry: committed.historyEntry });
|
|
347
|
+
history.push({ path, entry: committed.historyEntry, rowId: id });
|
|
245
348
|
}
|
|
246
349
|
}
|
|
247
350
|
}
|
|
@@ -310,7 +413,11 @@ export function markAskedUnknown(schema, snapshot, fields, options = {}) {
|
|
|
310
413
|
if (committed.changed) {
|
|
311
414
|
changed.push(parsed.path);
|
|
312
415
|
if (committed.historyEntry) {
|
|
313
|
-
history.push({
|
|
416
|
+
history.push({
|
|
417
|
+
path: parsed.path,
|
|
418
|
+
entry: committed.historyEntry,
|
|
419
|
+
rowId: typeof tableData.id === 'string' && tableData.id ? tableData.id : undefined,
|
|
420
|
+
});
|
|
314
421
|
}
|
|
315
422
|
}
|
|
316
423
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FuncRef } from '../functions/func-ref.js';
|
|
2
2
|
import type { FieldPath } from '../slot/path.js';
|
|
3
|
-
import type { ActionNode, AgendaHeader, AgendaTree, AuthoringNode, AuthoringWhen, BranchNode, FinishWhen, OnNoProgress, SectionNode, SwitchOnNode, TopicNode, TransferNode } from './types.js';
|
|
3
|
+
import type { ActionNode, AgendaHeader, AgendaTree, AuthoringNode, AuthoringWhen, BranchNode, FinishWhen, IvrDigit, IvrFallbackAction, IvrNode, OnNoProgress, SectionNode, SwitchOnNode, TopicNode, TransferNode } from './types.js';
|
|
4
|
+
import type { TransferDial, TransferMode, TransferSay } from './transfer-spec.js';
|
|
4
5
|
export declare function topic(id: string, config: {
|
|
5
6
|
label: string;
|
|
6
7
|
applyWhen: AuthoringWhen;
|
|
@@ -20,10 +21,27 @@ export declare function transfer(id: string, config: {
|
|
|
20
21
|
label: string;
|
|
21
22
|
applyWhen: AuthoringWhen;
|
|
22
23
|
numbers: string[];
|
|
23
|
-
mode:
|
|
24
|
-
say:
|
|
25
|
-
unsuccessfulSay:
|
|
26
|
-
}): TransferNode;
|
|
24
|
+
mode: TransferMode;
|
|
25
|
+
say: TransferSay;
|
|
26
|
+
unsuccessfulSay: TransferSay | null;
|
|
27
|
+
} & Omit<TransferDial, 'numbers' | 'mode' | 'say' | 'unsuccessfulSay'>): TransferNode;
|
|
28
|
+
export declare function ivr(id: string, config: {
|
|
29
|
+
label: string;
|
|
30
|
+
applyWhen: AuthoringWhen;
|
|
31
|
+
onDtmfResult: FuncRef;
|
|
32
|
+
finishWhen: FinishWhen;
|
|
33
|
+
fallbackAction: IvrFallbackAction;
|
|
34
|
+
confirmTimeoutMs: number;
|
|
35
|
+
pauseAfterSayMs: number;
|
|
36
|
+
loopSay: string;
|
|
37
|
+
enterSay: string | null;
|
|
38
|
+
succeedSay: string | null;
|
|
39
|
+
unsuccessSay: string | null;
|
|
40
|
+
finishOnKey?: IvrDigit | null;
|
|
41
|
+
minDigits?: number | null;
|
|
42
|
+
maxDigits?: number | null;
|
|
43
|
+
validateDigits?: IvrDigit[] | null;
|
|
44
|
+
}): IvrNode;
|
|
27
45
|
export declare function section(id: string, config: {
|
|
28
46
|
label: string;
|
|
29
47
|
applyWhen: AuthoringWhen;
|
|
@@ -7,6 +7,9 @@ export function action(id, config) {
|
|
|
7
7
|
export function transfer(id, config) {
|
|
8
8
|
return { kind: 'transfer', id, ...config };
|
|
9
9
|
}
|
|
10
|
+
export function ivr(id, config) {
|
|
11
|
+
return { kind: 'ivr', id, ...config };
|
|
12
|
+
}
|
|
10
13
|
export function section(id, config, children) {
|
|
11
14
|
return { kind: 'section', id, label: config.label, applyWhen: config.applyWhen, children };
|
|
12
15
|
}
|
|
@@ -21,7 +21,7 @@ export type StageDriveResult = {
|
|
|
21
21
|
fused: boolean;
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
* Walk until a topic/transfer/complete, executing actions inline.
|
|
24
|
+
* Walk until a topic/transfer/ivr/complete, executing actions inline.
|
|
25
25
|
* Action returning null is skipped for this tick; writes trigger a full re-walk.
|
|
26
26
|
*/
|
|
27
27
|
export declare function driveAgenda(items: FlatAgendaItem[], state: StageDriveState, compute: ComputeHost, hooks: StageDriveHooks, options?: {
|
package/dist/core/stage/drive.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { normalizeWrites } from '../slot/write.js';
|
|
1
2
|
import { evaluateWhen } from './finish-when.js';
|
|
2
3
|
import { unresolvedFinishWhenFields, walkAgenda } from './walk.js';
|
|
3
4
|
export const DEFAULT_MAX_ACTIONS_PER_TURN = 10;
|
|
4
5
|
/**
|
|
5
|
-
* Walk until a topic/transfer/complete, executing actions inline.
|
|
6
|
+
* Walk until a topic/transfer/ivr/complete, executing actions inline.
|
|
6
7
|
* Action returning null is skipped for this tick; writes trigger a full re-walk.
|
|
7
8
|
*/
|
|
8
9
|
export async function driveAgenda(items, state, compute, hooks, options = {}) {
|
|
@@ -11,11 +12,11 @@ export async function driveAgenda(items, state, compute, hooks, options = {}) {
|
|
|
11
12
|
let currentPlacementId = state.currentPlacementId;
|
|
12
13
|
const skip = new Set();
|
|
13
14
|
let actionsRun = 0;
|
|
14
|
-
let lastWalk
|
|
15
|
+
let lastWalk;
|
|
15
16
|
while (true) {
|
|
16
17
|
lastWalk = await walkAgenda(items, snapshot, { currentPlacementId, compute, skipPlacementIds: skip });
|
|
17
18
|
const hit = lastWalk.hit;
|
|
18
|
-
if (!hit || hit.kind === 'topic' || hit.kind === 'transfer') {
|
|
19
|
+
if (!hit || hit.kind === 'topic' || hit.kind === 'transfer' || hit.kind === 'ivr') {
|
|
19
20
|
return {
|
|
20
21
|
snapshot,
|
|
21
22
|
currentPlacementId: hit?.placementId ?? currentPlacementId,
|
|
@@ -35,13 +36,13 @@ export async function driveAgenda(items, state, compute, hooks, options = {}) {
|
|
|
35
36
|
}
|
|
36
37
|
actionsRun += 1;
|
|
37
38
|
const writes = await hooks.runAction(hit, snapshot);
|
|
38
|
-
if (writes
|
|
39
|
-
|
|
40
|
-
skip.clear();
|
|
41
|
-
currentPlacementId = hit.placementId;
|
|
39
|
+
if (!writes || !normalizeWrites(writes)) {
|
|
40
|
+
skip.add(hit.placementId);
|
|
42
41
|
continue;
|
|
43
42
|
}
|
|
44
|
-
|
|
43
|
+
snapshot = await hooks.applyWrites(writes, snapshot);
|
|
44
|
+
skip.clear();
|
|
45
|
+
currentPlacementId = hit.placementId;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
export async function advanceAgenda(items, state, compute, hooks, options = {}) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveFuncName } from '../functions/func-ref.js';
|
|
2
2
|
import { isOtherwise, jsonRules } from './condition.js';
|
|
3
3
|
import { andCompiledWhen, compileAuthoringWhen, compileWhen, whenFields } from './finish-when.js';
|
|
4
|
+
import { ivrResultField } from './ivr-fields.js';
|
|
4
5
|
import { transferResultField } from './transfer-fields.js';
|
|
5
6
|
function isTopicNode(node) {
|
|
6
7
|
return node.kind === 'topic';
|
|
@@ -11,6 +12,9 @@ function isActionNode(node) {
|
|
|
11
12
|
function isTransferNode(node) {
|
|
12
13
|
return node.kind === 'transfer';
|
|
13
14
|
}
|
|
15
|
+
function isIvrNode(node) {
|
|
16
|
+
return node.kind === 'ivr';
|
|
17
|
+
}
|
|
14
18
|
function isSectionNode(node) {
|
|
15
19
|
return node.kind === 'section';
|
|
16
20
|
}
|
|
@@ -41,15 +45,8 @@ function toAgendaBlock(node) {
|
|
|
41
45
|
say: node.say,
|
|
42
46
|
};
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
id: node.id,
|
|
47
|
-
label: node.label,
|
|
48
|
-
numbers: node.numbers,
|
|
49
|
-
mode: node.mode,
|
|
50
|
-
say: node.say,
|
|
51
|
-
unsuccessfulSay: node.unsuccessfulSay,
|
|
52
|
-
};
|
|
48
|
+
const { applyWhen: _applyWhen, ...block } = node;
|
|
49
|
+
return block;
|
|
53
50
|
}
|
|
54
51
|
function compileBlock(block, functions, issues, path) {
|
|
55
52
|
try {
|
|
@@ -63,6 +60,11 @@ function compileBlock(block, functions, issues, path) {
|
|
|
63
60
|
const run = resolveFuncName(block.run, functions);
|
|
64
61
|
return { ...block, run, finishWhen };
|
|
65
62
|
}
|
|
63
|
+
if (block.kind === 'ivr') {
|
|
64
|
+
const finishWhen = compileWhen(block.finishWhen, functions);
|
|
65
|
+
const onDtmfResult = resolveFuncName(block.onDtmfResult, functions);
|
|
66
|
+
return { ...block, onDtmfResult, finishWhen, resultField: ivrResultField(block.id) };
|
|
67
|
+
}
|
|
66
68
|
const resultField = transferResultField(block.id);
|
|
67
69
|
const finishWhen = compileWhen(jsonRules({
|
|
68
70
|
fact: resultField,
|
|
@@ -341,7 +343,7 @@ function asSectionPlacement(node, usedIds, path, issues) {
|
|
|
341
343
|
children: node.children.map((child, i) => asPlacement(child, usedIds, `${path}/${node.id}[${i}]`, issues)),
|
|
342
344
|
};
|
|
343
345
|
}
|
|
344
|
-
if (isTopicNode(node) || isActionNode(node) || isTransferNode(node)) {
|
|
346
|
+
if (isTopicNode(node) || isActionNode(node) || isTransferNode(node) || isIvrNode(node)) {
|
|
345
347
|
return {
|
|
346
348
|
kind: 'node',
|
|
347
349
|
placementId: uniquePlacementId(node.id, usedIds),
|
|
@@ -371,7 +373,7 @@ export function extractBlocksFromTree(tree, blocks = {}) {
|
|
|
371
373
|
return blocks;
|
|
372
374
|
function visit(nodes) {
|
|
373
375
|
for (const node of nodes) {
|
|
374
|
-
if (isTopicNode(node) || isActionNode(node) || isTransferNode(node)) {
|
|
376
|
+
if (isTopicNode(node) || isActionNode(node) || isTransferNode(node) || isIvrNode(node)) {
|
|
375
377
|
blocks[node.id] = toAgendaBlock(node);
|
|
376
378
|
}
|
|
377
379
|
else if (isSectionNode(node) || node.kind === 'branch') {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
export type { ActionBlock, ActionNode, AgendaBlock, AgendaConfig, AgendaHeader, AgendaTree, ApplyWhen, AuthoringNode, AuthoringWhen, BranchNode, Channel, CompiledActionBlock, CompiledAgenda, CompiledAgendaBlock, CompiledTopicBlock, CompiledTransferBlock, CompiledWhen, Condition, FirstMessage, FinishWhen, FlatActionItem, FlatAgendaItem, FlatNodePlacement, FlatTopicItem, FlatTransferItem, Interrupt, SequenceAgendas, JsonRulesCondition, NodePlacement, OnNoProgress, OtherwiseSentinel, Placement, SectionNode, SectionPlacement, SwitchOnNode, TopicBlock, TopicNode, TransferBlock, TransferNode, When, } from './types.js';
|
|
1
|
+
export type { ActionBlock, ActionNode, AgendaBlock, AgendaConfig, AgendaHeader, AgendaTree, ApplyWhen, AuthoringNode, AuthoringWhen, BranchNode, Channel, CompiledActionBlock, CompiledAgenda, CompiledAgendaBlock, CompiledTopicBlock, CompiledIvrBlock, CompiledTransferBlock, CompiledWhen, Condition, FirstMessage, FinishWhen, FlatActionItem, FlatAgendaItem, FlatIvrItem, FlatNodePlacement, FlatTopicItem, FlatTransferItem, DtmfResult, IvrBlock, IvrDigit, IvrFallbackAction, IvrNode, Interrupt, SequenceAgendas, JsonRulesCondition, NodePlacement, OnNoProgress, OtherwiseSentinel, Placement, SectionNode, SectionPlacement, SwitchOnNode, TopicBlock, TopicNode, TransferBlock, TransferInterruptAction, TransferNode, When, } from './types.js';
|
|
2
|
+
export type { TransferArguments, TransferCallerIdShown, TransferConfirmKey, TransferConfirmMethod, TransferDial, TransferFallbackAction, TransferMode, TransferSay, } from './transfer-spec.js';
|
|
3
|
+
export { E164_NUMBER, SIP_URI, interpolateFromSnapshot, interpolateTemplate, isTransferDestinationLiteral, resolveTransferArguments, resolveTransferDestinations, resolveTransferSay, transferArgumentsJson, validateTransferDestinations, } from './transfer-spec.js';
|
|
2
4
|
export { collectJsonRulesFacts, evaluateJsonRules, extractTemplateFields, isOtherwise, jsonRules, looksLikeJsonRules, normalizeJsonRules, otherwise, parseJsonRules, type ConditionEval, } from './condition.js';
|
|
3
5
|
export { andCompiledWhen, compileAuthoringWhen, compileFinishWhen, compileWhen, evaluateFinishWhen, evaluateWhen, finishWhenFields, notAnyWhen, whenFields, type FinishWhenEval, type WhenEval, } from './finish-when.js';
|
|
4
|
-
export { action, agenda, branch, section, switchOn, topic, transfer } from './builders.js';
|
|
6
|
+
export { action, agenda, branch, ivr, section, switchOn, topic, transfer } from './builders.js';
|
|
5
7
|
export { extractBlocksFromTree, flattenAgendaConfig, flattenAgendaPlacements, flattenAgendaTree, isAgendaTree, isSequenceAgendas, normalizeSequenceAgendas, treeToPlacements, type FlattenOptions, type FlattenResult, } from './flatten.js';
|
|
6
|
-
export { LAST_TRANSFER_RESULT_FIELD, collectTransferBlocks, registerTransferSessionFields, transferFieldKey, transferResultField, } from './transfer-fields.js';
|
|
8
|
+
export { CASE_LAST_TRANSFER_AT, CASE_LAST_TRANSFER_RESULT, CASE_TRANSFER_ATTEMPT_COUNT, LAST_TRANSFER_RESULT_FIELD, collectTransferBlocks, registerTransferSessionFields, transferFieldKey, transferResultField, } from './transfer-fields.js';
|
|
9
|
+
export { collectIvrBlocks, ivrAttempted, ivrResultField, registerIvrSessionFields, type IvrAttemptResult, } from './ivr-fields.js';
|
|
7
10
|
export { fatalAgendaIssues, validateCompiledAgenda, type ValidateAgendaOptions } from './validate.js';
|
|
8
|
-
export {
|
|
11
|
+
export { RESERVED_INTERRUPT_TOOL_NAMES, collectTransferInterruptHeaders, headerHasTransferInterrupt, CONTACT_LANGUAGE_FIELD, getContactLang, inboundIvrCollect, isSpokenLanguage, ivrOf, languageFieldOf, validateInterrupts, type SpokenLanguage, } from './interrupts.js';
|
|
12
|
+
export { unresolvedFinishWhenFields, walkAgenda, type WalkDecision, type WalkHitReason, type WalkOptions, type WalkResult, type WalkSkip, type WalkSkipReason, type WalkStats, } from './walk.js';
|
|
9
13
|
export { simulateAgenda, type SimulateResult, type SimulatedItem, type SimulatedItemStatus } from './simulate.js';
|
|
10
14
|
export { DEFAULT_MAX_ACTIONS_PER_TURN, advanceAgenda, driveAgenda, transferWrites, watchdogHint, type AdvanceResult, type StageDriveHooks, type StageDriveResult, type StageDriveState, } from './drive.js';
|
|
11
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/stage/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
export { E164_NUMBER, SIP_URI, interpolateFromSnapshot, interpolateTemplate, isTransferDestinationLiteral, resolveTransferArguments, resolveTransferDestinations, resolveTransferSay, transferArgumentsJson, validateTransferDestinations, } from './transfer-spec.js';
|
|
1
2
|
export { collectJsonRulesFacts, evaluateJsonRules, extractTemplateFields, isOtherwise, jsonRules, looksLikeJsonRules, normalizeJsonRules, otherwise, parseJsonRules, } from './condition.js';
|
|
2
3
|
export { andCompiledWhen, compileAuthoringWhen, compileFinishWhen, compileWhen, evaluateFinishWhen, evaluateWhen, finishWhenFields, notAnyWhen, whenFields, } from './finish-when.js';
|
|
3
|
-
export { action, agenda, branch, section, switchOn, topic, transfer } from './builders.js';
|
|
4
|
+
export { action, agenda, branch, ivr, section, switchOn, topic, transfer } from './builders.js';
|
|
4
5
|
export { extractBlocksFromTree, flattenAgendaConfig, flattenAgendaPlacements, flattenAgendaTree, isAgendaTree, isSequenceAgendas, normalizeSequenceAgendas, treeToPlacements, } from './flatten.js';
|
|
5
|
-
export { LAST_TRANSFER_RESULT_FIELD, collectTransferBlocks, registerTransferSessionFields, transferFieldKey, transferResultField, } from './transfer-fields.js';
|
|
6
|
+
export { CASE_LAST_TRANSFER_AT, CASE_LAST_TRANSFER_RESULT, CASE_TRANSFER_ATTEMPT_COUNT, LAST_TRANSFER_RESULT_FIELD, collectTransferBlocks, registerTransferSessionFields, transferFieldKey, transferResultField, } from './transfer-fields.js';
|
|
7
|
+
export { collectIvrBlocks, ivrAttempted, ivrResultField, registerIvrSessionFields, } from './ivr-fields.js';
|
|
6
8
|
export { fatalAgendaIssues, validateCompiledAgenda } from './validate.js';
|
|
9
|
+
export { RESERVED_INTERRUPT_TOOL_NAMES, collectTransferInterruptHeaders, headerHasTransferInterrupt, CONTACT_LANGUAGE_FIELD, getContactLang, inboundIvrCollect, isSpokenLanguage, ivrOf, languageFieldOf, validateInterrupts, } from './interrupts.js';
|
|
7
10
|
export { unresolvedFinishWhenFields, walkAgenda, } from './walk.js';
|
|
8
11
|
export { simulateAgenda } from './simulate.js';
|
|
9
12
|
export { DEFAULT_MAX_ACTIONS_PER_TURN, advanceAgenda, driveAgenda, transferWrites, watchdogHint, } from './drive.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FieldPath } from '../slot/path.js';
|
|
2
|
+
import type { CompiledSlotSchema, ValidationIssue } from '../slot/types.js';
|
|
3
|
+
import type { AgendaHeader, FlatAgendaItem, FlatIvrItem, Interrupt, IvrDigit, IvrFallbackAction } from './types.js';
|
|
4
|
+
export type SpokenLanguage = string;
|
|
5
|
+
export declare const CONTACT_LANGUAGE_FIELD: "contact.language";
|
|
6
|
+
export declare const RESERVED_INTERRUPT_TOOL_NAMES: Set<string>;
|
|
7
|
+
export declare function isSpokenLanguage(value: unknown): value is SpokenLanguage;
|
|
8
|
+
export declare function ivrOf(items: FlatAgendaItem[] | null | undefined): FlatIvrItem | null;
|
|
9
|
+
export declare function languageFieldOf(): FieldPath;
|
|
10
|
+
/** Voice language. Empty / missing / non-language value → english. */
|
|
11
|
+
export declare function getContactLang(readField?: (path: FieldPath) => unknown): SpokenLanguage;
|
|
12
|
+
export declare function inboundIvrCollect(items: FlatAgendaItem[] | null | undefined): {
|
|
13
|
+
id: string;
|
|
14
|
+
fallbackAction: IvrFallbackAction;
|
|
15
|
+
confirmTimeoutMs: number;
|
|
16
|
+
pauseAfterSayMs: number;
|
|
17
|
+
finishOnKey?: IvrDigit | null;
|
|
18
|
+
minDigits?: number | null;
|
|
19
|
+
maxDigits?: number | null;
|
|
20
|
+
validateDigits: IvrDigit[];
|
|
21
|
+
} | null;
|
|
22
|
+
export declare function headerHasTransferInterrupt(header: AgendaHeader | null | undefined): boolean;
|
|
23
|
+
export declare function collectTransferInterruptHeaders(headers: Array<AgendaHeader | null | undefined>): boolean;
|
|
24
|
+
export declare function validateInterrupts(interrupts: Interrupt[], options: {
|
|
25
|
+
schema: CompiledSlotSchema;
|
|
26
|
+
agendaId?: string;
|
|
27
|
+
}): ValidationIssue[];
|
|
28
|
+
//# sourceMappingURL=interrupts.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getCompiledTable } from '../slot/compile-schema.js';
|
|
2
|
+
import { validateTransferDestinations } from './transfer-spec.js';
|
|
3
|
+
export const CONTACT_LANGUAGE_FIELD = 'contact.language';
|
|
4
|
+
export const RESERVED_INTERRUPT_TOOL_NAMES = new Set(['voice_turn', 'record_opt_out', 'advance']);
|
|
5
|
+
export function isSpokenLanguage(value) {
|
|
6
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
7
|
+
}
|
|
8
|
+
export function ivrOf(items) {
|
|
9
|
+
return items?.find((item) => item.kind === 'ivr') ?? null;
|
|
10
|
+
}
|
|
11
|
+
export function languageFieldOf() {
|
|
12
|
+
return CONTACT_LANGUAGE_FIELD;
|
|
13
|
+
}
|
|
14
|
+
function looksLikeSpokenLanguage(value) {
|
|
15
|
+
return isSpokenLanguage(value) && /[a-zA-Z]/.test(value);
|
|
16
|
+
}
|
|
17
|
+
/** Voice language. Empty / missing / non-language value → english. */
|
|
18
|
+
export function getContactLang(readField) {
|
|
19
|
+
if (!readField)
|
|
20
|
+
return 'english';
|
|
21
|
+
const raw = readField(CONTACT_LANGUAGE_FIELD);
|
|
22
|
+
return looksLikeSpokenLanguage(raw) ? raw.trim() : 'english';
|
|
23
|
+
}
|
|
24
|
+
export function inboundIvrCollect(items) {
|
|
25
|
+
const ivr = ivrOf(items);
|
|
26
|
+
if (!ivr)
|
|
27
|
+
return null;
|
|
28
|
+
const validateDigits = (ivr.validateDigits ?? []).filter((digit) => Boolean(digit?.trim()));
|
|
29
|
+
if (!ivr.finishOnKey && validateDigits.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
id: ivr.id,
|
|
33
|
+
fallbackAction: ivr.fallbackAction,
|
|
34
|
+
confirmTimeoutMs: ivr.confirmTimeoutMs,
|
|
35
|
+
pauseAfterSayMs: ivr.pauseAfterSayMs,
|
|
36
|
+
finishOnKey: ivr.finishOnKey,
|
|
37
|
+
minDigits: ivr.minDigits,
|
|
38
|
+
maxDigits: ivr.maxDigits,
|
|
39
|
+
validateDigits,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function headerHasTransferInterrupt(header) {
|
|
43
|
+
return header?.interrupts.some((interrupt) => interrupt.action.type === 'transfer') ?? false;
|
|
44
|
+
}
|
|
45
|
+
export function collectTransferInterruptHeaders(headers) {
|
|
46
|
+
return headers.some((header) => headerHasTransferInterrupt(header));
|
|
47
|
+
}
|
|
48
|
+
export function validateInterrupts(interrupts, options) {
|
|
49
|
+
const issues = [];
|
|
50
|
+
const prefix = `${options.agendaId ?? 'agenda'}/header/interrupts`;
|
|
51
|
+
const names = new Set();
|
|
52
|
+
const priorities = new Set();
|
|
53
|
+
for (const interrupt of interrupts) {
|
|
54
|
+
const path = `${prefix}/${interrupt.id}`;
|
|
55
|
+
if (RESERVED_INTERRUPT_TOOL_NAMES.has(interrupt.tool.name)) {
|
|
56
|
+
issues.push({
|
|
57
|
+
code: 'interrupt-tool-reserved',
|
|
58
|
+
message: `Interrupt tool name '${interrupt.tool.name}' is reserved`,
|
|
59
|
+
path,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (names.has(interrupt.tool.name)) {
|
|
63
|
+
issues.push({
|
|
64
|
+
code: 'interrupt-tool-duplicate',
|
|
65
|
+
message: `Interrupt tool name '${interrupt.tool.name}' is not unique`,
|
|
66
|
+
path,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
names.add(interrupt.tool.name);
|
|
70
|
+
if (priorities.has(interrupt.priority)) {
|
|
71
|
+
issues.push({
|
|
72
|
+
code: 'interrupt-priority-tie',
|
|
73
|
+
message: `Interrupt priority ${interrupt.priority} is not unique`,
|
|
74
|
+
path,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
priorities.add(interrupt.priority);
|
|
78
|
+
if (interrupt.action.type === 'switch-language') {
|
|
79
|
+
if (interrupt.action.languages.length === 0) {
|
|
80
|
+
issues.push({
|
|
81
|
+
code: 'interrupt-languages-empty',
|
|
82
|
+
message: 'switch-language languages must not be empty',
|
|
83
|
+
path: `${path}/languages`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (!getCompiledTable(options.schema, 'contact')?.fields.language) {
|
|
87
|
+
issues.push({
|
|
88
|
+
code: 'interrupt-language-field',
|
|
89
|
+
message: `switch-language requires ${CONTACT_LANGUAGE_FIELD}`,
|
|
90
|
+
path: `${path}/languages`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (interrupt.action.type === 'transfer') {
|
|
95
|
+
issues.push(...validateTransferDestinations(interrupt.action.numbers, {
|
|
96
|
+
path: `${path}/numbers`,
|
|
97
|
+
code: 'interrupt-transfer-number',
|
|
98
|
+
}));
|
|
99
|
+
issues.push(...validateCriteriaList(interrupt.action.escalateCriteria, `${path}/escalateCriteria`));
|
|
100
|
+
issues.push(...validateCriteriaList(interrupt.action.noEscalationCriteria, `${path}/noEscalationCriteria`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return issues;
|
|
104
|
+
}
|
|
105
|
+
function validateCriteriaList(list, path) {
|
|
106
|
+
if (!list)
|
|
107
|
+
return [];
|
|
108
|
+
const issues = [];
|
|
109
|
+
for (let index = 0; index < list.length; index++) {
|
|
110
|
+
if (list[index]?.trim())
|
|
111
|
+
continue;
|
|
112
|
+
issues.push({
|
|
113
|
+
code: 'interrupt-escalation-criteria',
|
|
114
|
+
message: 'escalation criteria entry must not be empty',
|
|
115
|
+
path: `${path}[${index}]`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return issues;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=interrupts.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FieldPath } from '../slot/path.js';
|
|
2
|
+
import type { CompiledSlotSchema, RuntimeSlotsSnapshot } from '../slot/types.js';
|
|
3
|
+
import type { AgendaBlock, AuthoringNode, FlatIvrItem, IvrBlock } from './types.js';
|
|
4
|
+
export type IvrAttemptResult = 'collected' | 'failed';
|
|
5
|
+
export declare function ivrResultField(nodeId: string): FieldPath;
|
|
6
|
+
export declare function ivrAttempted(item: FlatIvrItem, snapshot: RuntimeSlotsSnapshot): boolean;
|
|
7
|
+
export declare function collectIvrBlocks(blocks: Record<string, AgendaBlock>, nodes?: AuthoringNode[]): IvrBlock[];
|
|
8
|
+
export declare function registerIvrSessionFields(schema: CompiledSlotSchema, ivrIds: Iterable<string>): CompiledSlotSchema;
|
|
9
|
+
//# sourceMappingURL=ivr-fields.d.ts.map
|