@superpanel-sdk/runtime 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/builder/codegen/emit.d.ts +5 -10
- package/dist/builder/codegen/emit.js +100 -106
- package/dist/builder/codegen/generate.d.ts +3 -5
- package/dist/builder/codegen/generate.js +13 -32
- package/dist/builder/company-imports.d.ts +5 -0
- package/dist/builder/company-imports.js +9 -1
- package/dist/builder/compile/compile-sequence.d.ts +13 -11
- package/dist/builder/compile/compile-sequence.js +135 -83
- package/dist/builder/compile/function-code.d.ts +15 -9
- package/dist/builder/compile/function-code.js +59 -29
- package/dist/builder/compile/registry.d.ts +4 -4
- package/dist/builder/compile/registry.js +14 -12
- package/dist/builder/errors.d.ts +9 -0
- package/dist/builder/errors.js +17 -0
- package/dist/builder/function-params.d.ts +10 -0
- package/dist/builder/function-params.js +203 -0
- package/dist/builder/load.d.ts +17 -17
- package/dist/builder/load.js +27 -25
- package/dist/builder/paths.d.ts +6 -1
- package/dist/builder/paths.js +16 -2
- package/dist/builder/schedules.d.ts +9 -7
- package/dist/builder/schedules.js +20 -21
- package/dist/builder/typecheck.d.ts +1 -1
- package/dist/builder/typecheck.js +3 -6
- package/dist/builder/types.d.ts +41 -5
- package/dist/builder/types.js +19 -0
- package/dist/cli/calls/index.d.ts +177 -0
- package/dist/cli/calls/index.js +96 -0
- package/dist/cli/debug/index.d.ts +31 -0
- package/dist/cli/debug/index.js +119 -0
- package/dist/cli/exec/index.d.ts +39 -0
- package/dist/cli/exec/index.js +138 -0
- package/dist/cli/exec/session.d.ts +48 -0
- package/dist/cli/exec/session.js +82 -0
- package/dist/cli/exec/trace.d.ts +18 -0
- package/dist/cli/exec/trace.js +52 -0
- package/dist/cli/index.js +84 -1
- package/dist/cli/logs/index.d.ts +47 -0
- package/dist/cli/logs/index.js +113 -0
- package/dist/cli/mobile/index.d.ts +12 -0
- package/dist/cli/mobile/index.js +405 -0
- package/dist/cli/serve/index.d.ts +40 -4
- package/dist/cli/serve/index.js +167 -28
- package/dist/cli/turns/index.d.ts +105 -0
- package/dist/cli/turns/index.js +128 -0
- package/dist/cli/turns/run.d.ts +13 -0
- package/dist/cli/turns/run.js +57 -0
- package/dist/cli/turns/shadow.d.ts +112 -0
- package/dist/cli/turns/shadow.js +54 -0
- package/dist/cli/type-gen/index.js +0 -1
- package/dist/compute-host/compute-host.d.ts +5 -6
- package/dist/compute-host/compute-host.js +4 -13
- package/dist/compute-host/source-compute-host.d.ts +5 -7
- package/dist/compute-host/source-compute-host.js +12 -33
- package/dist/core/case-orchestrator/contact-hours.d.ts +26 -6
- package/dist/core/case-orchestrator/contact-hours.js +27 -8
- package/dist/core/case-orchestrator/index.d.ts +1 -4
- package/dist/core/case-orchestrator/index.js +1 -4
- package/dist/core/case-orchestrator/render.d.ts +1 -1
- package/dist/core/case-orchestrator/render.js +15 -10
- package/dist/core/case-orchestrator/types.d.ts +83 -128
- package/dist/core/case-orchestrator/types.js +0 -4
- package/dist/core/context/email-message.d.ts +40 -1
- package/dist/core/context/email-message.js +27 -0
- package/dist/core/context/email.d.ts +13 -1
- package/dist/core/context/email.js +2 -3
- package/dist/core/context/exchanges.d.ts +25 -0
- package/dist/core/context/exchanges.js +25 -7
- package/dist/core/context/instances.d.ts +31 -2
- package/dist/core/context/instances.js +18 -4
- package/dist/core/context/platform-registry.d.ts +35 -0
- package/dist/core/context/sms.d.ts +30 -0
- package/dist/core/context/sms.js +20 -0
- package/dist/core/context/types.d.ts +29 -1
- package/dist/core/conversation/index.d.ts +58 -4
- package/dist/core/conversation/index.js +123 -4
- package/dist/core/functions/errors.d.ts +74 -0
- package/dist/core/functions/errors.js +26 -0
- package/dist/core/functions/events.d.ts +23 -78
- package/dist/core/functions/events.js +8 -17
- package/dist/core/functions/schedules.d.ts +9 -10
- package/dist/core/functions/schedules.js +2 -2
- package/dist/core/functions/types.d.ts +17 -72
- package/dist/core/functions/types.js +1 -9
- package/dist/core/page/define.d.ts +230 -160
- package/dist/core/page/define.js +145 -104
- package/dist/core/page/envelope.d.ts +55 -0
- package/dist/core/page/envelope.js +4 -0
- package/dist/core/page/fns.d.ts +448 -0
- package/dist/core/page/fns.js +539 -0
- package/dist/core/page/memory-reader.d.ts +27 -0
- package/dist/core/page/memory-reader.js +299 -0
- package/dist/core/page/page-host.d.ts +20 -0
- package/dist/core/page/page-host.js +57 -0
- package/dist/core/page/records-query.d.ts +234 -0
- package/dist/core/page/records-query.js +780 -0
- package/dist/core/page/render.d.ts +67 -25
- package/dist/core/page/render.js +492 -272
- package/dist/core/page/sandbox.d.ts +85 -0
- package/dist/core/page/sandbox.js +357 -0
- package/dist/core/page/time-format.d.ts +27 -0
- package/dist/core/page/time-format.js +84 -0
- package/dist/core/page/typed.d.ts +165 -159
- package/dist/core/page/workspace.d.ts +25 -1
- package/dist/core/page/workspace.js +48 -13
- package/dist/core/party/schema.d.ts +74 -3
- package/dist/core/party/schema.js +39 -6
- package/dist/core/party/types.d.ts +8 -13
- package/dist/core/sequence/define.d.ts +1 -2
- package/dist/core/slot/company-metadata.d.ts +11 -22
- package/dist/core/slot/company-metadata.js +12 -39
- package/dist/core/slot/compile-schema.js +6 -6
- package/dist/core/slot/field.d.ts +1 -2
- package/dist/core/slot/field.js +0 -2
- package/dist/core/slot/index.d.ts +3 -3
- package/dist/core/slot/index.js +3 -3
- package/dist/core/slot/naming.d.ts +1 -1
- package/dist/core/slot/naming.js +1 -1
- package/dist/core/slot/platform-fields.d.ts +4 -4
- package/dist/core/slot/platform-fields.js +5 -5
- package/dist/core/slot/schema-compat.d.ts +8 -7
- package/dist/core/slot/schema-compat.js +22 -31
- package/dist/core/slot/schema.d.ts +3 -59
- package/dist/core/slot/schema.js +0 -15
- package/dist/core/slot/types.d.ts +4 -10
- package/dist/core/slot/value.d.ts +2 -0
- package/dist/core/slot/value.js +6 -2
- package/dist/dev-overlay/protocol.d.ts +38 -5
- package/dist/fn-context/build-context.js +10 -4
- package/dist/fn-context/fake/index.d.ts +2 -0
- package/dist/fn-context/fake/index.js +5 -3
- package/dist/fn-context/fn-params.d.ts +2 -5
- package/dist/fn-context/fn-params.js +6 -9
- package/dist/fn-context/index.d.ts +1 -1
- package/dist/fn-context/make-ctx.js +5 -4
- package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +11 -3
- package/dist/fn-context/rpc/case-orchestrator-tools.js +0 -1
- package/dist/fn-context/rpc/errors.d.ts +13 -0
- package/dist/fn-context/rpc/errors.js +22 -0
- package/dist/fn-context/rpc/index.d.ts +1 -1
- package/dist/fn-context/rpc/index.js +1 -1
- package/dist/fn-context/rpc/integration/index.d.ts +11 -1
- package/dist/fn-context/rpc/log.d.ts +11 -1
- package/dist/fn-context/rpc/platform.d.ts +11 -1
- package/dist/fn-context/rpc/records.d.ts +13 -6
- package/dist/fn-context/rpc/records.js +2 -4
- package/dist/fn-context/rpc/trpc-client.d.ts +42 -0
- package/dist/fn-context/rpc/trpc-client.js +151 -22
- package/dist/fn-context/rpc/trpc-init.d.ts +19 -1
- package/dist/fn-context/rpc/trpc-init.js +10 -4
- package/dist/fn-context/rpc/trpc-router-factory.d.ts +66 -8
- package/dist/fn-context/run-scope.js +1 -1
- package/dist/fn-context/types.d.ts +20 -6
- package/dist/index.d.ts +11 -12
- package/dist/index.js +5 -8
- package/dist/logger.d.ts +4 -0
- package/dist/logger.js +8 -0
- package/dist/platform.d.ts +32 -33
- package/dist/platform.js +23 -22
- package/dist/testing-compile-child.d.ts +2 -0
- package/dist/testing-compile-child.js +20 -0
- package/dist/testing.d.ts +368 -0
- package/dist/testing.js +439 -0
- package/dist/voice-service/actions.d.ts +12 -35
- package/dist/voice-service/index.d.ts +2 -0
- package/dist/voice-service/index.js +2 -0
- package/dist/voice-service/protocol.d.ts +604 -0
- package/dist/voice-service/protocol.js +67 -0
- package/dist/voice-service/types.d.ts +16 -6
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +2 -1
- package/package.json +20 -16
- package/dist/core/case-orchestrator/follow-ups.d.ts +0 -31
- package/dist/core/case-orchestrator/follow-ups.js +0 -103
- package/dist/core/case-orchestrator/tools.d.ts +0 -29
- package/dist/core/case-orchestrator/tools.js +0 -31
- package/dist/core/case-orchestrator/validate.d.ts +0 -11
- package/dist/core/case-orchestrator/validate.js +0 -63
- package/dist/core/functions/define/bind.d.ts +0 -25
- package/dist/core/functions/define/bind.js +0 -14
- package/dist/core/functions/define/case.d.ts +0 -9
- package/dist/core/functions/define/case.js +0 -13
- package/dist/core/functions/define/events.d.ts +0 -20
- package/dist/core/functions/define/events.js +0 -21
- package/dist/core/functions/define/no-case.d.ts +0 -4
- package/dist/core/functions/define/no-case.js +0 -9
- package/dist/core/functions/func-ref.d.ts +0 -25
- package/dist/core/functions/func-ref.js +0 -20
package/dist/core/page/render.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* returned its
|
|
2
|
+
* A page's envelope → `RenderedPage` (15 §10.9 output). Runs on the platform, after the page script
|
|
3
|
+
* returned its page from the sandbox: the envelope the page module made of its JSX (31 §8).
|
|
4
4
|
*
|
|
5
5
|
* The document is walked once. Every slot becomes a cell joined with its schema definition; its value
|
|
6
6
|
* is taken from what the platform itself read (`PageReads`), never from the document, and a slot
|
|
@@ -12,57 +12,67 @@
|
|
|
12
12
|
* choose among the rows of their table there (15 §10.15 point 9). So lookups and actions are rendered
|
|
13
13
|
* after the walk, once every row on the page is known.
|
|
14
14
|
*
|
|
15
|
-
* Cells are addressed by slot id `table
|
|
15
|
+
* Cells are addressed by slot id `table:<uuid tail>.field` (`RowNames`: the row's own, layout-free);
|
|
16
16
|
* actions and blocks by their page path (`injuries.rows.0.markError`), resolved against the very page
|
|
17
17
|
* the AI saw.
|
|
18
18
|
*/
|
|
19
19
|
import { parentOf } from '../slot/company-metadata.js';
|
|
20
20
|
import { ROW_ID_FIELD } from '../slot/schema.js';
|
|
21
21
|
import { getCompiledTable, getTableField } from '../slot/compile-schema.js';
|
|
22
|
-
import { isChoose } from './define.js';
|
|
22
|
+
import { isChoose, PAGE_TOOLS } from './define.js';
|
|
23
|
+
import { isEnvelope } from './envelope.js';
|
|
24
|
+
import { CONTACT_TABLE } from '../slot/company-metadata.js';
|
|
25
|
+
import { fallbackSpoken } from '../conversation/index.js';
|
|
23
26
|
// ---------------------------------------------------------------------------------------------
|
|
24
27
|
// Row names (15 §10.6.2)
|
|
25
28
|
/**
|
|
26
|
-
* `table
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* `table:` and the last 12 hex digits of the row's uuid (33): what the row is called in logs, the
|
|
30
|
+
* timeline and the database too, so the same in every conversation and every process; nothing to keep.
|
|
31
|
+
* A name comes from the id alone, never from which row came first: two rows of a table alike in those
|
|
32
|
+
* digits (one page of n rows: about n² / 2⁴⁹) are both named by their whole id from then on, and the
|
|
33
|
+
* short name they share resolves to neither. So a name from an earlier conversation, or another
|
|
34
|
+
* process, means the row it meant or no row (the AI is told), never another row. A name resolves only
|
|
35
|
+
* to a row named here, and the AI's names are checked against the page's own (`one-of`, `enum`) first.
|
|
29
36
|
*/
|
|
30
37
|
export class RowNames {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
/** The rows named here: id → table. */
|
|
39
|
+
tables = new Map();
|
|
40
|
+
/** A short name → the ids of its table ending in its digits. */
|
|
41
|
+
byShort = new Map();
|
|
34
42
|
name(table, id) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.byName.set(name, { table, id });
|
|
43
|
-
return name;
|
|
43
|
+
this.tables.set(id, table);
|
|
44
|
+
const short = shortRowName(table, id);
|
|
45
|
+
let ids = this.byShort.get(short);
|
|
46
|
+
if (!ids)
|
|
47
|
+
this.byShort.set(short, (ids = new Set()));
|
|
48
|
+
ids.add(id);
|
|
49
|
+
return ids.size > 1 ? `${table}:${id}` : short;
|
|
44
50
|
}
|
|
45
51
|
resolve(name) {
|
|
46
|
-
|
|
52
|
+
const at = name.indexOf(':');
|
|
53
|
+
if (at < 0)
|
|
54
|
+
return undefined;
|
|
55
|
+
const table = name.slice(0, at);
|
|
56
|
+
const rest = name.slice(at + 1);
|
|
57
|
+
if (this.tables.get(rest) === table)
|
|
58
|
+
return { table, id: rest };
|
|
59
|
+
const ids = this.byShort.get(name);
|
|
60
|
+
if (ids?.size !== 1)
|
|
61
|
+
return undefined;
|
|
62
|
+
const [id] = ids;
|
|
63
|
+
return { table, id: id };
|
|
47
64
|
}
|
|
48
65
|
nameOf(id) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
static fromJSON(entries) {
|
|
55
|
-
const names = new RowNames();
|
|
56
|
-
for (const [name, table, id] of entries) {
|
|
57
|
-
names.byId.set(id, name);
|
|
58
|
-
names.byName.set(name, { table, id });
|
|
59
|
-
const n = Number(name.slice(name.lastIndexOf('#') + 1));
|
|
60
|
-
if (n > (names.counters.get(table) ?? 0))
|
|
61
|
-
names.counters.set(table, n);
|
|
62
|
-
}
|
|
63
|
-
return names;
|
|
66
|
+
const table = this.tables.get(id);
|
|
67
|
+
if (table === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
const short = shortRowName(table, id);
|
|
70
|
+
return this.byShort.get(short).size > 1 ? `${table}:${id}` : short;
|
|
64
71
|
}
|
|
65
72
|
}
|
|
73
|
+
function shortRowName(table, id) {
|
|
74
|
+
return `${table}:${id.slice(-12)}`;
|
|
75
|
+
}
|
|
66
76
|
export class PageDocumentError extends Error {
|
|
67
77
|
constructor(message) {
|
|
68
78
|
super(message);
|
|
@@ -72,30 +82,23 @@ export class PageDocumentError extends Error {
|
|
|
72
82
|
const MAX_DEPTH = 32;
|
|
73
83
|
/** Channel actions only a call can do; by text only `end` (17b §9.8). */
|
|
74
84
|
const CALL_ONLY = new Set(['transfer', 'keypad', 'announce', 'voicemail']);
|
|
85
|
+
/** Tools only a call can use (76 §2.18). */
|
|
86
|
+
const CALL_ONLY_TOOLS = new Set(['pressKeypad']);
|
|
75
87
|
export function renderDocument(input) {
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
throw new PageDocumentError(
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
const inner = renderDocument({ ...input, document: { action: perform.action } });
|
|
83
|
-
const action = inner.actions['action'];
|
|
84
|
-
if (!action)
|
|
85
|
-
throw new PageDocumentError(`the action the page function returned cannot run: ${inner.warnings.join('; ') || 'no row to run it on'}`);
|
|
86
|
-
return { ...inner, auto: { action, key: perform.key ?? actionKey(perform.action), wait: perform.wait ?? true } };
|
|
87
|
-
}
|
|
88
|
+
// Only what the page module marked (31 §8): an object shaped like a button is not one.
|
|
89
|
+
if (!isEnvelope(input.document))
|
|
90
|
+
throw new PageDocumentError('the page function returns page(…) or doNow(…) (76 §2.1)');
|
|
91
|
+
const envelope = input.document;
|
|
92
|
+
if ('do' in envelope)
|
|
93
|
+
return renderDo(input, envelope.do);
|
|
88
94
|
const { schema, reads, names } = input;
|
|
89
|
-
|
|
90
|
-
throw new PageDocumentError('the page script must return an object');
|
|
95
|
+
const envelopePage = envelope.page;
|
|
91
96
|
const cells = {};
|
|
92
97
|
const actions = {};
|
|
93
98
|
const rows = {};
|
|
94
99
|
const warnings = [];
|
|
95
|
-
/** Rows with a cell on the page, by table (page names). */
|
|
96
|
-
const cellRows = new Map();
|
|
97
100
|
const pendingActions = [];
|
|
98
|
-
const extractions = pageExtractions(
|
|
101
|
+
const extractions = pageExtractions(envelopePage.extraction, schema, warnings);
|
|
99
102
|
const rowName = (table, id) => {
|
|
100
103
|
const name = names.name(table, id);
|
|
101
104
|
if (!rows[name]) {
|
|
@@ -105,7 +108,6 @@ export function renderDocument(input) {
|
|
|
105
108
|
}
|
|
106
109
|
return name;
|
|
107
110
|
};
|
|
108
|
-
const rowsOnPage = (table) => [...(cellRows.get(table) ?? [])];
|
|
109
111
|
const cellOf = (key, path, value) => {
|
|
110
112
|
const refCell = isRefFieldCell(value) ? value : null;
|
|
111
113
|
const fieldCell = isFieldCell(value) ? value : refCell ? { $field: true, slot: refCell.slot, ...(refCell.readOnly ? { readOnly: true } : {}) } : null;
|
|
@@ -116,33 +118,30 @@ export function renderDocument(input) {
|
|
|
116
118
|
const read = reads.row(ref.id);
|
|
117
119
|
const table = getCompiledTable(schema, ref.table);
|
|
118
120
|
const def = table?.fields[ref.field];
|
|
121
|
+
// The row id and the parent never change and never are empty: nothing a field could show or open.
|
|
122
|
+
// Who the row belongs to is said in words, `text(…)` (31 §4).
|
|
123
|
+
const isParent = table ? parentOf(table)?.field === ref.field : false;
|
|
124
|
+
if (isParent || ref.field === ROW_ID_FIELD) {
|
|
125
|
+
throw new PageDocumentError(`${path}: ${ref.table}.${ref.field} is ${isParent ? 'the parent' : 'the row id'}: it never changes, so it is not a field on the page; say who the row belongs to with text(…), e.g. case: text("Zhang San's car accident case")`);
|
|
126
|
+
}
|
|
119
127
|
if (!read || read.table !== ref.table || !def || !(ref.field in read.slots)) {
|
|
120
128
|
warnings.push(`${path}: a slot this run did not read (${ref.table}.${ref.field}); shown as read-only text`);
|
|
121
129
|
return { kind: 'text', key, path, value: displayable(slot.value) };
|
|
122
130
|
}
|
|
123
131
|
const truth = read.slots[ref.field];
|
|
124
132
|
const name = rowName(ref.table, ref.id);
|
|
125
|
-
const onPage = cellRows.get(ref.table) ?? new Set();
|
|
126
|
-
onPage.add(name);
|
|
127
|
-
cellRows.set(ref.table, onPage);
|
|
128
133
|
const slotId = `${name}.${ref.field}`;
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
if (isLookup &&
|
|
133
|
-
throw new PageDocumentError(`${path}: ${ref.table}.${ref.field} is a ref field: place it with refField(slot, { options })`);
|
|
134
|
+
const isLookup = Boolean(def.ref);
|
|
135
|
+
// A ref field is placed with refField, read-only too: its candidates and their labels, or who its value
|
|
136
|
+
// is, are the page's (26 §16.2 #7); a bare row name says nothing (31 §4).
|
|
137
|
+
if (isLookup && !refCell)
|
|
138
|
+
throw new PageDocumentError(`${path}: ${ref.table}.${ref.field} is a ref field: place it with refField(slot, { options }) (read-only, refField(slot, { readOnly: true, label: '…' }))`);
|
|
134
139
|
if (refCell && !isLookup)
|
|
135
|
-
throw new PageDocumentError(`${path}: refField(...) is for a ref field; ${ref.table}.${ref.field} is
|
|
140
|
+
throw new PageDocumentError(`${path}: refField(...) is for a ref field; ${ref.table}.${ref.field} is not a ref`);
|
|
136
141
|
const offered = refCell ? refOptions(path, refCell, def.ref.table) : null;
|
|
137
|
-
// The page decides (19-page-writable-cells):
|
|
138
|
-
// it. Never the row id or the parent. A lookup opens where its table's rows are on the page
|
|
139
|
-
// (settled below): the AI cannot make up a row id.
|
|
140
|
-
const fixed = isParent || ref.field === ROW_ID_FIELD;
|
|
141
|
-
if (fieldCell && !fieldCell.readOnly && fixed) {
|
|
142
|
-
warnings.push(`${path}: field(...) cannot open ${ref.table}.${ref.field} (${isParent ? 'the parent' : 'the row id'}); it stays read-only`);
|
|
143
|
-
}
|
|
142
|
+
// The page decides (19-page-writable-cells): `field(slot, { readOnly: true })` is read-only, `field(slot)` opens it.
|
|
144
143
|
// A ref field with no row to offer is shown, not opened: nothing to choose (data, not a page mistake).
|
|
145
|
-
const placeWritable = fieldCell !== null && !fieldCell.readOnly &&
|
|
144
|
+
const placeWritable = fieldCell !== null && !fieldCell.readOnly && (!offered || offered.length > 0);
|
|
146
145
|
const existing = cells[slotId];
|
|
147
146
|
if (existing) {
|
|
148
147
|
existing.writable ||= placeWritable;
|
|
@@ -170,6 +169,11 @@ export function renderDocument(input) {
|
|
|
170
169
|
if (isLookup)
|
|
171
170
|
cell.type = 'ref';
|
|
172
171
|
cells[slotId] = cell;
|
|
172
|
+
// The row a ref holds is named even when nothing else of it is on the page (33): the AI reads
|
|
173
|
+
// `case_contact:3f9a2c1d7e04`, never a bare id. Named only: it does not join `rows`, which is
|
|
174
|
+
// what the page's actions may write.
|
|
175
|
+
if (def.ref && truth.state === 'filled' && typeof truth.value === 'string')
|
|
176
|
+
cell.valueRow = names.name(def.ref.table, truth.value);
|
|
173
177
|
}
|
|
174
178
|
if (offered) {
|
|
175
179
|
const target = cells[slotId];
|
|
@@ -191,59 +195,104 @@ export function renderDocument(input) {
|
|
|
191
195
|
throw new PageDocumentError(`${path}: refField offers ${option.id}, not a ${target} row this run read`);
|
|
192
196
|
return { name: rowName(target, option.id), label: option.label };
|
|
193
197
|
});
|
|
194
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Data (31 §4): shown as it is, blocks, lists and text, whatever keys it has (`$action` too). Nothing in
|
|
200
|
+
* it is a field or a button: the page module marked those, and data cannot carry the mark.
|
|
201
|
+
*/
|
|
202
|
+
const walkData = (key, path, value, depth) => {
|
|
195
203
|
if (depth > MAX_DEPTH)
|
|
196
204
|
throw new PageDocumentError(`${path || 'page'}: nested deeper than ${MAX_DEPTH}`);
|
|
197
|
-
if (value ===
|
|
198
|
-
return null;
|
|
199
|
-
if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
205
|
+
if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
200
206
|
return { kind: 'text', key, path, value };
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
pendingActions.push({ path, value });
|
|
204
|
-
return { kind: 'action', key, path, actionId: path };
|
|
205
|
-
}
|
|
206
|
-
const cell = cellOf(key, path, value);
|
|
207
|
-
if (cell)
|
|
208
|
-
return cell;
|
|
209
|
-
if (Array.isArray(value)) {
|
|
210
|
-
const children = value
|
|
211
|
-
.map((item, i) => walk(String(i), join(path, String(i)), item, depth + 1))
|
|
212
|
-
.filter((n) => n !== null);
|
|
213
|
-
return { kind: 'block', key, path, list: true, children };
|
|
214
|
-
}
|
|
207
|
+
if (Array.isArray(value))
|
|
208
|
+
return { kind: 'block', key, path, list: true, children: value.map((item, i) => walkData(String(i), join(path, String(i)), item, depth + 1)) };
|
|
215
209
|
if (isPlainObject(value)) {
|
|
216
|
-
|
|
217
|
-
|
|
210
|
+
return { kind: 'block', key, path, list: false, children: Object.entries(value).map(([k, item]) => walkData(k, join(path, k), item, depth + 1)) };
|
|
211
|
+
}
|
|
212
|
+
return { kind: 'text', key, path, value: String(value) };
|
|
213
|
+
};
|
|
214
|
+
/** One node of the envelope (31 §8): what the page module marked as a block, a field, a button, or data. */
|
|
215
|
+
const walkEnvelope = (node, parent, place, depth) => {
|
|
216
|
+
if (depth > MAX_DEPTH)
|
|
217
|
+
throw new PageDocumentError(`${parent || 'page'}: nested deeper than ${MAX_DEPTH}`);
|
|
218
|
+
// Where it is, for messages: its name, or its place when it has none (46 §2.2).
|
|
219
|
+
const path = join(parent, node.key || `#${place}`);
|
|
220
|
+
switch (node.k) {
|
|
221
|
+
case 'block': {
|
|
222
|
+
const block = { kind: 'block', key: node.key, path, list: node.list, children: [] };
|
|
223
|
+
if (node.title !== undefined)
|
|
224
|
+
block.title = node.title;
|
|
225
|
+
if (node.description !== undefined)
|
|
226
|
+
block.description = node.description;
|
|
227
|
+
node.children.forEach((child, i) => {
|
|
228
|
+
const out = walkEnvelope(child, path, i, depth + 1);
|
|
229
|
+
if (out)
|
|
230
|
+
block.children.push(out);
|
|
231
|
+
});
|
|
232
|
+
return block;
|
|
218
233
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
case 'field': {
|
|
235
|
+
const cell = cellOf(node.key, path, node.cell);
|
|
236
|
+
if (!cell)
|
|
237
|
+
throw new PageDocumentError(`${path}: not a field`);
|
|
238
|
+
return cell;
|
|
239
|
+
}
|
|
240
|
+
case 'button': {
|
|
241
|
+
// Its id comes once every button is known (46 §2.1).
|
|
242
|
+
const action = { kind: 'action', key: '', path, actionId: '' };
|
|
243
|
+
pendingActions.push({ path, value: node.action, node: action });
|
|
244
|
+
return action;
|
|
245
|
+
}
|
|
246
|
+
case 'data':
|
|
247
|
+
return walkData(node.key, path, node.value, depth);
|
|
248
|
+
case 'toolbox': {
|
|
249
|
+
// The tools as buttons in their own block (76 §2.18): the AI presses one like any button.
|
|
250
|
+
const block = { kind: 'block', key: node.key, path, list: false, title: 'Tools', children: [] };
|
|
251
|
+
node.tools.forEach((tool, i) => {
|
|
252
|
+
if (!PAGE_TOOLS.includes(tool))
|
|
253
|
+
throw new PageDocumentError(`${path}: ${tool} is not a platform tool (${PAGE_TOOLS.join(', ')})`);
|
|
254
|
+
const at = join(path, String(i));
|
|
255
|
+
// A call tool on a text or an email page (76 §2.12): left off, said in the log.
|
|
256
|
+
if (input.channel !== undefined && input.channel !== 'call' && CALL_ONLY_TOOLS.has(tool)) {
|
|
257
|
+
warnings.push(`${at}: ${tool}() is a call tool, and this is ${input.channel}: left off the page (place it only on a call)`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const action = { kind: 'action', key: '', path: at, actionId: '' };
|
|
261
|
+
pendingActions.push({ path: action.path, value: { $action: 'tool', tool: tool }, node: action });
|
|
262
|
+
block.children.push(action);
|
|
263
|
+
});
|
|
264
|
+
// Every tool left off (or none given): no toolbox on the page (76 §2.12).
|
|
265
|
+
return block.children.length ? block : null;
|
|
234
266
|
}
|
|
235
|
-
return block;
|
|
236
267
|
}
|
|
237
|
-
return { kind: 'text', key, path, value: String(value) };
|
|
238
268
|
};
|
|
239
|
-
const root =
|
|
269
|
+
const root = { kind: 'block', key: '', path: '', list: false, children: [] };
|
|
270
|
+
if (envelopePage.title !== undefined)
|
|
271
|
+
root.title = envelopePage.title;
|
|
272
|
+
if (envelopePage.description !== undefined)
|
|
273
|
+
root.description = envelopePage.description;
|
|
274
|
+
if (envelopePage.guide?.trim())
|
|
275
|
+
root.guide = envelopePage.guide;
|
|
276
|
+
envelopePage.children.forEach((child, i) => {
|
|
277
|
+
const out = walkEnvelope(child, '', i, 1);
|
|
278
|
+
if (out)
|
|
279
|
+
root.children.push(out);
|
|
280
|
+
});
|
|
281
|
+
const voice = envelopePage.voice;
|
|
240
282
|
// Every row on the page is known now: what lookups and row parameters may choose.
|
|
241
|
-
const env = { schema, reads, rowName,
|
|
242
|
-
|
|
283
|
+
const env = { schema, reads, rowName, warnings, ...(input.channel ? { channel: input.channel } : {}), ...(input.functions ? { functions: input.functions } : {}), ...(input.actions ? { actions: input.actions } : {}) };
|
|
284
|
+
const placed = [];
|
|
285
|
+
for (const { path, value, node } of pendingActions) {
|
|
243
286
|
const rendered = renderAction(path, value, env);
|
|
244
287
|
if (rendered)
|
|
245
|
-
|
|
288
|
+
placed.push({ action: rendered, node });
|
|
246
289
|
}
|
|
290
|
+
const ids = buttonIds(placed.map((p) => p.action), rows);
|
|
291
|
+
placed.forEach(({ action, node }, i) => {
|
|
292
|
+
const id = ids[i];
|
|
293
|
+
node.actionId = id;
|
|
294
|
+
actions[id] = { ...action, id };
|
|
295
|
+
});
|
|
247
296
|
// An action that cannot be pressed is not on the page.
|
|
248
297
|
const prune = (block) => {
|
|
249
298
|
block.children = block.children.filter((child) => child.kind !== 'action' || child.actionId in actions);
|
|
@@ -255,8 +304,27 @@ export function renderDocument(input) {
|
|
|
255
304
|
const page = { root, cells, actions, rows, warnings };
|
|
256
305
|
if (extractions.size)
|
|
257
306
|
page.extractions = Object.fromEntries(extractions);
|
|
307
|
+
if (voice !== undefined)
|
|
308
|
+
page.voice = voice;
|
|
258
309
|
return page;
|
|
259
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* `doNow()` (76): what the page does now, rendered as a button would be so its rows are checked the same
|
|
313
|
+
* way. Nobody fills what the page left out: a required parameter it did not prefill is an error (30 §7.2).
|
|
314
|
+
*/
|
|
315
|
+
function renderDo(input, run) {
|
|
316
|
+
if (run.action.$action === 'channel' && input.channel !== undefined && input.channel !== 'call' && CALL_ONLY.has(run.action.channel)) {
|
|
317
|
+
throw new PageDocumentError(`the page function returned ${run.action.channel}, which only a call can do (this is ${input.channel})`);
|
|
318
|
+
}
|
|
319
|
+
const inner = renderDocument({ ...input, document: { $envelope: 1, page: { children: [{ k: 'button', key: '', action: run.action }] } } });
|
|
320
|
+
const action = Object.values(inner.actions)[0];
|
|
321
|
+
if (!action)
|
|
322
|
+
throw new PageDocumentError(`the action the page function returned cannot run: ${inner.warnings.join('; ') || 'no row to run it on'}`);
|
|
323
|
+
const missing = action.input?.required ?? [];
|
|
324
|
+
if (missing.length)
|
|
325
|
+
throw new PageDocumentError(`doNow(): ${action.name} runs with nobody to fill ${missing.join(', ')}: prefill ${missing.length > 1 ? 'them' : 'it'}`);
|
|
326
|
+
return { ...inner, auto: { action, key: run.key ?? actionKey(run.action), wait: run.wait ?? true } };
|
|
327
|
+
}
|
|
260
328
|
/**
|
|
261
329
|
* The action as the AI presses it, or null when it cannot be pressed on this page: with a warning when
|
|
262
330
|
* the page placed it wrong; silently when a required row choice has no row on the page (no injury yet
|
|
@@ -290,30 +358,65 @@ function unfillable(properties, required, prefix = '') {
|
|
|
290
358
|
function renderActionOf(path, action, env) {
|
|
291
359
|
const { schema, reads, rowName, warnings } = env;
|
|
292
360
|
if (action.$action === 'channel') {
|
|
293
|
-
// Not on this channel (a transfer by text):
|
|
361
|
+
// Not on this channel (a transfer by text, 76 §2.12): left off the page, said in the log; the page
|
|
362
|
+
// should check the channel itself.
|
|
294
363
|
if (env.channel !== undefined && env.channel !== 'call' && CALL_ONLY.has(action.channel)) {
|
|
295
|
-
|
|
364
|
+
env.warnings.push(`${path}: ${action.channel} is a call action, and this is ${env.channel}: left off the page (place it only on a call)`);
|
|
365
|
+
return null;
|
|
296
366
|
}
|
|
297
367
|
return channelButton(path, action);
|
|
298
368
|
}
|
|
369
|
+
if (action.$action === 'tool') {
|
|
370
|
+
if (env.channel !== undefined && env.channel !== 'call' && CALL_ONLY_TOOLS.has(action.tool)) {
|
|
371
|
+
env.warnings.push(`${path}: ${action.tool}() is a call tool, and this is ${env.channel}: left off the page (place it only on a call)`);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
return toolButton(path, action.tool);
|
|
375
|
+
}
|
|
299
376
|
if (action.$action === 'createRow') {
|
|
300
377
|
const table = getCompiledTable(schema, action.table);
|
|
301
378
|
if (!table)
|
|
302
379
|
throw new PageDocumentError(`${path}: createRow on ${action.table}, not a table of this sequence`);
|
|
303
380
|
const parent = parentOf(table);
|
|
304
|
-
|
|
381
|
+
const fieldParam = (field) => ({ $schemaField: `${action.table}.${field}` });
|
|
382
|
+
const properties = {};
|
|
383
|
+
// A ref field is chosen among the rows the page gives (`options`, 35 §2), or fixed (`prefill`).
|
|
384
|
+
const chosen = [];
|
|
385
|
+
for (const [key, value] of Object.entries(action.prefill)) {
|
|
305
386
|
if (!getTableField(table, key))
|
|
306
387
|
throw new PageDocumentError(`${path}: createRow(${action.table}) prefills ${key}, not a field of it`);
|
|
388
|
+
if (isChoose(value)) {
|
|
389
|
+
if (parent?.field === key)
|
|
390
|
+
throw new PageDocumentError(`${path}: the parent ${key} is where the row goes, not the AI's choice: prefill it`);
|
|
391
|
+
const offered = chooseParam(`${path}: options.${key}`, fieldParam(key), value, env);
|
|
392
|
+
if (emptyOptional(offered, key, action.required))
|
|
393
|
+
continue;
|
|
394
|
+
properties[key] = offered;
|
|
395
|
+
chosen.push(key);
|
|
396
|
+
}
|
|
397
|
+
else if (parent?.field !== key) {
|
|
398
|
+
checkPrefill(`${path}: prefill.${key}`, fieldParam(key), value, env);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// A required field the page prefilled empty (null or '', as the write counts it): nobody fills it,
|
|
402
|
+
// and the write would fail (76 §2.14).
|
|
403
|
+
for (const key of action.required) {
|
|
404
|
+
const value = Object.hasOwn(action.prefill, key) ? action.prefill[key] : undefined;
|
|
405
|
+
if (value === null || value === '') {
|
|
406
|
+
throw new PageDocumentError(`${path}: createRow(${action.table}) ${key} is required: prefill a value, not ${value === null ? 'null' : "''"}, or leave it to the AI (fields)`);
|
|
407
|
+
}
|
|
307
408
|
}
|
|
308
|
-
const properties = {};
|
|
309
409
|
for (const fieldName of action.fields) {
|
|
310
|
-
|
|
410
|
+
const def = getTableField(table, fieldName);
|
|
411
|
+
if (!def)
|
|
311
412
|
throw new PageDocumentError(`${path}: createRow field ${action.table}.${fieldName} is not in this sequence`);
|
|
312
413
|
if (parent?.field === fieldName)
|
|
313
414
|
throw new PageDocumentError(`${path}: the parent ${fieldName} is prefilled, not a field to fill`);
|
|
314
415
|
if (fieldName in action.prefill)
|
|
315
416
|
continue;
|
|
316
|
-
|
|
417
|
+
if (def.ref)
|
|
418
|
+
throw new PageDocumentError(`${path}: ${action.table}.${fieldName} is a ref field: give the rows the AI chooses among, options: { ${fieldName}: choose(rows, label) }, or prefill it`);
|
|
419
|
+
properties[fieldName] = expandParam(path, fieldParam(fieldName), env);
|
|
317
420
|
}
|
|
318
421
|
// Where the new row hangs: prefilled, else the one parent this run's reads of the table were anchored on.
|
|
319
422
|
let parentId = null;
|
|
@@ -334,9 +437,7 @@ function renderActionOf(path, action, env) {
|
|
|
334
437
|
}
|
|
335
438
|
rowName(parentRow.table, parentId);
|
|
336
439
|
}
|
|
337
|
-
const set =
|
|
338
|
-
if (parent)
|
|
339
|
-
delete set[parent.field];
|
|
440
|
+
const set = Object.fromEntries(Object.entries(action.prefill).filter(([key, value]) => !isChoose(value) && key !== parent?.field));
|
|
340
441
|
const under = parent && parentId ? ` under ${rowName(parent.table, parentId)}` : '';
|
|
341
442
|
return {
|
|
342
443
|
id: path,
|
|
@@ -344,7 +445,7 @@ function renderActionOf(path, action, env) {
|
|
|
344
445
|
name: action.label ?? `Add ${table.label}`,
|
|
345
446
|
description: action.description ?? `Adds a new ${table.label} (${action.table})${under}.`,
|
|
346
447
|
input: { type: 'object', properties, required: action.required.filter((r) => r in properties) },
|
|
347
|
-
server: { kind: 'createRow', table: action.table, parentId, set, fields: action.fields, required: action.required },
|
|
448
|
+
server: { kind: 'createRow', table: action.table, parentId, set, fields: [...action.fields, ...chosen], required: action.required, ...(action.key ? { key: action.key } : {}) },
|
|
348
449
|
};
|
|
349
450
|
}
|
|
350
451
|
if (action.$action === 'deleteRow') {
|
|
@@ -360,40 +461,50 @@ function renderActionOf(path, action, env) {
|
|
|
360
461
|
name: action.label ?? `Delete ${name}`,
|
|
361
462
|
description: action.description ?? `Delete ${name} and everything under it.`,
|
|
362
463
|
input: null,
|
|
363
|
-
server: { kind: 'deleteRow', table: read.table, rowId: action.rowId, ...(action.reason ? { reason: action.reason } : {}) },
|
|
464
|
+
server: { kind: 'deleteRow', table: read.table, rowId: action.rowId, ...(action.reason ? { reason: action.reason } : {}), ...(action.key ? { key: action.key } : {}) },
|
|
364
465
|
};
|
|
365
466
|
}
|
|
366
|
-
if (action.$action === 'message')
|
|
367
|
-
return messageButton(path, action, env);
|
|
368
467
|
if (action.$action === 'followUp')
|
|
369
|
-
return followUpButton(path, action);
|
|
468
|
+
return followUpButton(path, action, env);
|
|
469
|
+
if (action.$action === 'cancelFollowUp')
|
|
470
|
+
return cancelFollowUpButton(path, action);
|
|
370
471
|
if (action.$action === 'function') {
|
|
371
|
-
|
|
472
|
+
// The function's parameters are its definition's (31 §7); a page may still declare them on the button.
|
|
473
|
+
const parameters = action.parameters ?? env.functions?.[action.name]?.parameters;
|
|
474
|
+
const { input, prefill } = actionInput(path, action.name, parameters, action.prefill ?? {}, env, action.args);
|
|
372
475
|
return {
|
|
373
476
|
id: path,
|
|
374
477
|
kind: 'function',
|
|
375
478
|
name: action.label ?? action.name,
|
|
376
479
|
description: action.description ?? `Runs ${action.name}.`,
|
|
377
480
|
input,
|
|
378
|
-
server: { kind: 'function', name: action.name, ...(action.say ? { say: action.say } : {}), ...(Object.keys(prefill).length ? { prefill } : {}) },
|
|
481
|
+
server: { kind: 'function', name: action.name, ...(action.say ? { say: action.say } : {}), ...(Object.keys(prefill).length ? { prefill } : {}), ...(action.key ? { key: action.key } : {}) },
|
|
379
482
|
};
|
|
380
483
|
}
|
|
381
|
-
|
|
484
|
+
// A page action's parameters are its `args` type's, as the build read it (38 §2.9).
|
|
485
|
+
const parameters = action.parameters ?? env.actions?.[action.name]?.parameters;
|
|
486
|
+
const { input, prefill } = actionInput(path, action.name, parameters, action.prefill, env, action.args);
|
|
382
487
|
return {
|
|
383
488
|
id: path,
|
|
384
489
|
kind: 'custom',
|
|
385
|
-
name: action.name,
|
|
386
|
-
description: action.description
|
|
490
|
+
name: action.label ?? action.name,
|
|
491
|
+
description: action.description ?? `Runs ${action.name}.`,
|
|
387
492
|
input,
|
|
388
|
-
server: { kind: 'custom', name: action.name, prefill },
|
|
493
|
+
server: { kind: 'custom', name: action.name, prefill, ...(action.key ? { key: action.key } : {}) },
|
|
389
494
|
};
|
|
390
495
|
}
|
|
496
|
+
/** An optional row with no row to choose: not asked (a required one leaves its button off, `renderAction`). */
|
|
497
|
+
function emptyOptional(offered, key, required) {
|
|
498
|
+
return offered.type === 'ref' && offered.enum.length === 0 && !required.includes(key);
|
|
499
|
+
}
|
|
391
500
|
/**
|
|
392
|
-
* What the AI fills of an action's parameters, and what the page fixed. A prefilled row is
|
|
393
|
-
* put on the page; a `choose(...)`
|
|
394
|
-
* with its label.
|
|
501
|
+
* What the AI fills of an action's parameters, and what the page fixed (35 §2). A prefilled row is
|
|
502
|
+
* checked and put on the page; a row in `options` (`choose(...)`) becomes the AI's choice among exactly
|
|
503
|
+
* those rows, each shown with its label. A row parameter with neither is the page's mistake: the AI is
|
|
504
|
+
* never left to guess among unlabelled rows. A required row with no row to choose stays, and
|
|
505
|
+
* `renderAction` leaves the button off; an optional one is not asked.
|
|
395
506
|
*/
|
|
396
|
-
function actionInput(path, name, parameters, given, env) {
|
|
507
|
+
function actionInput(path, name, parameters, given, env, args = {}) {
|
|
397
508
|
const declared = parameters?.properties ?? {};
|
|
398
509
|
const properties = {};
|
|
399
510
|
const prefill = {};
|
|
@@ -401,29 +512,55 @@ function actionInput(path, name, parameters, given, env) {
|
|
|
401
512
|
if (!(key in declared))
|
|
402
513
|
throw new PageDocumentError(`${path}: ${name} prefills ${key}, not one of its parameters`);
|
|
403
514
|
}
|
|
515
|
+
// How to fill each one on this page is the page's (31 §7): the AI fills only what is not prefilled.
|
|
516
|
+
for (const [key, text] of Object.entries(args)) {
|
|
517
|
+
if (!(key in declared))
|
|
518
|
+
throw new PageDocumentError(`${path}: ${name} has no parameter ${key} to describe`);
|
|
519
|
+
if (key in given && !isChoose(given[key]))
|
|
520
|
+
throw new PageDocumentError(`${path}: ${name}'s ${key} is prefilled: the AI does not fill it, so it needs no description`);
|
|
521
|
+
if (typeof text !== 'string')
|
|
522
|
+
throw new PageDocumentError(`${path}: the description of ${key} must be text`);
|
|
523
|
+
}
|
|
524
|
+
const required = parameters?.required ?? [];
|
|
404
525
|
for (const [key, param] of Object.entries(declared)) {
|
|
405
526
|
const value = given[key];
|
|
406
527
|
if (isChoose(value)) {
|
|
407
|
-
|
|
528
|
+
const chosen = chooseParam(`${path}: ${name} options.${key}`, param, value, env);
|
|
529
|
+
if (!emptyOptional(chosen, key, required))
|
|
530
|
+
properties[key] = chosen;
|
|
408
531
|
continue;
|
|
409
532
|
}
|
|
410
533
|
if (key in given) {
|
|
534
|
+
// Prefilled, nobody fills it: a required one must have a value (30 §7.2).
|
|
535
|
+
if ((value === null || value === undefined) && (parameters?.required ?? []).includes(key)) {
|
|
536
|
+
throw new PageDocumentError(`${path}: ${name} prefills its required ${key} with ${value === null ? 'null' : 'nothing'}: nobody fills it after the page`);
|
|
537
|
+
}
|
|
538
|
+
// An optional one fixed as none: not asked, and not passed (its parameter is `T | undefined`).
|
|
539
|
+
if (value === null || value === undefined)
|
|
540
|
+
continue;
|
|
411
541
|
checkPrefill(`${path}: ${name}({ ${key} })`, param, value, env);
|
|
412
542
|
prefill[key] = value;
|
|
413
543
|
continue;
|
|
414
544
|
}
|
|
545
|
+
if (isRefParam(param) || (isSchemaFieldParam(param) && rowTableOf(`${path}.${key}`, param, env.schema))) {
|
|
546
|
+
throw new PageDocumentError(`${path}: ${name}'s ${key} is a row: give the rows the AI chooses among, options: { ${key}: choose(rows, label) }, or prefill it`);
|
|
547
|
+
}
|
|
415
548
|
properties[key] = expandParam(`${path}.${key}`, param, env);
|
|
416
549
|
}
|
|
550
|
+
for (const [key, text] of Object.entries(args)) {
|
|
551
|
+
if (properties[key] && text.trim())
|
|
552
|
+
properties[key] = { ...properties[key], description: text };
|
|
553
|
+
}
|
|
417
554
|
const input = Object.keys(properties).length
|
|
418
|
-
? { type: 'object', properties, required:
|
|
555
|
+
? { type: 'object', properties, required: required.filter((r) => r in properties) }
|
|
419
556
|
: null;
|
|
420
557
|
return { input, prefill };
|
|
421
558
|
}
|
|
422
559
|
/** A row parameter's candidates, as `choose(...)` gave them: rows this run read, of the parameter's table. */
|
|
423
560
|
function chooseParam(where, param, chosen, env) {
|
|
424
|
-
const table = isSchemaFieldParam(param) ? rowTableOf(where, param, env.schema) : null;
|
|
561
|
+
const table = isRefParam(param) ? param.table : isSchemaFieldParam(param) ? rowTableOf(where, param, env.schema) : null;
|
|
425
562
|
if (!table)
|
|
426
|
-
throw new PageDocumentError(`${where}: choose(...) is for a row parameter (
|
|
563
|
+
throw new PageDocumentError(`${where}: choose(...) is for a row parameter (Id<'table'>)`);
|
|
427
564
|
if (chosen.table !== null && chosen.table !== table)
|
|
428
565
|
throw new PageDocumentError(`${where}: choose(...) gives ${chosen.table} rows, the parameter takes ${table}`);
|
|
429
566
|
const names = [];
|
|
@@ -445,12 +582,12 @@ function schemaFieldOf(where, param, schema) {
|
|
|
445
582
|
const [table, field, ...rest] = param.$schemaField.split('.');
|
|
446
583
|
const def = table ? getCompiledTable(schema, table) : undefined;
|
|
447
584
|
if (!table || !field || rest.length || !def)
|
|
448
|
-
throw new PageDocumentError(`${where}:
|
|
585
|
+
throw new PageDocumentError(`${where}: '${param.$schemaField}' is not a table.field of this sequence`);
|
|
449
586
|
if (field === 'id')
|
|
450
587
|
return { table, field, def: null };
|
|
451
588
|
const fieldDef = getTableField(def, field);
|
|
452
589
|
if (!fieldDef)
|
|
453
|
-
throw new PageDocumentError(`${where}:
|
|
590
|
+
throw new PageDocumentError(`${where}: '${param.$schemaField}' is not a field of this sequence`);
|
|
454
591
|
return { table, field, def: fieldDef };
|
|
455
592
|
}
|
|
456
593
|
/** The table a parameter chooses a row of (`table.id`, a lookup), or null. */
|
|
@@ -462,17 +599,17 @@ function rowTableOf(where, param, schema) {
|
|
|
462
599
|
}
|
|
463
600
|
/** A parameter as JSON Schema for the AI: schema fields filled in from the schema, row choices as enums. */
|
|
464
601
|
function expandParam(where, param, env) {
|
|
602
|
+
if (isRefParam(param))
|
|
603
|
+
throw new PageDocumentError(`${where}: a row inside a list or an object cannot be chosen: make it a parameter of its own, chosen with options`);
|
|
465
604
|
if (isSchemaFieldParam(param)) {
|
|
466
605
|
const rowTable = rowTableOf(where, param, env.schema);
|
|
467
606
|
const { table, def } = schemaFieldOf(where, param, env.schema);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return { type: 'ref', table: rowTable, enum: names, ...(help ? { description: help } : {}) };
|
|
472
|
-
}
|
|
607
|
+
// A row inside a list or an object: `options` cannot reach it (35 §2).
|
|
608
|
+
if (rowTable)
|
|
609
|
+
throw new PageDocumentError(`${where}: a row inside a list or an object cannot be chosen: make it a parameter of its own, chosen with options`);
|
|
473
610
|
// The page's extraction replaces the schema's; type and allowed values stay the schema's.
|
|
474
611
|
const fieldDef = param.extraction !== undefined ? { ...def, extraction: param.extraction } : def;
|
|
475
|
-
return fieldDef.dataType === 'contact_schedule' ? scheduleProperty(fieldDef) : scalarProperty(fieldDef);
|
|
612
|
+
return fieldDef.dataType === 'contact_schedule' ? scheduleProperty(fieldDef.extraction || undefined) : scalarProperty(fieldDef);
|
|
476
613
|
}
|
|
477
614
|
if (param.type === 'array') {
|
|
478
615
|
const out = { type: 'array', items: expandParam(`${where}[]`, param.items, env) };
|
|
@@ -491,6 +628,8 @@ function expandParam(where, param, env) {
|
|
|
491
628
|
out.description = param.description;
|
|
492
629
|
return out;
|
|
493
630
|
}
|
|
631
|
+
if (param.type === 'contact_schedule')
|
|
632
|
+
return scheduleProperty(param.description);
|
|
494
633
|
return { ...param };
|
|
495
634
|
}
|
|
496
635
|
/**
|
|
@@ -500,6 +639,13 @@ function expandParam(where, param, env) {
|
|
|
500
639
|
function checkPrefill(where, param, value, env) {
|
|
501
640
|
if (value === null || value === undefined)
|
|
502
641
|
return;
|
|
642
|
+
if (isRefParam(param)) {
|
|
643
|
+
const read = typeof value === 'string' ? env.reads.row(value) : undefined;
|
|
644
|
+
if (!read || read.table !== param.table)
|
|
645
|
+
throw new PageDocumentError(`${where} is not a ${param.table} row this run read`);
|
|
646
|
+
env.rowName(read.table, value);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
503
649
|
if (isSchemaFieldParam(param)) {
|
|
504
650
|
const rowTable = rowTableOf(where, param, env.schema);
|
|
505
651
|
if (!rowTable)
|
|
@@ -519,6 +665,10 @@ function checkPrefill(where, param, value, env) {
|
|
|
519
665
|
if (param.type === 'object') {
|
|
520
666
|
if (!isPlainObject(value))
|
|
521
667
|
throw new PageDocumentError(`${where} must be an object`);
|
|
668
|
+
// A prefilled object is the page's whole: the AI cannot fill a part of it (30 §7.2).
|
|
669
|
+
const missing = (param.required ?? []).filter((key) => value[key] === undefined || value[key] === null);
|
|
670
|
+
if (missing.length)
|
|
671
|
+
throw new PageDocumentError(`${where} is prefilled without ${missing.join(', ')}: a prefilled object needs every required part, nobody fills the rest`);
|
|
522
672
|
for (const [key, child] of Object.entries(param.properties))
|
|
523
673
|
checkPrefill(`${where}.${key}`, child, value[key], env);
|
|
524
674
|
}
|
|
@@ -532,8 +682,8 @@ function scalarProperty(def) {
|
|
|
532
682
|
return { type: 'select', enum: def.options ?? [], ...help };
|
|
533
683
|
return { type: def.dataType, ...help };
|
|
534
684
|
}
|
|
535
|
-
/** A `contact_schedule` field: its own structure
|
|
536
|
-
function scheduleProperty(
|
|
685
|
+
/** A contact's hours (a `contact_schedule` field, a `ContactSchedule` argument): its own structure, not text. */
|
|
686
|
+
function scheduleProperty(help) {
|
|
537
687
|
const window = {
|
|
538
688
|
type: 'object',
|
|
539
689
|
properties: {
|
|
@@ -547,7 +697,7 @@ function scheduleProperty(def) {
|
|
|
547
697
|
return {
|
|
548
698
|
type: 'object',
|
|
549
699
|
'x-type': 'contact_schedule',
|
|
550
|
-
...
|
|
700
|
+
...(help ? { description: help } : {}),
|
|
551
701
|
properties: {
|
|
552
702
|
weekly: {
|
|
553
703
|
type: 'object',
|
|
@@ -570,7 +720,7 @@ function scheduleProperty(def) {
|
|
|
570
720
|
};
|
|
571
721
|
}
|
|
572
722
|
/**
|
|
573
|
-
* An argument's help (26 §16.2 #5a): how to fill it, the page's (`
|
|
723
|
+
* An argument's help (26 §16.2 #5a): how to fill it, the page's (`page({ extraction })`) or else
|
|
574
724
|
* the field's; none when neither says. The label is not repeated in it; how its type is written is the
|
|
575
725
|
* system message's (26 §15.8).
|
|
576
726
|
*/
|
|
@@ -578,6 +728,94 @@ function describeField(def) {
|
|
|
578
728
|
return def.extraction ? { description: def.extraction } : {};
|
|
579
729
|
}
|
|
580
730
|
// ---------------------------------------------------------------------------------------------
|
|
731
|
+
/**
|
|
732
|
+
* The buttons' ids (46 §2.1), in page order: what each does (its run key's words: a function's name,
|
|
733
|
+
* `add:injury`, `remove`, `transfer`, …); where several on the page do the same, the rows that tell them
|
|
734
|
+
* apart, by page name (`voidInjury@injury:3f9a2c1d7e04`); where that still leaves two alike, `#2`, `#3`
|
|
735
|
+
* after the first. Unique on the page, and the same from one render to the next while the button does
|
|
736
|
+
* the same thing to the same rows.
|
|
737
|
+
*/
|
|
738
|
+
export function buttonIds(buttons, rows) {
|
|
739
|
+
const nameOf = new Map(Object.entries(rows).map(([name, row]) => [row.id, name]));
|
|
740
|
+
const bases = buttons.map(buttonBase);
|
|
741
|
+
const rowsOf = buttons.map((button) => boundRows(button, nameOf));
|
|
742
|
+
const ids = buttons.map((_, i) => {
|
|
743
|
+
const alike = bases.flatMap((base, j) => (base === bases[i] ? [j] : []));
|
|
744
|
+
if (alike.length === 1)
|
|
745
|
+
return bases[i];
|
|
746
|
+
// Only the rows that differ among them: the case or contact they all share says nothing.
|
|
747
|
+
const shared = rowsOf[alike[0]].filter((name) => alike.every((j) => rowsOf[j].includes(name)));
|
|
748
|
+
const own = rowsOf[i].filter((name) => !shared.includes(name));
|
|
749
|
+
return own.length ? `${bases[i]}@${own.join(',')}` : bases[i];
|
|
750
|
+
});
|
|
751
|
+
const seen = new Map();
|
|
752
|
+
return ids.map((id) => {
|
|
753
|
+
const n = (seen.get(id) ?? 0) + 1;
|
|
754
|
+
seen.set(id, n);
|
|
755
|
+
return n === 1 ? id : `${id}#${n}`;
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* The button the AI means by `id` on this page (46 §2.1), or null. Its own id first; else an id it read
|
|
760
|
+
* on an earlier page, where alike buttons needed their rows (`bookSlot@slot_option:d12f648904ec`) and now
|
|
761
|
+
* one is left (`bookSlot`): the one button doing that to those rows. Never a guess among several.
|
|
762
|
+
*/
|
|
763
|
+
export function pressedButton(page, id) {
|
|
764
|
+
if (id in page.actions)
|
|
765
|
+
return id;
|
|
766
|
+
const plain = id.replace(/#\d+$/, '');
|
|
767
|
+
const at = plain.lastIndexOf('@');
|
|
768
|
+
if (at < 0)
|
|
769
|
+
return null;
|
|
770
|
+
const base = plain.slice(0, at);
|
|
771
|
+
const rows = plain.slice(at + 1).split(',').filter(Boolean);
|
|
772
|
+
const nameOf = new Map(Object.entries(page.rows).map(([name, row]) => [row.id, name]));
|
|
773
|
+
const found = Object.values(page.actions).filter((button) => {
|
|
774
|
+
if (buttonBase(button) !== base)
|
|
775
|
+
return false;
|
|
776
|
+
const bound = boundRows(button, nameOf);
|
|
777
|
+
return rows.every((row) => bound.includes(row));
|
|
778
|
+
});
|
|
779
|
+
return found.length === 1 ? found[0].id : null;
|
|
780
|
+
}
|
|
781
|
+
/** What a button does, as its id says it (46 §2.1): its run key, in words the AI reads. */
|
|
782
|
+
function buttonBase(button) {
|
|
783
|
+
const server = button.server;
|
|
784
|
+
if (server.kind === 'createRow')
|
|
785
|
+
return server.key ?? `add:${server.table}`;
|
|
786
|
+
if (server.kind === 'deleteRow')
|
|
787
|
+
return server.key ?? 'remove';
|
|
788
|
+
if (server.kind === 'channel' && server.spec.channel === 'keypad')
|
|
789
|
+
return `keypad:${server.spec.key}`;
|
|
790
|
+
return runKeyOf(button);
|
|
791
|
+
}
|
|
792
|
+
/** The page names of the rows a button works on: the row it removes, where it adds, the rows it is prefilled with. */
|
|
793
|
+
function boundRows(button, nameOf) {
|
|
794
|
+
const server = button.server;
|
|
795
|
+
const values = [];
|
|
796
|
+
if (server.kind === 'deleteRow')
|
|
797
|
+
values.push(server.rowId);
|
|
798
|
+
if (server.kind === 'createRow')
|
|
799
|
+
values.push(server.parentId, ...Object.values(server.set));
|
|
800
|
+
if (server.kind === 'custom' || server.kind === 'function')
|
|
801
|
+
values.push(...Object.values(server.prefill ?? {}));
|
|
802
|
+
if (server.kind === 'followUp')
|
|
803
|
+
values.push(server.prefill.contactId);
|
|
804
|
+
const names = [];
|
|
805
|
+
const visit = (value) => {
|
|
806
|
+
if (typeof value === 'string') {
|
|
807
|
+
const name = nameOf.get(value);
|
|
808
|
+
if (name && !names.includes(name))
|
|
809
|
+
names.push(name);
|
|
810
|
+
}
|
|
811
|
+
else if (Array.isArray(value))
|
|
812
|
+
value.forEach(visit);
|
|
813
|
+
else if (isPlainObject(value))
|
|
814
|
+
Object.values(value).forEach(visit);
|
|
815
|
+
};
|
|
816
|
+
values.forEach(visit);
|
|
817
|
+
return names;
|
|
818
|
+
}
|
|
581
819
|
function join(path, key) {
|
|
582
820
|
return path ? `${path}.${key}` : key;
|
|
583
821
|
}
|
|
@@ -596,24 +834,6 @@ function isFieldCell(value) {
|
|
|
596
834
|
function isRefFieldCell(value) {
|
|
597
835
|
return isPlainObject(value) && value.$refField === true && Array.isArray(value.options);
|
|
598
836
|
}
|
|
599
|
-
function isAction(value) {
|
|
600
|
-
return (isPlainObject(value) &&
|
|
601
|
-
(value.$action === 'createRow' ||
|
|
602
|
-
value.$action === 'deleteRow' ||
|
|
603
|
-
value.$action === 'custom' ||
|
|
604
|
-
(value.$action === 'function' && typeof value.name === 'string') ||
|
|
605
|
-
(value.$action === 'message' && typeof value.key === 'string') ||
|
|
606
|
-
(value.$action === 'followUp' && Array.isArray(value.channels)) ||
|
|
607
|
-
(value.$action === 'channel' && typeof value.channel === 'string')));
|
|
608
|
-
}
|
|
609
|
-
/** The page function returned an action to run now (`perform(…)`, or the action itself). */
|
|
610
|
-
function performOf(document) {
|
|
611
|
-
if (isPlainObject(document) && document.$perform === true && isAction(document.action))
|
|
612
|
-
return document;
|
|
613
|
-
if (isAction(document))
|
|
614
|
-
return { $perform: true, action: document };
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
837
|
/**
|
|
618
838
|
* The run key of a button on the page (17b §7): the action's own key, whoever runs it — the model
|
|
619
839
|
* pressing it, or the page function returning it — not its place on the page, which moves with rows.
|
|
@@ -621,90 +841,62 @@ function performOf(document) {
|
|
|
621
841
|
export function runKeyOf(action) {
|
|
622
842
|
const server = action.server;
|
|
623
843
|
if (server.kind === 'custom')
|
|
624
|
-
return server.name;
|
|
844
|
+
return server.key ?? server.name;
|
|
625
845
|
if (server.kind === 'createRow')
|
|
626
|
-
return `createRow:${server.table}`;
|
|
846
|
+
return server.key ?? `createRow:${server.table}`;
|
|
627
847
|
if (server.kind === 'deleteRow')
|
|
628
|
-
return `deleteRow:${server.rowId}`;
|
|
848
|
+
return server.key ?? `deleteRow:${server.rowId}`;
|
|
629
849
|
if (server.kind === 'function')
|
|
630
|
-
return server.name;
|
|
631
|
-
if (server.kind === '
|
|
850
|
+
return server.key ?? server.name;
|
|
851
|
+
if (server.kind === 'followUp' || server.kind === 'cancelFollowUp')
|
|
632
852
|
return server.key;
|
|
853
|
+
// A tool's run key is its name, fixed (76 §2.18): a page has one toolbox, each tool once.
|
|
854
|
+
if (server.kind === 'tool')
|
|
855
|
+
return server.tool;
|
|
633
856
|
return actionKey(server.spec);
|
|
634
857
|
}
|
|
635
858
|
/** The run key of an action the page function returned, when it names none. */
|
|
636
859
|
export function actionKey(action) {
|
|
637
860
|
if (action.$action === 'custom')
|
|
638
|
-
return action.name;
|
|
861
|
+
return action.key ?? action.name;
|
|
639
862
|
if (action.$action === 'function')
|
|
640
|
-
return action.name;
|
|
641
|
-
if (action.$action === '
|
|
863
|
+
return action.key ?? action.name;
|
|
864
|
+
if (action.$action === 'followUp' || action.$action === 'cancelFollowUp')
|
|
642
865
|
return action.key;
|
|
643
866
|
if (action.$action === 'createRow')
|
|
644
|
-
return `createRow:${action.table}`;
|
|
867
|
+
return action.key ?? `createRow:${action.table}`;
|
|
645
868
|
if (action.$action === 'deleteRow')
|
|
646
|
-
return `deleteRow:${action.rowId}`;
|
|
869
|
+
return action.key ?? `deleteRow:${action.rowId}`;
|
|
870
|
+
if (action.$action === 'tool')
|
|
871
|
+
return action.tool;
|
|
647
872
|
if (action.channel === 'keypad' || action.channel === 'announce')
|
|
648
873
|
return action.key;
|
|
649
|
-
return action.channel;
|
|
650
|
-
}
|
|
651
|
-
/** Marks removed with the todos (24 §2.3): a page still writing one is told what replaced it. */
|
|
652
|
-
/** The marks a page object may carry: `$title`, `$description` on any block; `$guide`, `$extraction` on the root. */
|
|
653
|
-
const KNOWN_MARKS = new Set(['$title', '$description', '$guide', '$extraction']);
|
|
654
|
-
const REMOVED_MARKS = {
|
|
655
|
-
$say: 'write $guide on the page\'s root',
|
|
656
|
-
$done: 'show what is done as a result, or stop guiding to it',
|
|
657
|
-
$locked: 'leave out what the AI must not touch; say why in plain text',
|
|
658
|
-
$summary: 'nothing is folded any more',
|
|
659
|
-
$recommend: 'write $guide on the page\'s root',
|
|
660
|
-
};
|
|
874
|
+
return action.key ?? action.channel;
|
|
875
|
+
}
|
|
661
876
|
/**
|
|
662
877
|
* The page's `$extraction` (26 §15.3): how to fill a field on this page, by `table.field`, replacing the
|
|
663
878
|
* schema's for every place it is on the page. One per field for the whole page, so a field means the
|
|
664
879
|
* same everywhere on it. A `table.field` the schema does not have is warned and left out.
|
|
665
880
|
*/
|
|
666
|
-
|
|
881
|
+
/** `page({ extraction })` (26 §15.3): how to fill a field on this page, by `table.field`. */
|
|
882
|
+
function pageExtractions(value, schema, warnings) {
|
|
667
883
|
const out = new Map();
|
|
668
|
-
const value = isPlainObject(document) ? document.$extraction : undefined;
|
|
669
884
|
if (value === undefined || value === null)
|
|
670
885
|
return out;
|
|
671
886
|
if (!isPlainObject(value))
|
|
672
|
-
throw new PageDocumentError('page
|
|
887
|
+
throw new PageDocumentError('page({ extraction }): an object of table.field → text');
|
|
673
888
|
for (const [key, text] of Object.entries(value)) {
|
|
674
889
|
if (typeof text !== 'string')
|
|
675
|
-
throw new PageDocumentError(`page:
|
|
890
|
+
throw new PageDocumentError(`page({ extraction }): '${key}' must be text`);
|
|
676
891
|
const [table, field, ...rest] = key.split('.');
|
|
677
892
|
if (!table || !field || rest.length || !getCompiledTable(schema, table)?.fields[field]) {
|
|
678
|
-
warnings.push(`page:
|
|
893
|
+
warnings.push(`page({ extraction }): '${key}' is not a table.field of the schema; left out`);
|
|
679
894
|
continue;
|
|
680
895
|
}
|
|
681
896
|
out.set(key, text);
|
|
682
897
|
}
|
|
683
898
|
return out;
|
|
684
899
|
}
|
|
685
|
-
/** `$guide` and `$extraction` on the root (24 §2.1, 26 §15.3); on any other block they are refused, as the removed marks are. */
|
|
686
|
-
function pageMarks(block, value, path, isRoot, warnings) {
|
|
687
|
-
const where = path || 'page';
|
|
688
|
-
for (const [mark, instead] of Object.entries(REMOVED_MARKS)) {
|
|
689
|
-
if (value[mark] !== undefined)
|
|
690
|
-
throw new PageDocumentError(`${where}: ${mark} was removed (24 §2.3): ${instead}`);
|
|
691
|
-
}
|
|
692
|
-
// A misspelt mark (`$titel`) would otherwise do nothing, silently (26 §16.2 #4).
|
|
693
|
-
for (const mark of Object.keys(value)) {
|
|
694
|
-
if (mark.startsWith('$') && !KNOWN_MARKS.has(mark))
|
|
695
|
-
warnings.push(`${where}: ${mark} is not a page mark (${[...KNOWN_MARKS].join(', ')}); ignored`);
|
|
696
|
-
}
|
|
697
|
-
if (!isRoot && value.$extraction !== undefined)
|
|
698
|
-
throw new PageDocumentError(`${where}: $extraction is the page's: write it on the page's root, one per page (26 §15.3)`);
|
|
699
|
-
if (value.$guide === undefined || value.$guide === null)
|
|
700
|
-
return;
|
|
701
|
-
if (typeof value.$guide !== 'string')
|
|
702
|
-
throw new PageDocumentError(`${where}: $guide must be text`);
|
|
703
|
-
if (!isRoot)
|
|
704
|
-
throw new PageDocumentError(`${where}: $guide is the page's: write it on the page's root, one per page (24 §2.1)`);
|
|
705
|
-
if (value.$guide.trim())
|
|
706
|
-
block.guide = value.$guide;
|
|
707
|
-
}
|
|
708
900
|
const CHANNEL_NAMES = {
|
|
709
901
|
transfer: ['Transfer to a person', 'Hand the call to a person. Press it only when the customer wants a person or the page says so.'],
|
|
710
902
|
keypad: ['Collect a keypress', 'The customer answers by pressing a key on their phone.'],
|
|
@@ -716,6 +908,32 @@ const BACKGROUND_TRANSFER = [
|
|
|
716
908
|
'Call a person in the background',
|
|
717
909
|
'Calls a person while you keep helping the customer; the customer is put through once the person takes the call. Press it only when the customer wants a person or the page says so.',
|
|
718
910
|
];
|
|
911
|
+
/** What the AI sees of each tool (76 §2.18). */
|
|
912
|
+
const TOOL_BUTTONS = {
|
|
913
|
+
pressKeypad: {
|
|
914
|
+
name: 'Press keys on their phone menu',
|
|
915
|
+
description: "Press keys on the other side's automated phone menu, e.g. 'Press 1 for prescriptions'. Give the keys as one string: digits 0–9, * and #; w waits half a second (\"1w2\"). Only for an automated menu, never to talk to a person.",
|
|
916
|
+
input: { type: 'object', properties: { keys: { type: 'string', description: 'The keys to press, in order: 0–9, *, #, w (wait half a second)' } }, required: ['keys'] },
|
|
917
|
+
},
|
|
918
|
+
convertTime: {
|
|
919
|
+
name: 'Convert a time',
|
|
920
|
+
description: 'Convert between a local time in a time zone and an instant in UTC. Give local and time_zone to get the instant in UTC; give at and time_zone to get the weekday, date and time there.',
|
|
921
|
+
input: {
|
|
922
|
+
type: 'object',
|
|
923
|
+
properties: {
|
|
924
|
+
time_zone: { type: 'string', description: 'IANA time zone, e.g. America/Chicago' },
|
|
925
|
+
local: { type: 'string', description: 'A local date and time there, YYYY-MM-DDTHH:mm' },
|
|
926
|
+
at: { type: 'datetime', description: 'An instant, UTC (…Z)' },
|
|
927
|
+
},
|
|
928
|
+
required: ['time_zone'],
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
/** A platform tool as a button (76 §2.18): the platform runs it with what the AI gave. */
|
|
933
|
+
function toolButton(path, tool) {
|
|
934
|
+
const { name, description, input } = TOOL_BUTTONS[tool];
|
|
935
|
+
return { id: path, kind: 'tool', name, description, input: structuredClone(input), server: { kind: 'tool', tool } };
|
|
936
|
+
}
|
|
719
937
|
/** A channel action as a button: nothing for the AI to fill; what it does stays on the server. */
|
|
720
938
|
function channelButton(path, action) {
|
|
721
939
|
const [name, description] = action.channel === 'transfer' && action.mode === 'background' ? BACKGROUND_TRANSFER : CHANNEL_NAMES[action.channel];
|
|
@@ -725,79 +943,81 @@ function channelButton(path, action) {
|
|
|
725
943
|
id: path,
|
|
726
944
|
kind: 'channel',
|
|
727
945
|
name: label ?? name,
|
|
728
|
-
description: text ?? (action.channel === 'keypad' ? `${description} It says: "${action.say}"` : description),
|
|
946
|
+
description: text ?? (action.channel === 'keypad' ? `${description} It says: "${fallbackSpoken(action.say).text}"` : description),
|
|
729
947
|
input: null,
|
|
730
948
|
server: { kind: 'channel', spec: action },
|
|
731
949
|
};
|
|
732
950
|
}
|
|
951
|
+
const CHANNEL_WORDS = { call: 'a call', sms: 'a text', email: 'an email' };
|
|
733
952
|
/**
|
|
734
|
-
*
|
|
735
|
-
*
|
|
953
|
+
* The next-step button (40 §2.2): the AI gives when, the channel, why, and the customer's own words when
|
|
954
|
+
* they asked for it (an agreed follow-up, 09). Whom: the conversation's contact, unless the page fixed one
|
|
955
|
+
* (`prefill.contactId`) or gave the contacts the AI chooses among (`choose(...)`). Independent
|
|
956
|
+
* appointments coexist; an optional explicit target edits one (51).
|
|
736
957
|
*/
|
|
737
|
-
function
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
return null;
|
|
743
|
-
}
|
|
744
|
-
env.rowName('contact', action.to);
|
|
745
|
-
}
|
|
746
|
-
const what = action.channel === 'sms' ? 'text' : 'email';
|
|
958
|
+
function followUpButton(path, action, env) {
|
|
959
|
+
const given = action.prefill ?? {};
|
|
960
|
+
// One channel offered, or the page fixed it: nothing for the AI to choose.
|
|
961
|
+
const channel = given.channel ?? (action.channels.length === 1 ? action.channels[0] : undefined);
|
|
962
|
+
const ways = (channel ? [channel] : action.channels).map((c) => CHANNEL_WORDS[c]).join(' or ');
|
|
747
963
|
const properties = {};
|
|
748
|
-
if (
|
|
749
|
-
properties.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
964
|
+
if (!channel)
|
|
965
|
+
properties.channel = { type: 'select', enum: [...action.channels], description: 'How to reach them' };
|
|
966
|
+
properties.scheduled_action = { type: 'string', description: 'To edit one existing appointment, its step-… ID shown below; leave out to ADD an independent appointment' };
|
|
967
|
+
properties.name = { type: 'string', description: 'Optional business label, only when the page gives a naming convention; do not invent one for deduplication' };
|
|
968
|
+
// Every time the AI writes is UTC (76 §2.19): it asks the customer's time and time zone and converts.
|
|
969
|
+
properties.at = {
|
|
970
|
+
type: 'datetime',
|
|
971
|
+
description: 'When, in UTC (ending in Z): ask them the time and their time zone (unless the page says it), then convert; for a time the customer said vaguely ("tomorrow afternoon") a time inside it',
|
|
972
|
+
};
|
|
973
|
+
properties.reason = { type: 'string', description: 'Why, in a few words: what the contact is for' };
|
|
974
|
+
properties.customer_quote = { type: 'string', description: 'When the customer asked for it or agreed to it: their own words, as said; leave out otherwise' };
|
|
975
|
+
const prefill = channel ? { channel } : {};
|
|
976
|
+
const contact = given.contactId;
|
|
977
|
+
if (typeof contact === 'string') {
|
|
978
|
+
checkPrefill(`${path}: prefill.contactId`, { type: 'ref', table: CONTACT_TABLE }, contact, env);
|
|
979
|
+
prefill.contactId = contact;
|
|
980
|
+
}
|
|
981
|
+
else if (contact) {
|
|
982
|
+
const offered = chooseParam(`${path}: options.contactId`, { type: 'ref', table: CONTACT_TABLE }, contact, env);
|
|
983
|
+
// The choice adds people besides the conversation's contact (always the default): none to add, and
|
|
984
|
+
// the button books for that contact, as when the AI leaves the choice out.
|
|
985
|
+
if (offered.type === 'ref' && offered.enum.length)
|
|
986
|
+
properties.contact = { ...offered, description: 'Whom to contact; leave out for the person in this conversation' };
|
|
987
|
+
}
|
|
760
988
|
return {
|
|
761
989
|
id: path,
|
|
762
|
-
kind: '
|
|
763
|
-
name: action.label ??
|
|
990
|
+
kind: 'followUp',
|
|
991
|
+
name: action.label ?? 'Book the next step',
|
|
764
992
|
description: action.description ??
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
993
|
+
[
|
|
994
|
+
`Books the case's next step: ${ways} at a set time, and why.`,
|
|
995
|
+
"Press it when the customer asks to be contacted at a time or agrees to one you offered (give their words), or when the case needs a contact at a set time; tell them it is booked only once it is.",
|
|
996
|
+
"Independent appointments can coexist, even for the same person and channel. Adding does not replace any appointment.",
|
|
997
|
+
"To change one appointment, give its scheduled_action ID from the list below. Only that appointment changes; leave contact out to keep its contact.",
|
|
998
|
+
].join(' '),
|
|
999
|
+
input: { type: 'object', properties, required: [...(channel ? [] : ['channel']), 'at', 'reason'] },
|
|
1000
|
+
server: { kind: 'followUp', key: action.key, channels: [...action.channels], prefill },
|
|
770
1001
|
};
|
|
771
1002
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
function followUpButton(path, action) {
|
|
775
|
-
const ways = action.channels.map((c) => CHANNEL_WORDS[c]).join(' or ');
|
|
1003
|
+
/** The cancel-next-step button (40 §2.2): the AI says why. */
|
|
1004
|
+
function cancelFollowUpButton(path, action) {
|
|
776
1005
|
return {
|
|
777
1006
|
id: path,
|
|
778
|
-
kind: '
|
|
779
|
-
name: action.label ?? '
|
|
1007
|
+
kind: 'cancelFollowUp',
|
|
1008
|
+
name: action.label ?? 'Cancel the next step',
|
|
780
1009
|
description: action.description ??
|
|
781
|
-
|
|
782
|
-
input: {
|
|
783
|
-
|
|
784
|
-
properties: {
|
|
785
|
-
channel: { type: 'select', enum: [...action.channels], description: 'How they want to be reached' },
|
|
786
|
-
at: { type: 'datetime', description: 'When, as they said it, with the offset of their time zone; for a vague time ("tomorrow afternoon") a time inside it' },
|
|
787
|
-
customer_quote: { type: 'string', description: 'Their own words asking for it or agreeing to it, as said' },
|
|
788
|
-
note: { type: 'string', description: 'What it is for, in a few words' },
|
|
789
|
-
},
|
|
790
|
-
required: ['channel', 'at', 'customer_quote'],
|
|
791
|
-
},
|
|
792
|
-
server: { kind: 'followUp', key: action.key, channels: [...action.channels] },
|
|
1010
|
+
"Cancels ONE appointment by its scheduled_action ID from the list below, with a reason. Other appointments stay. Talking to someone does not automatically complete their appointments; cancel only what is no longer needed.",
|
|
1011
|
+
input: { type: 'object', properties: { scheduled_action: { type: 'string', description: 'The step-… ID of the appointment to cancel' }, reason: { type: 'string', description: 'Why it is no longer needed' } }, required: ['scheduled_action', 'reason'] },
|
|
1012
|
+
server: { kind: 'cancelFollowUp', key: action.key },
|
|
793
1013
|
};
|
|
794
1014
|
}
|
|
1015
|
+
function isRefParam(param) {
|
|
1016
|
+
return !isSchemaFieldParam(param) && param.type === 'ref';
|
|
1017
|
+
}
|
|
795
1018
|
function isSchemaFieldParam(value) {
|
|
796
1019
|
return isPlainObject(value) && typeof value.$schemaField === 'string';
|
|
797
1020
|
}
|
|
798
|
-
function isRowLike(value) {
|
|
799
|
-
return typeof value.id === 'string' && Object.values(value).some(isSlotLike);
|
|
800
|
-
}
|
|
801
1021
|
function displayable(value) {
|
|
802
1022
|
if (value === null || value === undefined)
|
|
803
1023
|
return null;
|