@superpanel-sdk/runtime 0.2.3 → 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 +30 -10
- package/dist/builder/codegen/emit.d.ts +25 -12
- package/dist/builder/codegen/emit.js +339 -78
- package/dist/builder/codegen/generate.d.ts +21 -2
- package/dist/builder/codegen/generate.js +147 -34
- package/dist/builder/company-import-graph.d.ts +2 -0
- package/dist/builder/company-import-graph.js +40 -8
- package/dist/builder/company-imports.d.ts +29 -0
- package/dist/builder/company-imports.js +122 -0
- package/dist/builder/compile/compile-sequence.d.ts +61 -8
- package/dist/builder/compile/compile-sequence.js +334 -80
- package/dist/builder/compile/function-code.d.ts +19 -3
- package/dist/builder/compile/function-code.js +123 -45
- package/dist/builder/compile/registry.d.ts +7 -3
- package/dist/builder/compile/registry.js +30 -27
- 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 +70 -10
- package/dist/builder/load.js +258 -67
- package/dist/builder/paths.d.ts +9 -1
- package/dist/builder/paths.js +32 -2
- package/dist/builder/schedules.d.ts +25 -0
- package/dist/builder/schedules.js +101 -0
- package/dist/builder/slot-schema-validation.d.ts +4 -0
- package/dist/builder/slot-schema-validation.js +10 -0
- package/dist/builder/typecheck.d.ts +8 -0
- package/dist/builder/typecheck.js +102 -0
- package/dist/builder/types.d.ts +127 -19
- package/dist/builder/types.js +21 -1
- package/dist/cli/auth/auth.d.ts +37 -0
- package/dist/cli/auth/auth.js +211 -0
- package/dist/cli/auth/backend-url.d.ts +5 -0
- package/dist/cli/auth/backend-url.js +25 -0
- package/dist/cli/auth/clipboard.d.ts +9 -0
- package/dist/cli/auth/clipboard.js +38 -0
- package/dist/cli/auth/http.d.ts +20 -0
- package/dist/cli/auth/http.js +27 -0
- package/dist/cli/auth/index.d.ts +10 -0
- package/dist/cli/auth/index.js +27 -0
- package/dist/cli/auth/progress.d.ts +12 -0
- package/dist/cli/auth/progress.js +15 -0
- package/dist/cli/auth/serve-login.d.ts +8 -0
- package/dist/cli/auth/serve-login.js +24 -0
- package/dist/cli/calls/index.d.ts +177 -0
- package/dist/cli/calls/index.js +96 -0
- package/dist/cli/compile/index.js +3 -0
- package/dist/cli/config.d.ts +10 -0
- package/dist/cli/config.js +52 -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 +112 -9
- 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 +109 -3
- package/dist/cli/serve/index.js +506 -118
- 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 +9 -9
- package/dist/compiler.d.ts +7 -0
- package/dist/compiler.js +7 -0
- package/dist/compute-host/compute-host.d.ts +14 -23
- package/dist/compute-host/compute-host.js +18 -30
- package/dist/compute-host/index.d.ts +2 -4
- package/dist/compute-host/index.js +1 -2
- package/dist/compute-host/source-compute-host.d.ts +10 -10
- package/dist/compute-host/source-compute-host.js +33 -40
- package/dist/core/case-orchestrator/contact-hours.d.ts +70 -0
- package/dist/core/case-orchestrator/contact-hours.js +127 -0
- package/dist/core/case-orchestrator/index.d.ts +3 -0
- package/dist/core/case-orchestrator/index.js +3 -0
- package/dist/core/case-orchestrator/render.d.ts +75 -0
- package/dist/core/case-orchestrator/render.js +244 -0
- package/dist/core/case-orchestrator/time.d.ts +11 -0
- package/dist/core/case-orchestrator/time.js +29 -0
- package/dist/core/case-orchestrator/types.d.ts +708 -0
- package/dist/core/case-orchestrator/types.js +42 -0
- package/dist/core/context/call.d.ts +291 -0
- package/dist/core/context/call.js +94 -0
- package/dist/core/context/channels.d.ts +60 -0
- package/dist/core/context/channels.js +35 -0
- package/dist/core/context/contact.d.ts +96 -6
- package/dist/core/context/contact.js +48 -2
- package/dist/core/context/email-message.d.ts +198 -0
- package/dist/core/context/email-message.js +86 -0
- package/dist/core/context/email.d.ts +109 -0
- package/dist/core/context/email.js +90 -0
- package/dist/core/context/exchanges.d.ts +167 -0
- package/dist/core/context/exchanges.js +81 -0
- package/dist/core/context/instances.d.ts +307 -0
- package/dist/core/context/instances.js +137 -0
- package/dist/core/context/kv.d.ts +113 -0
- package/dist/core/context/kv.js +60 -0
- package/dist/core/context/platform-registry.d.ts +544 -4
- package/dist/core/context/platform-registry.js +14 -2
- package/dist/core/context/platform.d.ts +2 -1
- package/dist/core/context/sms.d.ts +177 -0
- package/dist/core/context/sms.js +77 -0
- package/dist/core/context/types.d.ts +79 -48
- package/dist/core/context/types.js +1 -7
- package/dist/core/conversation/index.d.ts +85 -0
- package/dist/core/conversation/index.js +133 -0
- package/dist/core/functions/errors.d.ts +74 -0
- package/dist/core/functions/errors.js +26 -0
- package/dist/core/functions/events.d.ts +192 -0
- package/dist/core/functions/events.js +23 -0
- package/dist/core/functions/schedules.d.ts +50 -0
- package/dist/core/functions/schedules.js +20 -0
- package/dist/core/functions/types.d.ts +29 -73
- package/dist/core/functions/types.js +1 -4
- package/dist/core/office-hours/index.d.ts +39 -0
- package/dist/core/office-hours/index.js +187 -0
- package/dist/core/page/define.d.ts +904 -0
- package/dist/core/page/define.js +518 -0
- 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/ops.d.ts +58 -0
- package/dist/core/page/ops.js +220 -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 +295 -0
- package/dist/core/page/render.js +1060 -0
- 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 +46 -0
- package/dist/core/page/time-format.js +125 -0
- package/dist/core/page/timeline.d.ts +40 -0
- package/dist/core/page/timeline.js +2 -0
- package/dist/core/page/typed.d.ts +648 -0
- package/dist/core/page/typed.js +2 -0
- package/dist/core/page/workspace.d.ts +57 -0
- package/dist/core/page/workspace.js +266 -0
- package/dist/core/party/index.d.ts +3 -0
- package/dist/core/party/index.js +3 -0
- package/dist/core/party/schema.d.ts +208 -0
- package/dist/core/party/schema.js +113 -0
- package/dist/core/party/types.d.ts +35 -0
- package/dist/core/party/types.js +15 -0
- package/dist/core/sequence/define.d.ts +56 -0
- package/dist/core/sequence/define.js +10 -0
- package/dist/core/sequence/index.d.ts +63 -0
- package/dist/core/sequence/index.js +231 -0
- package/dist/core/slot/binding.d.ts +40 -0
- package/dist/core/slot/binding.js +2 -0
- package/dist/core/slot/company-metadata.d.ts +102 -0
- package/dist/core/slot/company-metadata.js +536 -0
- package/dist/core/slot/compile-schema.d.ts +19 -13
- package/dist/core/slot/compile-schema.js +51 -248
- package/dist/core/slot/contact-timezone.d.ts +110 -0
- package/dist/core/slot/contact-timezone.js +186 -0
- package/dist/core/slot/field.d.ts +2 -25
- package/dist/core/slot/field.js +0 -71
- package/dist/core/slot/id-ref.d.ts +17 -2
- package/dist/core/slot/id-ref.js +16 -8
- package/dist/core/slot/index.d.ts +12 -13
- package/dist/core/slot/index.js +11 -12
- package/dist/core/slot/naming.d.ts +15 -0
- package/dist/core/slot/naming.js +96 -0
- package/dist/core/slot/platform-fields.d.ts +119 -0
- package/dist/core/slot/platform-fields.js +121 -0
- package/dist/core/slot/ref.d.ts +16 -0
- package/dist/core/slot/ref.js +14 -0
- package/dist/core/slot/row.d.ts +2 -9
- package/dist/core/slot/row.js +13 -23
- package/dist/core/slot/schema-compat.d.ts +17 -0
- package/dist/core/slot/schema-compat.js +103 -0
- package/dist/core/slot/schema.d.ts +353 -76
- package/dist/core/slot/schema.js +163 -10
- package/dist/core/slot/types.d.ts +30 -103
- package/dist/core/slot/value.d.ts +28 -3
- package/dist/core/slot/value.js +93 -57
- package/dist/dev-overlay/protocol.d.ts +74 -8
- package/dist/fn-context/build-context.d.ts +3 -1
- package/dist/fn-context/build-context.js +26 -10
- package/dist/fn-context/fake/empty-api.js +9 -28
- package/dist/fn-context/fake/index.d.ts +13 -4
- package/dist/fn-context/fake/index.js +29 -2
- package/dist/fn-context/fn-params.d.ts +3 -0
- package/dist/fn-context/fn-params.js +10 -0
- package/dist/fn-context/index.d.ts +5 -5
- package/dist/fn-context/index.js +4 -4
- package/dist/fn-context/log.js +2 -2
- package/dist/fn-context/make-ctx.d.ts +11 -0
- package/dist/fn-context/make-ctx.js +20 -9
- package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +32 -0
- package/dist/fn-context/rpc/case-orchestrator-tools.js +12 -0
- 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 +4 -2
- package/dist/fn-context/rpc/index.js +4 -2
- package/dist/fn-context/rpc/integration/index.d.ts +15 -5
- package/dist/fn-context/rpc/integration/index.js +1 -1
- package/dist/fn-context/rpc/integration/list-surfaces.d.ts +21 -0
- package/dist/fn-context/rpc/integration/{list-bindings.js → list-surfaces.js} +4 -4
- package/dist/fn-context/rpc/log.d.ts +11 -1
- package/dist/fn-context/rpc/platform.d.ts +13 -3
- package/dist/fn-context/rpc/platform.js +1 -1
- package/dist/fn-context/rpc/records.d.ts +85 -0
- package/dist/fn-context/rpc/records.js +24 -0
- package/dist/fn-context/rpc/trpc-client.d.ts +44 -2
- package/dist/fn-context/rpc/trpc-client.js +222 -12
- package/dist/fn-context/rpc/trpc-init.d.ts +21 -3
- package/dist/fn-context/rpc/trpc-init.js +10 -4
- package/dist/fn-context/rpc/trpc-router-factory.d.ts +111 -7
- package/dist/fn-context/rpc/trpc-router-factory.js +2 -0
- package/dist/fn-context/run-scope.d.ts +4 -0
- package/dist/fn-context/run-scope.js +9 -0
- package/dist/fn-context/types.d.ts +52 -5
- package/dist/index.d.ts +36 -46
- package/dist/index.js +30 -33
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +49 -0
- package/dist/platform.d.ts +75 -0
- package/dist/platform.js +57 -0
- 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 +50 -6
- 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 +55 -8
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +11 -0
- package/package.json +38 -15
- package/dist/builder/compile/compile-agenda.d.ts +0 -15
- package/dist/builder/compile/compile-agenda.js +0 -64
- package/dist/builder/merge.d.ts +0 -4
- package/dist/builder/merge.js +0 -19
- package/dist/compute-host/read-only-cache.d.ts +0 -20
- package/dist/compute-host/read-only-cache.js +0 -61
- package/dist/core/context/db.d.ts +0 -69
- package/dist/core/context/db.js +0 -44
- package/dist/core/context/docusign.d.ts +0 -134
- package/dist/core/context/docusign.js +0 -108
- package/dist/core/context/filevine.d.ts +0 -323
- package/dist/core/context/filevine.js +0 -254
- package/dist/core/context/http.d.ts +0 -9
- package/dist/core/context/http.js +0 -4
- package/dist/core/context/hubspot.d.ts +0 -250
- package/dist/core/context/hubspot.js +0 -191
- package/dist/core/context/twilio.d.ts +0 -50
- package/dist/core/context/twilio.js +0 -42
- package/dist/core/functions/define/case-context.d.ts +0 -14
- package/dist/core/functions/define/case-context.js +0 -15
- package/dist/core/functions/define/compute.d.ts +0 -7
- package/dist/core/functions/define/compute.js +0 -17
- package/dist/core/functions/define/custom.d.ts +0 -4
- package/dist/core/functions/define/custom.js +0 -8
- package/dist/core/functions/define/system.d.ts +0 -7
- package/dist/core/functions/define/system.js +0 -12
- package/dist/core/functions/func-ref.d.ts +0 -16
- package/dist/core/functions/func-ref.js +0 -20
- package/dist/core/slot/deps.d.ts +0 -15
- package/dist/core/slot/deps.js +0 -88
- package/dist/core/slot/derived.d.ts +0 -32
- package/dist/core/slot/derived.js +0 -115
- package/dist/core/slot/platform-session-fields.d.ts +0 -38
- package/dist/core/slot/platform-session-fields.js +0 -48
- package/dist/core/slot/sf-schema.d.ts +0 -23
- package/dist/core/slot/sf-schema.js +0 -125
- package/dist/core/slot/sf-validate.d.ts +0 -14
- package/dist/core/slot/sf-validate.js +0 -132
- package/dist/core/slot/snapshot.d.ts +0 -18
- package/dist/core/slot/snapshot.js +0 -138
- package/dist/core/slot/write.d.ts +0 -48
- package/dist/core/slot/write.js +0 -426
- package/dist/core/stage/builders.d.ts +0 -54
- package/dist/core/stage/builders.js +0 -31
- package/dist/core/stage/condition.d.ts +0 -17
- package/dist/core/stage/condition.js +0 -198
- package/dist/core/stage/drive.d.ts +0 -43
- package/dist/core/stage/drive.js +0 -78
- package/dist/core/stage/finish-when.d.ts +0 -20
- package/dist/core/stage/finish-when.js +0 -82
- package/dist/core/stage/flatten.d.ts +0 -27
- package/dist/core/stage/flatten.js +0 -466
- package/dist/core/stage/index.d.ts +0 -15
- package/dist/core/stage/index.js +0 -13
- package/dist/core/stage/interrupts.d.ts +0 -28
- package/dist/core/stage/interrupts.js +0 -120
- package/dist/core/stage/ivr-fields.d.ts +0 -9
- package/dist/core/stage/ivr-fields.js +0 -75
- package/dist/core/stage/simulate.d.ts +0 -24
- package/dist/core/stage/simulate.js +0 -33
- package/dist/core/stage/transfer-fields.d.ts +0 -19
- package/dist/core/stage/transfer-fields.js +0 -129
- package/dist/core/stage/transfer-spec.d.ts +0 -48
- package/dist/core/stage/transfer-spec.js +0 -113
- package/dist/core/stage/types.d.ts +0 -208
- package/dist/core/stage/types.js +0 -2
- package/dist/core/stage/validate.d.ts +0 -11
- package/dist/core/stage/validate.js +0 -330
- package/dist/core/stage/walk.d.ts +0 -39
- package/dist/core/stage/walk.js +0 -119
- package/dist/fn-context/rpc/integration/list-bindings.d.ts +0 -21
- package/dist/fn-context/system-args.d.ts +0 -6
- package/dist/fn-context/system-args.js +0 -14
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*
|
|
5
|
+
* The document is walked once. Every slot becomes a cell joined with its schema definition; its value
|
|
6
|
+
* is taken from what the platform itself read (`PageReads`), never from the document, and a slot
|
|
7
|
+
* whose row this run did not read is shown as plain read-only content (anti-forgery). Every action
|
|
8
|
+
* becomes a button with the function-call arguments the AI fills; what the page prefilled stays here,
|
|
9
|
+
* on the server. An action that cannot be pressed is left off the page.
|
|
10
|
+
*
|
|
11
|
+
* The rows on the page are the rows with a cell on it: a lookup cell, and an action's row parameter,
|
|
12
|
+
* choose among the rows of their table there (15 §10.15 point 9). So lookups and actions are rendered
|
|
13
|
+
* after the walk, once every row on the page is known.
|
|
14
|
+
*
|
|
15
|
+
* Cells are addressed by slot id `table:<uuid tail>.field` (`RowNames`: the row's own, layout-free);
|
|
16
|
+
* actions and blocks by their page path (`injuries.rows.0.markError`), resolved against the very page
|
|
17
|
+
* the AI saw.
|
|
18
|
+
*/
|
|
19
|
+
import { parentOf } from '../slot/company-metadata.js';
|
|
20
|
+
import { ROW_ID_FIELD } from '../slot/schema.js';
|
|
21
|
+
import { getCompiledTable, getTableField } from '../slot/compile-schema.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';
|
|
26
|
+
// ---------------------------------------------------------------------------------------------
|
|
27
|
+
// Row names (15 §10.6.2)
|
|
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.
|
|
36
|
+
*/
|
|
37
|
+
export class RowNames {
|
|
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();
|
|
42
|
+
name(table, id) {
|
|
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;
|
|
50
|
+
}
|
|
51
|
+
resolve(name) {
|
|
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 };
|
|
64
|
+
}
|
|
65
|
+
nameOf(id) {
|
|
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;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function shortRowName(table, id) {
|
|
74
|
+
return `${table}:${id.slice(-12)}`;
|
|
75
|
+
}
|
|
76
|
+
export class PageDocumentError extends Error {
|
|
77
|
+
constructor(message) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = 'PageDocumentError';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const MAX_DEPTH = 32;
|
|
83
|
+
/** Channel actions only a call can do; by text only `end` (17b §9.8). */
|
|
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']);
|
|
87
|
+
export function renderDocument(input) {
|
|
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);
|
|
94
|
+
const { schema, reads, names } = input;
|
|
95
|
+
const envelopePage = envelope.page;
|
|
96
|
+
const cells = {};
|
|
97
|
+
const actions = {};
|
|
98
|
+
const rows = {};
|
|
99
|
+
const warnings = [];
|
|
100
|
+
const pendingActions = [];
|
|
101
|
+
const extractions = pageExtractions(envelopePage.extraction, schema, warnings);
|
|
102
|
+
const rowName = (table, id) => {
|
|
103
|
+
const name = names.name(table, id);
|
|
104
|
+
if (!rows[name]) {
|
|
105
|
+
const parent = parentOf(getCompiledTable(schema, table));
|
|
106
|
+
const parentValue = parent ? reads.row(id)?.slots[parent.field]?.value : null;
|
|
107
|
+
rows[name] = { table, id, parentId: typeof parentValue === 'string' ? parentValue : null };
|
|
108
|
+
}
|
|
109
|
+
return name;
|
|
110
|
+
};
|
|
111
|
+
const cellOf = (key, path, value) => {
|
|
112
|
+
const refCell = isRefFieldCell(value) ? value : null;
|
|
113
|
+
const fieldCell = isFieldCell(value) ? value : refCell ? { $field: true, slot: refCell.slot, ...(refCell.readOnly ? { readOnly: true } : {}) } : null;
|
|
114
|
+
const slot = fieldCell ? fieldCell.slot : value;
|
|
115
|
+
if (!isSlotLike(slot))
|
|
116
|
+
return null;
|
|
117
|
+
const ref = slot.ref;
|
|
118
|
+
const read = reads.row(ref.id);
|
|
119
|
+
const table = getCompiledTable(schema, ref.table);
|
|
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
|
+
}
|
|
127
|
+
if (!read || read.table !== ref.table || !def || !(ref.field in read.slots)) {
|
|
128
|
+
warnings.push(`${path}: a slot this run did not read (${ref.table}.${ref.field}); shown as read-only text`);
|
|
129
|
+
return { kind: 'text', key, path, value: displayable(slot.value) };
|
|
130
|
+
}
|
|
131
|
+
const truth = read.slots[ref.field];
|
|
132
|
+
const name = rowName(ref.table, ref.id);
|
|
133
|
+
const slotId = `${name}.${ref.field}`;
|
|
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: '…' }))`);
|
|
139
|
+
if (refCell && !isLookup)
|
|
140
|
+
throw new PageDocumentError(`${path}: refField(...) is for a ref field; ${ref.table}.${ref.field} is not a ref`);
|
|
141
|
+
const offered = refCell ? refOptions(path, refCell, def.ref.table) : null;
|
|
142
|
+
// The page decides (19-page-writable-cells): `field(slot, { readOnly: true })` is read-only, `field(slot)` opens it.
|
|
143
|
+
// A ref field with no row to offer is shown, not opened: nothing to choose (data, not a page mistake).
|
|
144
|
+
const placeWritable = fieldCell !== null && !fieldCell.readOnly && (!offered || offered.length > 0);
|
|
145
|
+
const existing = cells[slotId];
|
|
146
|
+
if (existing) {
|
|
147
|
+
existing.writable ||= placeWritable;
|
|
148
|
+
existing.paths.push(path);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const cell = {
|
|
152
|
+
slotId,
|
|
153
|
+
ref: { table: ref.table, id: ref.id, field: ref.field },
|
|
154
|
+
row: name,
|
|
155
|
+
label: def.label,
|
|
156
|
+
type: def.dataType,
|
|
157
|
+
choices: def.options,
|
|
158
|
+
value: truth.value,
|
|
159
|
+
state: truth.state,
|
|
160
|
+
reason: truth.reason,
|
|
161
|
+
updatedAt: truth.updatedAt,
|
|
162
|
+
writable: placeWritable,
|
|
163
|
+
paths: [path],
|
|
164
|
+
};
|
|
165
|
+
if (def.description)
|
|
166
|
+
cell.description = def.description;
|
|
167
|
+
if (def.extraction)
|
|
168
|
+
cell.extraction = def.extraction;
|
|
169
|
+
if (isLookup)
|
|
170
|
+
cell.type = 'ref';
|
|
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);
|
|
177
|
+
}
|
|
178
|
+
if (offered) {
|
|
179
|
+
const target = cells[slotId];
|
|
180
|
+
// Writable, the rows offered; read-only, only the labels (who the current value is).
|
|
181
|
+
if (placeWritable)
|
|
182
|
+
target.options = [...new Set([...(target.options ?? []), ...offered.map((o) => o.name)])];
|
|
183
|
+
target.optionLabels = { ...target.optionLabels, ...Object.fromEntries(offered.map((o) => [o.name, o.label])) };
|
|
184
|
+
}
|
|
185
|
+
const node = { kind: 'cell', key, path, slotId, readOnly: !placeWritable };
|
|
186
|
+
// A ref field's definition is always on the page: its meaning is not in its name (26 §16.2 #7).
|
|
187
|
+
if (fieldCell?.expand || refCell)
|
|
188
|
+
node.expand = true;
|
|
189
|
+
return node;
|
|
190
|
+
};
|
|
191
|
+
/** A refField's rows: each must be a row this run read, of the table the field points at (it goes on the page). */
|
|
192
|
+
const refOptions = (path, cell, target) => cell.options.map((option) => {
|
|
193
|
+
const read = reads.row(option.id);
|
|
194
|
+
if (!read || read.table !== target)
|
|
195
|
+
throw new PageDocumentError(`${path}: refField offers ${option.id}, not a ${target} row this run read`);
|
|
196
|
+
return { name: rowName(target, option.id), label: option.label };
|
|
197
|
+
});
|
|
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) => {
|
|
203
|
+
if (depth > MAX_DEPTH)
|
|
204
|
+
throw new PageDocumentError(`${path || 'page'}: nested deeper than ${MAX_DEPTH}`);
|
|
205
|
+
if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
206
|
+
return { kind: 'text', key, path, value };
|
|
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)) };
|
|
209
|
+
if (isPlainObject(value)) {
|
|
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;
|
|
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;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
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;
|
|
282
|
+
// Every row on the page is known now: what lookups and row parameters may choose.
|
|
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) {
|
|
286
|
+
const rendered = renderAction(path, value, env);
|
|
287
|
+
if (rendered)
|
|
288
|
+
placed.push({ action: rendered, node });
|
|
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
|
+
});
|
|
296
|
+
// An action that cannot be pressed is not on the page.
|
|
297
|
+
const prune = (block) => {
|
|
298
|
+
block.children = block.children.filter((child) => child.kind !== 'action' || child.actionId in actions);
|
|
299
|
+
for (const child of block.children)
|
|
300
|
+
if (child.kind === 'block')
|
|
301
|
+
prune(child);
|
|
302
|
+
};
|
|
303
|
+
prune(root);
|
|
304
|
+
const page = { root, cells, actions, rows, warnings };
|
|
305
|
+
if (extractions.size)
|
|
306
|
+
page.extractions = Object.fromEntries(extractions);
|
|
307
|
+
if (voice !== undefined)
|
|
308
|
+
page.voice = voice;
|
|
309
|
+
return page;
|
|
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
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The action as the AI presses it, or null when it cannot be pressed on this page: with a warning when
|
|
330
|
+
* the page placed it wrong; silently when a required row choice has no row on the page (no injury yet
|
|
331
|
+
* to link a provider to), which is the data, not a mistake.
|
|
332
|
+
*/
|
|
333
|
+
function renderAction(path, action, env) {
|
|
334
|
+
const rendered = renderActionOf(path, action, env);
|
|
335
|
+
if (!rendered?.input)
|
|
336
|
+
return rendered;
|
|
337
|
+
const missing = unfillable(rendered.input.properties, rendered.input.required);
|
|
338
|
+
if (!missing)
|
|
339
|
+
return rendered;
|
|
340
|
+
// Said, so whoever reads the logs knows why the AI had no such button (25 §10).
|
|
341
|
+
env.warnings.push(`${path}: ${rendered.name} left off the page: no ${missing.table} row to choose for ${missing.key}`);
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
/** The first required row choice with no row to choose, at any depth; null when every one can be filled. */
|
|
345
|
+
function unfillable(properties, required, prefix = '') {
|
|
346
|
+
for (const key of required ?? []) {
|
|
347
|
+
const prop = properties[key];
|
|
348
|
+
if (prop?.type === 'ref' && !prop.enum.length)
|
|
349
|
+
return { key: `${prefix}${key}`, table: prop.table };
|
|
350
|
+
if (prop?.type === 'object') {
|
|
351
|
+
const inner = unfillable(prop.properties, prop.required, `${prefix}${key}.`);
|
|
352
|
+
if (inner)
|
|
353
|
+
return inner;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
function renderActionOf(path, action, env) {
|
|
359
|
+
const { schema, reads, rowName, warnings } = env;
|
|
360
|
+
if (action.$action === 'channel') {
|
|
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.
|
|
363
|
+
if (env.channel !== undefined && env.channel !== 'call' && CALL_ONLY.has(action.channel)) {
|
|
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;
|
|
366
|
+
}
|
|
367
|
+
return channelButton(path, action);
|
|
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
|
+
}
|
|
376
|
+
if (action.$action === 'createRow') {
|
|
377
|
+
const table = getCompiledTable(schema, action.table);
|
|
378
|
+
if (!table)
|
|
379
|
+
throw new PageDocumentError(`${path}: createRow on ${action.table}, not a table of this sequence`);
|
|
380
|
+
const parent = parentOf(table);
|
|
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)) {
|
|
386
|
+
if (!getTableField(table, key))
|
|
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
|
+
}
|
|
408
|
+
}
|
|
409
|
+
for (const fieldName of action.fields) {
|
|
410
|
+
const def = getTableField(table, fieldName);
|
|
411
|
+
if (!def)
|
|
412
|
+
throw new PageDocumentError(`${path}: createRow field ${action.table}.${fieldName} is not in this sequence`);
|
|
413
|
+
if (parent?.field === fieldName)
|
|
414
|
+
throw new PageDocumentError(`${path}: the parent ${fieldName} is prefilled, not a field to fill`);
|
|
415
|
+
if (fieldName in action.prefill)
|
|
416
|
+
continue;
|
|
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);
|
|
420
|
+
}
|
|
421
|
+
// Where the new row hangs: prefilled, else the one parent this run's reads of the table were anchored on.
|
|
422
|
+
let parentId = null;
|
|
423
|
+
if (parent) {
|
|
424
|
+
const preset = action.prefill[parent.field];
|
|
425
|
+
const parents = reads.parentsOf(action.table);
|
|
426
|
+
parentId = typeof preset === 'string' ? preset : parents.length === 1 ? parents[0] : null;
|
|
427
|
+
if (!parentId) {
|
|
428
|
+
warnings.push(`${path}: createRow(${action.table}) needs ${parent.field} prefilled: this run read ${parents.length} parents of it; left off the page`);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
// The parent row must be a row this run read, of the parent field's table: it goes on the page,
|
|
432
|
+
// so pressing the button can write under it.
|
|
433
|
+
const parentRow = reads.row(parentId);
|
|
434
|
+
if (!parentRow || parentRow.table !== parent.table) {
|
|
435
|
+
warnings.push(`${path}: createRow(${action.table}) parent ${parentId} is not a ${parent.table} row this run read; left off the page`);
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
rowName(parentRow.table, parentId);
|
|
439
|
+
}
|
|
440
|
+
const set = Object.fromEntries(Object.entries(action.prefill).filter(([key, value]) => !isChoose(value) && key !== parent?.field));
|
|
441
|
+
const under = parent && parentId ? ` under ${rowName(parent.table, parentId)}` : '';
|
|
442
|
+
return {
|
|
443
|
+
id: path,
|
|
444
|
+
kind: 'createRow',
|
|
445
|
+
name: action.label ?? `Add ${table.label}`,
|
|
446
|
+
description: action.description ?? `Adds a new ${table.label} (${action.table})${under}.`,
|
|
447
|
+
input: { type: 'object', properties, required: action.required.filter((r) => r in properties) },
|
|
448
|
+
server: { kind: 'createRow', table: action.table, parentId, set, fields: [...action.fields, ...chosen], required: action.required, ...(action.key ? { key: action.key } : {}) },
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (action.$action === 'deleteRow') {
|
|
452
|
+
const read = reads.row(action.rowId);
|
|
453
|
+
if (!read) {
|
|
454
|
+
warnings.push(`${path}: deleteRow on a row this run did not read; left off the page`);
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
const name = rowName(read.table, action.rowId);
|
|
458
|
+
return {
|
|
459
|
+
id: path,
|
|
460
|
+
kind: 'deleteRow',
|
|
461
|
+
name: action.label ?? `Delete ${name}`,
|
|
462
|
+
description: action.description ?? `Delete ${name} and everything under it.`,
|
|
463
|
+
input: null,
|
|
464
|
+
server: { kind: 'deleteRow', table: read.table, rowId: action.rowId, ...(action.reason ? { reason: action.reason } : {}), ...(action.key ? { key: action.key } : {}) },
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (action.$action === 'followUp')
|
|
468
|
+
return followUpButton(path, action, env);
|
|
469
|
+
if (action.$action === 'cancelFollowUp')
|
|
470
|
+
return cancelFollowUpButton(path, action);
|
|
471
|
+
if (action.$action === 'function') {
|
|
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);
|
|
475
|
+
return {
|
|
476
|
+
id: path,
|
|
477
|
+
kind: 'function',
|
|
478
|
+
name: action.label ?? action.name,
|
|
479
|
+
description: action.description ?? `Runs ${action.name}.`,
|
|
480
|
+
input,
|
|
481
|
+
server: { kind: 'function', name: action.name, ...(action.say ? { say: action.say } : {}), ...(Object.keys(prefill).length ? { prefill } : {}), ...(action.key ? { key: action.key } : {}) },
|
|
482
|
+
};
|
|
483
|
+
}
|
|
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);
|
|
487
|
+
return {
|
|
488
|
+
id: path,
|
|
489
|
+
kind: 'custom',
|
|
490
|
+
name: action.label ?? action.name,
|
|
491
|
+
description: action.description ?? `Runs ${action.name}.`,
|
|
492
|
+
input,
|
|
493
|
+
server: { kind: 'custom', name: action.name, prefill, ...(action.key ? { key: action.key } : {}) },
|
|
494
|
+
};
|
|
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
|
+
}
|
|
500
|
+
/**
|
|
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.
|
|
506
|
+
*/
|
|
507
|
+
function actionInput(path, name, parameters, given, env, args = {}) {
|
|
508
|
+
const declared = parameters?.properties ?? {};
|
|
509
|
+
const properties = {};
|
|
510
|
+
const prefill = {};
|
|
511
|
+
for (const key of Object.keys(given)) {
|
|
512
|
+
if (!(key in declared))
|
|
513
|
+
throw new PageDocumentError(`${path}: ${name} prefills ${key}, not one of its parameters`);
|
|
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 ?? [];
|
|
525
|
+
for (const [key, param] of Object.entries(declared)) {
|
|
526
|
+
const value = given[key];
|
|
527
|
+
if (isChoose(value)) {
|
|
528
|
+
const chosen = chooseParam(`${path}: ${name} options.${key}`, param, value, env);
|
|
529
|
+
if (!emptyOptional(chosen, key, required))
|
|
530
|
+
properties[key] = chosen;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
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;
|
|
541
|
+
checkPrefill(`${path}: ${name}({ ${key} })`, param, value, env);
|
|
542
|
+
prefill[key] = value;
|
|
543
|
+
continue;
|
|
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
|
+
}
|
|
548
|
+
properties[key] = expandParam(`${path}.${key}`, param, env);
|
|
549
|
+
}
|
|
550
|
+
for (const [key, text] of Object.entries(args)) {
|
|
551
|
+
if (properties[key] && text.trim())
|
|
552
|
+
properties[key] = { ...properties[key], description: text };
|
|
553
|
+
}
|
|
554
|
+
const input = Object.keys(properties).length
|
|
555
|
+
? { type: 'object', properties, required: required.filter((r) => r in properties) }
|
|
556
|
+
: null;
|
|
557
|
+
return { input, prefill };
|
|
558
|
+
}
|
|
559
|
+
/** A row parameter's candidates, as `choose(...)` gave them: rows this run read, of the parameter's table. */
|
|
560
|
+
function chooseParam(where, param, chosen, env) {
|
|
561
|
+
const table = isRefParam(param) ? param.table : isSchemaFieldParam(param) ? rowTableOf(where, param, env.schema) : null;
|
|
562
|
+
if (!table)
|
|
563
|
+
throw new PageDocumentError(`${where}: choose(...) is for a row parameter (Id<'table'>)`);
|
|
564
|
+
if (chosen.table !== null && chosen.table !== table)
|
|
565
|
+
throw new PageDocumentError(`${where}: choose(...) gives ${chosen.table} rows, the parameter takes ${table}`);
|
|
566
|
+
const names = [];
|
|
567
|
+
const labels = {};
|
|
568
|
+
for (const option of chosen.options) {
|
|
569
|
+
const read = env.reads.row(option.id);
|
|
570
|
+
if (!read || read.table !== table)
|
|
571
|
+
throw new PageDocumentError(`${where}: choose(...) offers ${option.id}, not a ${table} row this run read`);
|
|
572
|
+
const rowName = env.rowName(table, option.id);
|
|
573
|
+
names.push(rowName);
|
|
574
|
+
labels[rowName] = option.label;
|
|
575
|
+
}
|
|
576
|
+
// Each row with its label, as a ref field's candidates are written (26 §16.2 #7).
|
|
577
|
+
const help = isSchemaFieldParam(param) ? (param.extraction ?? schemaFieldOf(where, param, env.schema).def?.extraction) : undefined;
|
|
578
|
+
return { type: 'ref', table, enum: names, labels, ...(help ? { description: help } : {}) };
|
|
579
|
+
}
|
|
580
|
+
/** `table.field` of a schema-field parameter, checked against this sequence. */
|
|
581
|
+
function schemaFieldOf(where, param, schema) {
|
|
582
|
+
const [table, field, ...rest] = param.$schemaField.split('.');
|
|
583
|
+
const def = table ? getCompiledTable(schema, table) : undefined;
|
|
584
|
+
if (!table || !field || rest.length || !def)
|
|
585
|
+
throw new PageDocumentError(`${where}: '${param.$schemaField}' is not a table.field of this sequence`);
|
|
586
|
+
if (field === 'id')
|
|
587
|
+
return { table, field, def: null };
|
|
588
|
+
const fieldDef = getTableField(def, field);
|
|
589
|
+
if (!fieldDef)
|
|
590
|
+
throw new PageDocumentError(`${where}: '${param.$schemaField}' is not a field of this sequence`);
|
|
591
|
+
return { table, field, def: fieldDef };
|
|
592
|
+
}
|
|
593
|
+
/** The table a parameter chooses a row of (`table.id`, a lookup), or null. */
|
|
594
|
+
function rowTableOf(where, param, schema) {
|
|
595
|
+
const { table, def } = schemaFieldOf(where, param, schema);
|
|
596
|
+
if (!def)
|
|
597
|
+
return table;
|
|
598
|
+
return def.ref ? def.ref.table : null;
|
|
599
|
+
}
|
|
600
|
+
/** A parameter as JSON Schema for the AI: schema fields filled in from the schema, row choices as enums. */
|
|
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`);
|
|
604
|
+
if (isSchemaFieldParam(param)) {
|
|
605
|
+
const rowTable = rowTableOf(where, param, env.schema);
|
|
606
|
+
const { table, def } = schemaFieldOf(where, param, env.schema);
|
|
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`);
|
|
610
|
+
// The page's extraction replaces the schema's; type and allowed values stay the schema's.
|
|
611
|
+
const fieldDef = param.extraction !== undefined ? { ...def, extraction: param.extraction } : def;
|
|
612
|
+
return fieldDef.dataType === 'contact_schedule' ? scheduleProperty(fieldDef.extraction || undefined) : scalarProperty(fieldDef);
|
|
613
|
+
}
|
|
614
|
+
if (param.type === 'array') {
|
|
615
|
+
const out = { type: 'array', items: expandParam(`${where}[]`, param.items, env) };
|
|
616
|
+
if (param.description)
|
|
617
|
+
out.description = param.description;
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
if (param.type === 'object') {
|
|
621
|
+
const properties = {};
|
|
622
|
+
for (const [key, child] of Object.entries(param.properties))
|
|
623
|
+
properties[key] = expandParam(`${where}.${key}`, child, env);
|
|
624
|
+
const out = { type: 'object', properties };
|
|
625
|
+
if (param.required)
|
|
626
|
+
out.required = [...param.required];
|
|
627
|
+
if (param.description)
|
|
628
|
+
out.description = param.description;
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
if (param.type === 'contact_schedule')
|
|
632
|
+
return scheduleProperty(param.description);
|
|
633
|
+
return { ...param };
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* A prefilled row argument, at any depth (`{ target: { id } }`, a list of rows), must be a row this run
|
|
637
|
+
* read, of its table: it then goes on the page, so the action may write it.
|
|
638
|
+
*/
|
|
639
|
+
function checkPrefill(where, param, value, env) {
|
|
640
|
+
if (value === null || value === undefined)
|
|
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
|
+
}
|
|
649
|
+
if (isSchemaFieldParam(param)) {
|
|
650
|
+
const rowTable = rowTableOf(where, param, env.schema);
|
|
651
|
+
if (!rowTable)
|
|
652
|
+
return;
|
|
653
|
+
const read = typeof value === 'string' ? env.reads.row(value) : undefined;
|
|
654
|
+
if (!read || read.table !== rowTable)
|
|
655
|
+
throw new PageDocumentError(`${where} is not a ${rowTable} row this run read`);
|
|
656
|
+
env.rowName(read.table, value);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (param.type === 'array') {
|
|
660
|
+
if (!Array.isArray(value))
|
|
661
|
+
throw new PageDocumentError(`${where} must be a list`);
|
|
662
|
+
value.forEach((item, i) => checkPrefill(`${where}[${i}]`, param.items, item, env));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (param.type === 'object') {
|
|
666
|
+
if (!isPlainObject(value))
|
|
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`);
|
|
672
|
+
for (const [key, child] of Object.entries(param.properties))
|
|
673
|
+
checkPrefill(`${where}.${key}`, child, value[key], env);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/** A schema field's argument: the field's own type and options. */
|
|
677
|
+
function scalarProperty(def) {
|
|
678
|
+
const help = describeField(def);
|
|
679
|
+
if (def.dataType === 'multi_select')
|
|
680
|
+
return { type: 'multi_select', enum: def.options ?? [], ...help };
|
|
681
|
+
if (def.dataType === 'select')
|
|
682
|
+
return { type: 'select', enum: def.options ?? [], ...help };
|
|
683
|
+
return { type: def.dataType, ...help };
|
|
684
|
+
}
|
|
685
|
+
/** A contact's hours (a `contact_schedule` field, a `ContactSchedule` argument): its own structure, not text. */
|
|
686
|
+
function scheduleProperty(help) {
|
|
687
|
+
const window = {
|
|
688
|
+
type: 'object',
|
|
689
|
+
properties: {
|
|
690
|
+
start: { type: 'string', description: 'HH:mm' },
|
|
691
|
+
end: { type: 'string', description: 'HH:mm, after start; may be 24:00' },
|
|
692
|
+
},
|
|
693
|
+
required: ['start', 'end'],
|
|
694
|
+
};
|
|
695
|
+
const windows = { type: 'array', items: window, description: 'sorted by start, not overlapping; empty: no contact' };
|
|
696
|
+
const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
|
|
697
|
+
return {
|
|
698
|
+
type: 'object',
|
|
699
|
+
'x-type': 'contact_schedule',
|
|
700
|
+
...(help ? { description: help } : {}),
|
|
701
|
+
properties: {
|
|
702
|
+
weekly: {
|
|
703
|
+
type: 'object',
|
|
704
|
+
nullable: true,
|
|
705
|
+
description: 'all seven weekdays; null: no weekly restriction',
|
|
706
|
+
properties: Object.fromEntries(days.map((day) => [day, windows])),
|
|
707
|
+
required: [...days],
|
|
708
|
+
},
|
|
709
|
+
dateOverrides: {
|
|
710
|
+
type: 'array',
|
|
711
|
+
description: 'dates whose windows replace weekly',
|
|
712
|
+
items: {
|
|
713
|
+
type: 'object',
|
|
714
|
+
properties: { dates: { type: 'array', items: { type: 'string', description: 'YYYY-MM-DD' } }, windows },
|
|
715
|
+
required: ['dates', 'windows'],
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
required: ['weekly', 'dateOverrides'],
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* An argument's help (26 §16.2 #5a): how to fill it, the page's (`page({ extraction })`) or else
|
|
724
|
+
* the field's; none when neither says. The label is not repeated in it; how its type is written is the
|
|
725
|
+
* system message's (26 §15.8).
|
|
726
|
+
*/
|
|
727
|
+
function describeField(def) {
|
|
728
|
+
return def.extraction ? { description: def.extraction } : {};
|
|
729
|
+
}
|
|
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
|
+
}
|
|
819
|
+
function join(path, key) {
|
|
820
|
+
return path ? `${path}.${key}` : key;
|
|
821
|
+
}
|
|
822
|
+
function isPlainObject(value) {
|
|
823
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
824
|
+
}
|
|
825
|
+
function isSlotLike(value) {
|
|
826
|
+
if (!isPlainObject(value) || !isPlainObject(value.ref) || !('state' in value))
|
|
827
|
+
return false;
|
|
828
|
+
const ref = value.ref;
|
|
829
|
+
return typeof ref.table === 'string' && typeof ref.id === 'string' && typeof ref.field === 'string';
|
|
830
|
+
}
|
|
831
|
+
function isFieldCell(value) {
|
|
832
|
+
return isPlainObject(value) && value.$field === true;
|
|
833
|
+
}
|
|
834
|
+
function isRefFieldCell(value) {
|
|
835
|
+
return isPlainObject(value) && value.$refField === true && Array.isArray(value.options);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* The run key of a button on the page (17b §7): the action's own key, whoever runs it — the model
|
|
839
|
+
* pressing it, or the page function returning it — not its place on the page, which moves with rows.
|
|
840
|
+
*/
|
|
841
|
+
export function runKeyOf(action) {
|
|
842
|
+
const server = action.server;
|
|
843
|
+
if (server.kind === 'custom')
|
|
844
|
+
return server.key ?? server.name;
|
|
845
|
+
if (server.kind === 'createRow')
|
|
846
|
+
return server.key ?? `createRow:${server.table}`;
|
|
847
|
+
if (server.kind === 'deleteRow')
|
|
848
|
+
return server.key ?? `deleteRow:${server.rowId}`;
|
|
849
|
+
if (server.kind === 'function')
|
|
850
|
+
return server.key ?? server.name;
|
|
851
|
+
if (server.kind === 'followUp' || server.kind === 'cancelFollowUp')
|
|
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;
|
|
856
|
+
return actionKey(server.spec);
|
|
857
|
+
}
|
|
858
|
+
/** The run key of an action the page function returned, when it names none. */
|
|
859
|
+
export function actionKey(action) {
|
|
860
|
+
if (action.$action === 'custom')
|
|
861
|
+
return action.key ?? action.name;
|
|
862
|
+
if (action.$action === 'function')
|
|
863
|
+
return action.key ?? action.name;
|
|
864
|
+
if (action.$action === 'followUp' || action.$action === 'cancelFollowUp')
|
|
865
|
+
return action.key;
|
|
866
|
+
if (action.$action === 'createRow')
|
|
867
|
+
return action.key ?? `createRow:${action.table}`;
|
|
868
|
+
if (action.$action === 'deleteRow')
|
|
869
|
+
return action.key ?? `deleteRow:${action.rowId}`;
|
|
870
|
+
if (action.$action === 'tool')
|
|
871
|
+
return action.tool;
|
|
872
|
+
if (action.channel === 'keypad' || action.channel === 'announce')
|
|
873
|
+
return action.key;
|
|
874
|
+
return action.key ?? action.channel;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* The page's `$extraction` (26 §15.3): how to fill a field on this page, by `table.field`, replacing the
|
|
878
|
+
* schema's for every place it is on the page. One per field for the whole page, so a field means the
|
|
879
|
+
* same everywhere on it. A `table.field` the schema does not have is warned and left out.
|
|
880
|
+
*/
|
|
881
|
+
/** `page({ extraction })` (26 §15.3): how to fill a field on this page, by `table.field`. */
|
|
882
|
+
function pageExtractions(value, schema, warnings) {
|
|
883
|
+
const out = new Map();
|
|
884
|
+
if (value === undefined || value === null)
|
|
885
|
+
return out;
|
|
886
|
+
if (!isPlainObject(value))
|
|
887
|
+
throw new PageDocumentError('page({ extraction }): an object of table.field → text');
|
|
888
|
+
for (const [key, text] of Object.entries(value)) {
|
|
889
|
+
if (typeof text !== 'string')
|
|
890
|
+
throw new PageDocumentError(`page({ extraction }): '${key}' must be text`);
|
|
891
|
+
const [table, field, ...rest] = key.split('.');
|
|
892
|
+
if (!table || !field || rest.length || !getCompiledTable(schema, table)?.fields[field]) {
|
|
893
|
+
warnings.push(`page({ extraction }): '${key}' is not a table.field of the schema; left out`);
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
out.set(key, text);
|
|
897
|
+
}
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
900
|
+
const CHANNEL_NAMES = {
|
|
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.'],
|
|
902
|
+
keypad: ['Collect a keypress', 'The customer answers by pressing a key on their phone.'],
|
|
903
|
+
announce: ['Play a fixed statement', 'Plays a fixed statement, word for word.'],
|
|
904
|
+
end: ['End the conversation', 'Ends the conversation after your last line (say goodbye in the same step).'],
|
|
905
|
+
voicemail: ['Leave the voicemail', 'Leaves a fixed message after the beep, then hangs up.'],
|
|
906
|
+
};
|
|
907
|
+
const BACKGROUND_TRANSFER = [
|
|
908
|
+
'Call a person in the background',
|
|
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.',
|
|
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
|
+
}
|
|
937
|
+
/** A channel action as a button: nothing for the AI to fill; what it does stays on the server. */
|
|
938
|
+
function channelButton(path, action) {
|
|
939
|
+
const [name, description] = action.channel === 'transfer' && action.mode === 'background' ? BACKGROUND_TRANSFER : CHANNEL_NAMES[action.channel];
|
|
940
|
+
const label = 'label' in action ? action.label : undefined;
|
|
941
|
+
const text = 'description' in action ? action.description : undefined;
|
|
942
|
+
return {
|
|
943
|
+
id: path,
|
|
944
|
+
kind: 'channel',
|
|
945
|
+
name: label ?? name,
|
|
946
|
+
description: text ?? (action.channel === 'keypad' ? `${description} It says: "${fallbackSpoken(action.say).text}"` : description),
|
|
947
|
+
input: null,
|
|
948
|
+
server: { kind: 'channel', spec: action },
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
const CHANNEL_WORDS = { call: 'a call', sms: 'a text', email: 'an email' };
|
|
952
|
+
/**
|
|
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).
|
|
957
|
+
*/
|
|
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 ');
|
|
963
|
+
const properties = {};
|
|
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
|
+
}
|
|
988
|
+
return {
|
|
989
|
+
id: path,
|
|
990
|
+
kind: 'followUp',
|
|
991
|
+
name: action.label ?? 'Book the next step',
|
|
992
|
+
description: action.description ??
|
|
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 },
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
/** The cancel-next-step button (40 §2.2): the AI says why. */
|
|
1004
|
+
function cancelFollowUpButton(path, action) {
|
|
1005
|
+
return {
|
|
1006
|
+
id: path,
|
|
1007
|
+
kind: 'cancelFollowUp',
|
|
1008
|
+
name: action.label ?? 'Cancel the next step',
|
|
1009
|
+
description: action.description ??
|
|
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 },
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function isRefParam(param) {
|
|
1016
|
+
return !isSchemaFieldParam(param) && param.type === 'ref';
|
|
1017
|
+
}
|
|
1018
|
+
function isSchemaFieldParam(value) {
|
|
1019
|
+
return isPlainObject(value) && typeof value.$schemaField === 'string';
|
|
1020
|
+
}
|
|
1021
|
+
function displayable(value) {
|
|
1022
|
+
if (value === null || value === undefined)
|
|
1023
|
+
return null;
|
|
1024
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
1025
|
+
return value;
|
|
1026
|
+
return JSON.stringify(value);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* The page to show when the page script failed (15 §10.9): the last good page, every cell read-only
|
|
1030
|
+
* and no actions, marked stale; without one, an empty page.
|
|
1031
|
+
*/
|
|
1032
|
+
export function fallbackPage(last, reason) {
|
|
1033
|
+
if (!last) {
|
|
1034
|
+
return {
|
|
1035
|
+
root: { kind: 'block', key: '', path: '', list: false, children: [] },
|
|
1036
|
+
cells: {},
|
|
1037
|
+
actions: {},
|
|
1038
|
+
rows: {},
|
|
1039
|
+
warnings: [reason],
|
|
1040
|
+
fallback: { reason, stale: false },
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
const readOnly = (node) => node.kind === 'cell'
|
|
1044
|
+
? { ...node, readOnly: true }
|
|
1045
|
+
: node.kind === 'action'
|
|
1046
|
+
? null
|
|
1047
|
+
: node.kind === 'block'
|
|
1048
|
+
? { ...node, children: node.children.map(readOnly).filter((n) => n !== null) }
|
|
1049
|
+
: node;
|
|
1050
|
+
return {
|
|
1051
|
+
root: readOnly(last.root),
|
|
1052
|
+
cells: Object.fromEntries(Object.entries(last.cells).map(([id, cell]) => [id, { ...cell, writable: false }])),
|
|
1053
|
+
actions: {},
|
|
1054
|
+
rows: last.rows,
|
|
1055
|
+
...(last.extractions ? { extractions: last.extractions } : {}),
|
|
1056
|
+
warnings: [...last.warnings, reason],
|
|
1057
|
+
fallback: { reason, stale: true },
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
//# sourceMappingURL=render.js.map
|