@datalayer/agent-runtimes 1.3.82 → 1.3.84
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/lib/chat/base/ChatBase.js +16 -0
- package/lib/chat/base/loopElement.d.ts +53 -0
- package/lib/chat/base/loopElement.js +78 -0
- package/lib/chat/base/loopWindow.d.ts +31 -0
- package/lib/chat/base/loopWindow.js +74 -0
- package/lib/components/a2ui/datalayer/FileUpload.d.ts +10 -2
- package/lib/components/a2ui/datalayer/FileUpload.js +10 -7
- package/lib/components/a2ui/datalayer/Form.d.ts +2 -0
- package/lib/components/a2ui/datalayer/Form.js +26 -2
- package/lib/components/a2ui/datalayer/formWidgets.d.ts +19 -0
- package/lib/components/a2ui/datalayer/formWidgets.js +51 -0
- package/lib/loop/apps/AppComposer.d.ts +39 -4
- package/lib/loop/apps/AppComposer.js +191 -19
- package/lib/loop/apps/AppRenderer.d.ts +20 -3
- package/lib/loop/apps/AppRenderer.js +58 -27
- package/lib/loop/apps/appspec.d.ts +2 -0
- package/lib/loop/apps/appspec.js +196 -39
- package/lib/loop/apps/checks.d.ts +6 -0
- package/lib/loop/apps/checks.js +187 -8
- package/lib/loop/apps/composer.d.ts +21 -10
- package/lib/loop/apps/composer.js +41 -8
- package/lib/loop/apps/language.d.ts +68 -0
- package/lib/loop/apps/language.js +275 -0
- package/lib/loop/apps/settingsInputs.d.ts +29 -0
- package/lib/loop/apps/settingsInputs.js +147 -0
- package/lib/loop/apps/uploads.d.ts +43 -0
- package/lib/loop/apps/uploads.js +66 -0
- package/lib/loop/core/index.d.ts +14 -1
- package/lib/loop/embed/AppEmbed.d.ts +14 -0
- package/lib/loop/embed/AppEmbed.js +64 -2
- package/lib/loop/embed/element.js +23 -0
- package/lib/loop/embed/embedSession.d.ts +14 -0
- package/lib/loop/embed/embedSession.js +34 -0
- package/lib/loop/embed/hostBridge.d.ts +22 -1
- package/lib/loop/plugins/app-elements/AppElements.d.ts +27 -0
- package/lib/loop/plugins/app-elements/AppElements.js +76 -0
- package/lib/loop/plugins/app-elements/index.d.ts +20 -0
- package/lib/loop/plugins/app-elements/index.js +43 -0
- package/lib/loop/plugins/app-page/appPageModel.d.ts +14 -5
- package/lib/loop/plugins/app-page/appPageModel.js +82 -88
- package/lib/loop/plugins/chat/ChatView.js +10 -2
- package/lib/protocols/BrowserAgentAdapter.d.ts +8 -6
- package/lib/protocols/BrowserAgentAdapter.js +13 -10
- package/lib/runtimes/browser/protocol.d.ts +1 -1
- package/lib/specs/agents/agents.js +6 -3
- package/lib/specs/apps.js +337 -176
- package/lib/specs/appspecSchema.js +455 -88
- package/lib/specs/cogs.js +3 -2
- package/lib/specs/uiPlugins.js +6 -1
- package/lib/types/agentspecs.d.ts +167 -11
- package/package.json +1 -1
- package/scripts/codegen/generate_apps.py +16 -1
package/lib/loop/apps/appspec.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
import { BEHAVIOURS } from './rules';
|
|
6
|
+
import { DEFAULT_MAX_FILES, DEFAULT_UPLOAD_MB } from './uploads';
|
|
6
7
|
/** The version of the spec itself. */
|
|
7
8
|
export const APP_SCHEMA = 'loop.app/v1';
|
|
9
|
+
/** The language an application's own words are in, unless it says (LOOP P-26). */
|
|
10
|
+
export const DEFAULT_LANGUAGE = 'en';
|
|
8
11
|
/** The face of an application that has not chosen one. */
|
|
9
12
|
export const DEFAULT_EMOJI = '\u{1F440}';
|
|
10
13
|
export const APP_KINDS = ['chat', 'widget', 'decision', 'worker'];
|
|
@@ -124,7 +127,9 @@ export function emptyAppspec(kind = 'chat') {
|
|
|
124
127
|
starters: [],
|
|
125
128
|
commands: [],
|
|
126
129
|
modes: [],
|
|
127
|
-
|
|
130
|
+
profiles: [],
|
|
131
|
+
language: DEFAULT_LANGUAGE,
|
|
132
|
+
translations: {},
|
|
128
133
|
components: [],
|
|
129
134
|
outputs: [],
|
|
130
135
|
},
|
|
@@ -148,6 +153,7 @@ export function emptyAppspec(kind = 'chat') {
|
|
|
148
153
|
notifications: [],
|
|
149
154
|
setup: [],
|
|
150
155
|
enabled: true,
|
|
156
|
+
unavailable_because: '',
|
|
151
157
|
tags: [],
|
|
152
158
|
icon: DEFAULT_ICON,
|
|
153
159
|
emoji: DEFAULT_EMOJI,
|
|
@@ -170,6 +176,7 @@ const KNOWN_KEYS = [
|
|
|
170
176
|
'model',
|
|
171
177
|
'skills',
|
|
172
178
|
'backend_tools',
|
|
179
|
+
'tools',
|
|
173
180
|
'context',
|
|
174
181
|
'contents',
|
|
175
182
|
'connections',
|
|
@@ -186,12 +193,26 @@ const KNOWN_KEYS = [
|
|
|
186
193
|
'notifications',
|
|
187
194
|
'decision',
|
|
188
195
|
'enabled',
|
|
196
|
+
'unavailable_because',
|
|
189
197
|
'tags',
|
|
190
198
|
'icon',
|
|
191
199
|
'emoji',
|
|
192
200
|
'avatar',
|
|
193
201
|
'banner',
|
|
194
202
|
];
|
|
203
|
+
/** Where a check of its code runs (LOOP P-06). */
|
|
204
|
+
const CHECK_STAGES = ['answer', 'tool_call'];
|
|
205
|
+
/** A tool of its own, written in its code (LOOP P-06); a wrong one is said by the checks. */
|
|
206
|
+
function parseTool(data) {
|
|
207
|
+
return {
|
|
208
|
+
name: text(data.name),
|
|
209
|
+
description: text(data.description),
|
|
210
|
+
parameters: isData(data.parameters)
|
|
211
|
+
? data.parameters
|
|
212
|
+
: { ...NO_HOST_PARAMETERS },
|
|
213
|
+
does: texts(data.does),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
195
216
|
function parseConnection(data) {
|
|
196
217
|
return {
|
|
197
218
|
server: text(data.server),
|
|
@@ -242,23 +263,66 @@ function parseMode(data) {
|
|
|
242
263
|
}
|
|
243
264
|
return mode;
|
|
244
265
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
type: oneOf(data.type, ['select', 'text', 'toggle', 'slider', 'number'], 'text'),
|
|
266
|
+
/** A starter, its category said only when it has one (LOOP P-20). */
|
|
267
|
+
function parseStarter(data) {
|
|
268
|
+
const starter = {
|
|
249
269
|
label: text(data.label),
|
|
250
|
-
|
|
270
|
+
message: text(data.message),
|
|
251
271
|
};
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
if (typeof data.min === 'number') {
|
|
256
|
-
setting.min = data.min;
|
|
272
|
+
if (typeof data.category === 'string' && data.category) {
|
|
273
|
+
starter.category = data.category;
|
|
257
274
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
275
|
+
return starter;
|
|
276
|
+
}
|
|
277
|
+
/** One of several assistants in one application (LOOP P-20). */
|
|
278
|
+
function parseProfile(data) {
|
|
279
|
+
return {
|
|
280
|
+
id: text(data.id),
|
|
281
|
+
label: text(data.label),
|
|
282
|
+
description: text(data.description),
|
|
283
|
+
instructions: text(data.instructions),
|
|
284
|
+
model: typeof data.model === 'string' && data.model ? data.model : null,
|
|
285
|
+
starters: records(data.starters).map(parseStarter),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/** Words by key, what is not a word left out. */
|
|
289
|
+
function wordsBy(value) {
|
|
290
|
+
return Object.fromEntries(Object.entries(isData(value) ? value : {}).filter((entry) => typeof entry[1] === 'string'));
|
|
291
|
+
}
|
|
292
|
+
/** Records by key, what is not one left out. */
|
|
293
|
+
function recordsBy(value) {
|
|
294
|
+
return Object.fromEntries(Object.entries(isData(value) ? value : {}).filter((entry) => isData(entry[1])));
|
|
295
|
+
}
|
|
296
|
+
const mapValues = (value, map) => Object.fromEntries(Object.entries(value).map(([key, item]) => [key, map(item)]));
|
|
297
|
+
/** An application's words in one other language (LOOP P-26), every key present. */
|
|
298
|
+
function parseTranslation(data) {
|
|
299
|
+
return {
|
|
300
|
+
name: text(data.name),
|
|
301
|
+
description: text(data.description),
|
|
302
|
+
welcome: text(data.welcome),
|
|
303
|
+
starters: mapValues(recordsBy(data.starters), starter => ({
|
|
304
|
+
label: text(starter.label),
|
|
305
|
+
message: text(starter.message),
|
|
306
|
+
})),
|
|
307
|
+
categories: wordsBy(data.categories),
|
|
308
|
+
settings: mapValues(recordsBy(data.settings), field => ({
|
|
309
|
+
title: text(field.title),
|
|
310
|
+
description: text(field.description),
|
|
311
|
+
options: wordsBy(field.options),
|
|
312
|
+
})),
|
|
313
|
+
commands: wordsBy(data.commands),
|
|
314
|
+
modes: mapValues(recordsBy(data.modes), mode => ({
|
|
315
|
+
label: text(mode.label),
|
|
316
|
+
options: mapValues(recordsBy(mode.options), option => ({
|
|
317
|
+
label: text(option.label),
|
|
318
|
+
description: text(option.description),
|
|
319
|
+
})),
|
|
320
|
+
})),
|
|
321
|
+
profiles: mapValues(recordsBy(data.profiles), profile => ({
|
|
322
|
+
label: text(profile.label),
|
|
323
|
+
description: text(profile.description),
|
|
324
|
+
})),
|
|
325
|
+
};
|
|
262
326
|
}
|
|
263
327
|
function parseSurface(data) {
|
|
264
328
|
return {
|
|
@@ -289,22 +353,45 @@ function parseVoice(data) {
|
|
|
289
353
|
where: oneOf(voice.where, ['auto', 'device', 'server'], DEFAULT_VOICE.where),
|
|
290
354
|
};
|
|
291
355
|
}
|
|
356
|
+
/** What a person may send without being asked (P-21), as the spec says it. */
|
|
357
|
+
function parseUploads(data) {
|
|
358
|
+
return {
|
|
359
|
+
kinds: records(data.kinds).map(kind => ({
|
|
360
|
+
type: text(kind.type),
|
|
361
|
+
maxMb: numberOf(kind.max_mb, DEFAULT_UPLOAD_MB),
|
|
362
|
+
})),
|
|
363
|
+
maxFiles: numberOf(data.max_files, DEFAULT_MAX_FILES),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
292
366
|
function parseInterface(data, kind) {
|
|
293
367
|
const parsed = {
|
|
294
368
|
layout: oneOf(data.layout, ['chat', 'page', 'split'], DEFAULT_LAYOUTS[kind]),
|
|
295
369
|
accent: oneOf(data.accent, APP_ACCENTS, 'green'),
|
|
296
370
|
welcome: text(data.welcome),
|
|
297
|
-
starters: records(data.starters).map(
|
|
298
|
-
label: text(starter.label),
|
|
299
|
-
message: text(starter.message),
|
|
300
|
-
})),
|
|
371
|
+
starters: records(data.starters).map(parseStarter),
|
|
301
372
|
commands: records(data.commands).map(parseCommand),
|
|
302
373
|
modes: records(data.modes).map(parseMode),
|
|
303
|
-
|
|
374
|
+
profiles: records(data.profiles).map(parseProfile),
|
|
375
|
+
language: text(data.language, DEFAULT_LANGUAGE),
|
|
376
|
+
translations: mapValues(recordsBy(data.translations), parseTranslation),
|
|
304
377
|
components: texts(data.components),
|
|
305
378
|
voice: parseVoice(data.voice),
|
|
306
379
|
outputs: texts(data.outputs),
|
|
307
380
|
};
|
|
381
|
+
// Its settings are the JSON Schema of a form (C-16), kept as written: what
|
|
382
|
+
// is not one — the list of settings of earlier versions among it — is not
|
|
383
|
+
// read, and is said by the checks.
|
|
384
|
+
if (isData(data.settings)) {
|
|
385
|
+
parsed.settings = JSON.parse(JSON.stringify(data.settings));
|
|
386
|
+
}
|
|
387
|
+
// How its settings' fields are drawn (P-20), kept as written: a wrong
|
|
388
|
+
// widget is said by the checks.
|
|
389
|
+
if (isData(data.settings_ui)) {
|
|
390
|
+
parsed.settingsUi = JSON.parse(JSON.stringify(data.settings_ui));
|
|
391
|
+
}
|
|
392
|
+
if (isData(data.uploads)) {
|
|
393
|
+
parsed.uploads = parseUploads(data.uploads);
|
|
394
|
+
}
|
|
308
395
|
if (isData(data.surface)) {
|
|
309
396
|
parsed.surface = parseSurface(data.surface);
|
|
310
397
|
}
|
|
@@ -428,6 +515,12 @@ export function parseAppspec(document) {
|
|
|
428
515
|
const verified = isData(tests.verified) ? tests.verified : {};
|
|
429
516
|
const record = isData(data.record) ? data.record : {};
|
|
430
517
|
const checks = isData(data.checks) ? data.checks : {};
|
|
518
|
+
// A check of its code runs on an answer or a tool call (LOOP P-06).
|
|
519
|
+
for (const check of records(checks.code)) {
|
|
520
|
+
if (!CHECK_STAGES.includes(check.on)) {
|
|
521
|
+
problems.push(`The check \`${text(check.name)}\` of its code runs on answer or tool_call, not \`${String(check.on)}\`.`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
431
524
|
const keepFor = text(record.keep_for, DEFAULT_KEEP_FOR);
|
|
432
525
|
if (retentionDays(keepFor) === 0) {
|
|
433
526
|
problems.push(`Cannot read the retention \`${keepFor}\`: write 90_days, 18_months or 1_years.`);
|
|
@@ -447,6 +540,7 @@ export function parseAppspec(document) {
|
|
|
447
540
|
model: text(data.model),
|
|
448
541
|
skills: texts(data.skills),
|
|
449
542
|
backendTools: texts(data.backend_tools),
|
|
543
|
+
tools: records(data.tools).map(parseTool),
|
|
450
544
|
context: texts(data.context),
|
|
451
545
|
contents: texts(data.contents),
|
|
452
546
|
connections: records(data.connections).map(parseConnection),
|
|
@@ -469,6 +563,8 @@ export function parseAppspec(document) {
|
|
|
469
563
|
cases: records(tests.cases).map(testCase => ({
|
|
470
564
|
ask: text(testCase.ask),
|
|
471
565
|
expect: text(testCase.expect),
|
|
566
|
+
// Decided by its code (LOOP P-06), when it names the function.
|
|
567
|
+
...(text(testCase.code) ? { code: text(testCase.code) } : {}),
|
|
472
568
|
})),
|
|
473
569
|
verified: {
|
|
474
570
|
live: texts(verified.live),
|
|
@@ -488,6 +584,11 @@ export function parseAppspec(document) {
|
|
|
488
584
|
guards: texts(checks.guards),
|
|
489
585
|
gates: texts(checks.gates),
|
|
490
586
|
track: text(checks.track),
|
|
587
|
+
code: records(checks.code).map(check => ({
|
|
588
|
+
name: text(check.name),
|
|
589
|
+
on: oneOf(check.on, CHECK_STAGES, 'answer'),
|
|
590
|
+
description: text(check.description),
|
|
591
|
+
})),
|
|
491
592
|
},
|
|
492
593
|
deployment: parseDeployment(isData(data.deployment) ? data.deployment : {}),
|
|
493
594
|
goal: text(data.goal),
|
|
@@ -495,6 +596,7 @@ export function parseAppspec(document) {
|
|
|
495
596
|
memory: text(data.memory),
|
|
496
597
|
notifications: texts(data.notifications),
|
|
497
598
|
enabled: flag(data.enabled, true),
|
|
599
|
+
unavailable_because: text(data.unavailable_because),
|
|
498
600
|
tags: texts(data.tags),
|
|
499
601
|
icon: text(data.icon, DEFAULT_ICON),
|
|
500
602
|
emoji: text(data.emoji, DEFAULT_EMOJI) || DEFAULT_EMOJI,
|
|
@@ -568,16 +670,44 @@ function dumpSurface(surface) {
|
|
|
568
670
|
.text('composed_by', surface.composedBy)
|
|
569
671
|
.text('composed_at', surface.composedAt).data;
|
|
570
672
|
}
|
|
673
|
+
function dumpStarter(starter) {
|
|
674
|
+
return new Writer()
|
|
675
|
+
.text('label', starter.label, '\u0000')
|
|
676
|
+
.text('message', starter.message, '\u0000')
|
|
677
|
+
.text('category', starter.category ?? '').data;
|
|
678
|
+
}
|
|
679
|
+
/** Words by key, what is empty left out. */
|
|
680
|
+
const saidWords = (words) => Object.fromEntries(Object.entries(words).filter(([, said]) => said !== ''));
|
|
681
|
+
/** One translation as it is written (P-26): only what it says. */
|
|
682
|
+
function dumpTranslation(words) {
|
|
683
|
+
const parts = (value, dump) => Object.fromEntries(Object.entries(value).map(([key, item]) => [key, dump(item)]));
|
|
684
|
+
return new Writer()
|
|
685
|
+
.text('name', words.name)
|
|
686
|
+
.text('description', words.description)
|
|
687
|
+
.text('welcome', words.welcome)
|
|
688
|
+
.part('starters', parts(words.starters, starter => new Writer()
|
|
689
|
+
.text('label', starter.label)
|
|
690
|
+
.text('message', starter.message).data))
|
|
691
|
+
.part('categories', saidWords(words.categories))
|
|
692
|
+
.part('settings', parts(words.settings, field => new Writer()
|
|
693
|
+
.text('title', field.title)
|
|
694
|
+
.text('description', field.description)
|
|
695
|
+
.part('options', saidWords(field.options)).data))
|
|
696
|
+
.part('commands', saidWords(words.commands))
|
|
697
|
+
.part('modes', parts(words.modes, mode => new Writer().text('label', mode.label).part('options', parts(mode.options, option => new Writer()
|
|
698
|
+
.text('label', option.label)
|
|
699
|
+
.text('description', option.description).data)).data))
|
|
700
|
+
.part('profiles', parts(words.profiles, profile => new Writer()
|
|
701
|
+
.text('label', profile.label)
|
|
702
|
+
.text('description', profile.description).data)).data;
|
|
703
|
+
}
|
|
571
704
|
function dumpInterface(spec, kind) {
|
|
572
705
|
const writer = new Writer()
|
|
573
706
|
// A layout is written when it is not the one its kind starts with.
|
|
574
707
|
.value('layout', spec.layout, DEFAULT_LAYOUTS[kind])
|
|
575
708
|
.value('accent', spec.accent, 'green')
|
|
576
709
|
.text('welcome', spec.welcome)
|
|
577
|
-
.list('starters', spec.starters.map(
|
|
578
|
-
label: starter.label,
|
|
579
|
-
message: starter.message,
|
|
580
|
-
})))
|
|
710
|
+
.list('starters', spec.starters.map(dumpStarter))
|
|
581
711
|
.list('commands', spec.commands.map(command => ({
|
|
582
712
|
name: command.name,
|
|
583
713
|
description: command.description,
|
|
@@ -598,20 +728,30 @@ function dumpInterface(spec, kind) {
|
|
|
598
728
|
}
|
|
599
729
|
return written.data;
|
|
600
730
|
}))
|
|
601
|
-
.list('
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
.
|
|
731
|
+
.list('profiles', (spec.profiles ?? []).map(profile => new Writer()
|
|
732
|
+
.text('id', profile.id, '\u0000')
|
|
733
|
+
.text('label', profile.label, '\u0000')
|
|
734
|
+
.text('description', profile.description)
|
|
735
|
+
.text('instructions', profile.instructions)
|
|
736
|
+
.text('model', profile.model ?? '')
|
|
737
|
+
.list('starters', profile.starters.map(dumpStarter)).data))
|
|
738
|
+
.part('settings', spec.settings ? { ...spec.settings } : {});
|
|
739
|
+
// How its settings' fields are drawn (P-20): written when said.
|
|
740
|
+
if (spec.settingsUi) {
|
|
741
|
+
writer.data.settings_ui = { ...spec.settingsUi };
|
|
742
|
+
}
|
|
743
|
+
writer
|
|
744
|
+
.text('language', spec.language ?? DEFAULT_LANGUAGE, DEFAULT_LANGUAGE)
|
|
745
|
+
.part('translations', mapValues(spec.translations ?? {}, dumpTranslation));
|
|
746
|
+
// What a person may send without being asked (P-21): written when said.
|
|
747
|
+
if (spec.uploads) {
|
|
748
|
+
writer.data.uploads = new Writer()
|
|
749
|
+
.list('kinds', spec.uploads.kinds.map(kind => new Writer()
|
|
750
|
+
.text('type', kind.type, '\u0000')
|
|
751
|
+
.value('max_mb', kind.maxMb, DEFAULT_UPLOAD_MB).data))
|
|
752
|
+
.value('max_files', spec.uploads.maxFiles, DEFAULT_MAX_FILES).data;
|
|
753
|
+
}
|
|
754
|
+
writer.list('components', spec.components);
|
|
615
755
|
if (spec.surface) {
|
|
616
756
|
writer.data.surface = dumpSurface(spec.surface);
|
|
617
757
|
}
|
|
@@ -687,6 +827,16 @@ export function dumpAppspec(app) {
|
|
|
687
827
|
.text('model', app.model)
|
|
688
828
|
.list('skills', app.skills)
|
|
689
829
|
.list('backend_tools', app.backendTools)
|
|
830
|
+
.list('tools', (app.tools ?? []).map(tool => ({
|
|
831
|
+
name: tool.name,
|
|
832
|
+
description: tool.description,
|
|
833
|
+
// No arguments is what it is unless said.
|
|
834
|
+
...(JSON.stringify(tool.parameters) ===
|
|
835
|
+
JSON.stringify(NO_HOST_PARAMETERS)
|
|
836
|
+
? {}
|
|
837
|
+
: { parameters: tool.parameters }),
|
|
838
|
+
does: tool.does,
|
|
839
|
+
})))
|
|
690
840
|
.list('context', app.context)
|
|
691
841
|
.list('contents', app.contents)
|
|
692
842
|
.list('connections', app.connections.map(connection => new Writer()
|
|
@@ -715,6 +865,7 @@ export function dumpAppspec(app) {
|
|
|
715
865
|
.list('cases', app.tests.cases.map(testCase => ({
|
|
716
866
|
ask: testCase.ask,
|
|
717
867
|
expect: testCase.expect,
|
|
868
|
+
...(testCase.code ? { code: testCase.code } : {}),
|
|
718
869
|
})))
|
|
719
870
|
.part('verified', new Writer()
|
|
720
871
|
.list('live', app.tests.verified.live)
|
|
@@ -727,7 +878,12 @@ export function dumpAppspec(app) {
|
|
|
727
878
|
.part('checks', new Writer()
|
|
728
879
|
.list('guards', app.checks.guards)
|
|
729
880
|
.list('gates', app.checks.gates)
|
|
730
|
-
.text('track', app.checks.track)
|
|
881
|
+
.text('track', app.checks.track)
|
|
882
|
+
.list('code', (app.checks.code ?? []).map(check => ({
|
|
883
|
+
name: check.name,
|
|
884
|
+
on: check.on,
|
|
885
|
+
description: check.description,
|
|
886
|
+
}))).data)
|
|
731
887
|
.part('deployment', new Writer()
|
|
732
888
|
.present('hosted', app.deployment.hosted &&
|
|
733
889
|
new Writer()
|
|
@@ -765,6 +921,7 @@ export function dumpAppspec(app) {
|
|
|
765
921
|
}
|
|
766
922
|
writer
|
|
767
923
|
.value('enabled', app.enabled, true)
|
|
924
|
+
.text('unavailable_because', app.unavailable_because)
|
|
768
925
|
.list('tags', app.tags)
|
|
769
926
|
.text('icon', app.icon ?? DEFAULT_ICON, DEFAULT_ICON)
|
|
770
927
|
.text('emoji', app.emoji, DEFAULT_EMOJI)
|
|
@@ -14,6 +14,12 @@ export interface AppCheck {
|
|
|
14
14
|
* UI plugins, by the name a surface gives it.
|
|
15
15
|
*/
|
|
16
16
|
export declare const componentNamed: (name: string) => ComponentSpec | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* What its code declares that agentspecs refuses (LOOP P-06): its tools,
|
|
19
|
+
* its checks and the tests it decides, each by a Python name, each name
|
|
20
|
+
* once; a tool says what it does, by class, each class once.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ownCodeProblems(app: AppSpec): string[];
|
|
17
23
|
/** How an organization's own context is named: never a catalogue id (agentspecs' `ORGANIZATION_FRAME_PREFIX`). */
|
|
18
24
|
/**
|
|
19
25
|
* What stops a Form block from asking (LOOP C-16), in agentspecs'
|
package/lib/loop/apps/checks.js
CHANGED
|
@@ -43,6 +43,9 @@ import { APP_THEME_MODES, APP_THEME_VARIANTS, isAssistantCharacterId, parseAppsp
|
|
|
43
43
|
import { classesOf, splitRef, toolBehaviours } from './rules';
|
|
44
44
|
import { COMMAND_INPUT, COMMAND_NAME, MODE_ID } from './composer';
|
|
45
45
|
import { HOST_NAME, hostToolsOf } from './hostTools';
|
|
46
|
+
import { MAX_UPLOAD_MB, UPLOAD_KIND } from './uploads';
|
|
47
|
+
import { formUiProblems } from './settingsInputs';
|
|
48
|
+
import { translationProblems } from './language';
|
|
46
49
|
export const NOT_READY = 'Not ready';
|
|
47
50
|
export const NEEDS_ATTENTION = 'Needs attention';
|
|
48
51
|
export const PASSES = 'Passes the instant checks';
|
|
@@ -80,6 +83,54 @@ const matches = (name, pattern) => new RegExp('^' +
|
|
|
80
83
|
? '[\\s\\S]'
|
|
81
84
|
: c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('') +
|
|
82
85
|
'$').test(name);
|
|
86
|
+
/** The name of a function of an application's code (agentspecs' `CODE_NAME`). */
|
|
87
|
+
const CODE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
88
|
+
/**
|
|
89
|
+
* What its code declares that agentspecs refuses (LOOP P-06): its tools,
|
|
90
|
+
* its checks and the tests it decides, each by a Python name, each name
|
|
91
|
+
* once; a tool says what it does, by class, each class once.
|
|
92
|
+
*/
|
|
93
|
+
export function ownCodeProblems(app) {
|
|
94
|
+
const problems = [];
|
|
95
|
+
const tools = app.tools ?? [];
|
|
96
|
+
for (const tool of tools) {
|
|
97
|
+
if (!CODE_NAME.test(tool.name)) {
|
|
98
|
+
problems.push(`Cannot use “${tool.name}” as a tool's name: letters, digits and \`_\`, as in Python.`);
|
|
99
|
+
}
|
|
100
|
+
if (!tool.description.trim()) {
|
|
101
|
+
problems.push(`The tool “${tool.name}” says what it does, for the agent.`);
|
|
102
|
+
}
|
|
103
|
+
if (tool.does.length === 0) {
|
|
104
|
+
problems.push(`The tool “${tool.name}” says what it does: read, write, send, buy, delete or publish.`);
|
|
105
|
+
}
|
|
106
|
+
for (const action of tool.does) {
|
|
107
|
+
if (!CLASS_NAMES.has(action)) {
|
|
108
|
+
problems.push(`The tool “${tool.name}” does “${action}”, which is no class of action.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (new Set(tool.does).size !== tool.does.length) {
|
|
112
|
+
problems.push('A tool says each thing it does once.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const names = tools.map(tool => tool.name);
|
|
116
|
+
if (new Set(names).size !== names.length) {
|
|
117
|
+
problems.push('Two of its tools have the same name.');
|
|
118
|
+
}
|
|
119
|
+
const checks = (app.checks.code ?? []).map(check => check.name);
|
|
120
|
+
for (const name of checks) {
|
|
121
|
+
if (!CODE_NAME.test(name)) {
|
|
122
|
+
problems.push(`Cannot use “${name}” as a check's name: letters, digits and \`_\`, as in Python.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (new Set(checks).size !== checks.length) {
|
|
126
|
+
problems.push('Two checks of its code have the same name.');
|
|
127
|
+
}
|
|
128
|
+
const decided = app.tests.cases.flatMap(testCase => testCase.code ? [testCase.code] : []);
|
|
129
|
+
if (new Set(decided).size !== decided.length) {
|
|
130
|
+
problems.push('Two tests are decided by the same function of its code.');
|
|
131
|
+
}
|
|
132
|
+
return problems;
|
|
133
|
+
}
|
|
83
134
|
/** What the spec says of itself that the tolerant reader lets through. */
|
|
84
135
|
function shapeProblems(app) {
|
|
85
136
|
const problems = [];
|
|
@@ -95,6 +146,14 @@ function shapeProblems(app) {
|
|
|
95
146
|
if (Boolean(app.agent) === Boolean(app.team)) {
|
|
96
147
|
problems.push('An application names who does the work: an `agent`, or a `team`, and not both.');
|
|
97
148
|
}
|
|
149
|
+
// agentspecs refuses either way round (LOOP E-01): off says why, on says nothing.
|
|
150
|
+
const because = (app.unavailable_because ?? '').trim();
|
|
151
|
+
if (!app.enabled && !because) {
|
|
152
|
+
problems.push('An application that is not offered says why, under `unavailable_because`.');
|
|
153
|
+
}
|
|
154
|
+
if (app.enabled && because) {
|
|
155
|
+
problems.push('An application offered today is available: remove `unavailable_because`, or set `enabled: false`.');
|
|
156
|
+
}
|
|
98
157
|
if (app.kind === 'decision' && !app.decision) {
|
|
99
158
|
problems.push('A decision application says what it decides, under `decision`.');
|
|
100
159
|
}
|
|
@@ -112,6 +171,36 @@ function shapeProblems(app) {
|
|
|
112
171
|
else if (app.triggers.length > 0) {
|
|
113
172
|
problems.push(`A ${app.kind} application starts when somebody opens it: \`triggers\` are a worker's.`);
|
|
114
173
|
}
|
|
174
|
+
// Its profiles (LOOP P-20): two at least, or none; each id slash-safe and
|
|
175
|
+
// said once, each with a label.
|
|
176
|
+
const profiles = app.interface.profiles ?? [];
|
|
177
|
+
if (profiles.length === 1) {
|
|
178
|
+
problems.push('One profile is the application itself: say two at least, or put its instructions, model and starters on the application.');
|
|
179
|
+
}
|
|
180
|
+
const profileIds = new Set();
|
|
181
|
+
for (const profile of profiles) {
|
|
182
|
+
if (!MODE_ID.test(profile.id)) {
|
|
183
|
+
problems.push(`Cannot use “${profile.id}” as a profile's id: lower-case letters, digits, \`_\` and \`-\`, a letter first.`);
|
|
184
|
+
}
|
|
185
|
+
if (!profile.label.trim()) {
|
|
186
|
+
problems.push(`The profile “${profile.id}” has no label.`);
|
|
187
|
+
}
|
|
188
|
+
if (profileIds.has(profile.id)) {
|
|
189
|
+
problems.push(`Two profiles are “${profile.id}”.`);
|
|
190
|
+
}
|
|
191
|
+
profileIds.add(profile.id);
|
|
192
|
+
}
|
|
193
|
+
// How its settings' fields are drawn (P-20): by a widget that draws each.
|
|
194
|
+
if (app.interface.settingsUi) {
|
|
195
|
+
if (!app.interface.settings) {
|
|
196
|
+
problems.push('`settings_ui` draws the settings’ fields: say `settings` first.');
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
problems.push(...formUiProblems('The form “settings”', app.interface.settings, app.interface.settingsUi));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Its words in other languages (P-26): of languages, of what it says.
|
|
203
|
+
problems.push(...translationProblems(app.interface));
|
|
115
204
|
// What the host page passes and offers (D-10): named once each, in words
|
|
116
205
|
// its agent's tools can carry, each function saying what it does.
|
|
117
206
|
const host = app.deployment.embedded?.host;
|
|
@@ -146,6 +235,8 @@ function shapeProblems(app) {
|
|
|
146
235
|
}
|
|
147
236
|
}
|
|
148
237
|
}
|
|
238
|
+
// What its code declares (LOOP P-06), as agentspecs refuses it.
|
|
239
|
+
problems.push(...ownCodeProblems(app));
|
|
149
240
|
const servers = app.connections.map(connection => idOf(connection.server));
|
|
150
241
|
if (new Set(servers).size !== servers.length) {
|
|
151
242
|
problems.push('The application connects to the same server twice.');
|
|
@@ -204,6 +295,10 @@ export function formProblems(node) {
|
|
|
204
295
|
if (missing.length > 0) {
|
|
205
296
|
problems.push(`${said} requires ${missing.map(name => `“${String(name)}”`).join(', ')}, which it does not ask.`);
|
|
206
297
|
}
|
|
298
|
+
// How its fields are drawn (P-20): by a widget that draws each.
|
|
299
|
+
if (node.ui !== undefined && node.ui !== null) {
|
|
300
|
+
problems.push(...formUiProblems(said, schema, node.ui));
|
|
301
|
+
}
|
|
207
302
|
return problems;
|
|
208
303
|
}
|
|
209
304
|
const isPlainRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -270,6 +365,12 @@ function referenceProblems(app, organizationFrames) {
|
|
|
270
365
|
if (app.model && !getModel(app.model)) {
|
|
271
366
|
missing('model', app.model);
|
|
272
367
|
}
|
|
368
|
+
// The model a profile runs on is the catalogue's (LOOP P-20).
|
|
369
|
+
for (const profile of app.interface.profiles ?? []) {
|
|
370
|
+
if (profile.model && !getModel(profile.model)) {
|
|
371
|
+
problems.push(`The profile “${profile.id}” runs on “${profile.model}”, which is no model.`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
273
374
|
// The model a mode runs on is the catalogue's (LOOP P-19).
|
|
274
375
|
for (const mode of app.interface.modes ?? []) {
|
|
275
376
|
for (const option of mode.options) {
|
|
@@ -328,6 +429,9 @@ function referenceProblems(app, organizationFrames) {
|
|
|
328
429
|
if (server === undefined) {
|
|
329
430
|
if (hostTools.includes(target))
|
|
330
431
|
continue;
|
|
432
|
+
// A tool of its own, written in its code (LOOP P-06).
|
|
433
|
+
if ((app.tools ?? []).some(tool => tool.name === target))
|
|
434
|
+
continue;
|
|
331
435
|
if (!getBackendToolSpec(name)) {
|
|
332
436
|
problems.push(`The rule “${rule.action}” names the tool “${target}”, which the catalogue does not have.`);
|
|
333
437
|
}
|
|
@@ -472,7 +576,6 @@ const ENUMS = {
|
|
|
472
576
|
behaviour: ['do_it', 'if_asked', 'ask_first', 'leave_to_me'],
|
|
473
577
|
layout: ['chat', 'page', 'split'],
|
|
474
578
|
accent: ['green', 'rose', 'sky', 'lime', 'sun', 'violet'],
|
|
475
|
-
settingType: ['select', 'text', 'toggle', 'slider', 'number'],
|
|
476
579
|
record: [
|
|
477
580
|
'conversations',
|
|
478
581
|
'actions',
|
|
@@ -639,9 +742,44 @@ export function documentShapeProblems(document) {
|
|
|
639
742
|
}
|
|
640
743
|
text(ui.welcome, 'interface.welcome');
|
|
641
744
|
texts(ui.components, 'interface.components');
|
|
642
|
-
|
|
745
|
+
const starter = (item, where) => {
|
|
643
746
|
required(item, 'label', where);
|
|
644
747
|
required(item, 'message', where);
|
|
748
|
+
text(item.category, `${where}.category`);
|
|
749
|
+
unknownKeys(item, ['label', 'message', 'category'], where, 'a starter');
|
|
750
|
+
};
|
|
751
|
+
records(ui.starters, 'interface.starters', starter);
|
|
752
|
+
// Its profiles (LOOP P-20), as agentspecs refuses them: no key unknown.
|
|
753
|
+
records(ui.profiles, 'interface.profiles', (profile, where) => {
|
|
754
|
+
required(profile, 'id', where);
|
|
755
|
+
required(profile, 'label', where);
|
|
756
|
+
for (const key of [
|
|
757
|
+
'id',
|
|
758
|
+
'label',
|
|
759
|
+
'description',
|
|
760
|
+
'instructions',
|
|
761
|
+
'model',
|
|
762
|
+
]) {
|
|
763
|
+
text(profile[key], `${where}.${key}`);
|
|
764
|
+
}
|
|
765
|
+
unknownKeys(profile, ['id', 'label', 'description', 'instructions', 'model', 'starters'], where, 'a profile');
|
|
766
|
+
records(profile.starters, `${where}.starters`, starter);
|
|
767
|
+
});
|
|
768
|
+
text(ui.language, 'interface.language');
|
|
769
|
+
mapping(ui.translations, 'interface.translations', translations => {
|
|
770
|
+
for (const [tag, words] of Object.entries(translations)) {
|
|
771
|
+
mapping(words, `interface.translations.${tag}`, said => unknownKeys(said, [
|
|
772
|
+
'name',
|
|
773
|
+
'description',
|
|
774
|
+
'welcome',
|
|
775
|
+
'starters',
|
|
776
|
+
'categories',
|
|
777
|
+
'settings',
|
|
778
|
+
'commands',
|
|
779
|
+
'modes',
|
|
780
|
+
'profiles',
|
|
781
|
+
], `interface.translations.${tag}`, 'a translation'));
|
|
782
|
+
}
|
|
645
783
|
});
|
|
646
784
|
// Its commands and modes in the composer (LOOP P-19), as agentspecs
|
|
647
785
|
// refuses them: names and ids slash-safe and said once, no key unknown.
|
|
@@ -721,12 +859,53 @@ export function documentShapeProblems(document) {
|
|
|
721
859
|
if (choosingModel.length > 1) {
|
|
722
860
|
at('interface.modes', `let only one mode choose the model, not ${choosingModel.join(', ')}`);
|
|
723
861
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
862
|
+
// Its settings are the JSON Schema of a form (C-16), checked as a Form's
|
|
863
|
+
// schema is; the list of settings of earlier versions is refused.
|
|
864
|
+
if (Array.isArray(ui.settings)) {
|
|
865
|
+
at('interface.settings', 'is the JSON Schema of a form, an object of named fields (LOOP C-16), not a list of settings: say each one as a property, its title and its default');
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
mapping(ui.settings, 'interface.settings', settings => {
|
|
869
|
+
problems.push(...formProblems({ id: 'settings', schema: settings }));
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
// What a person may send without being asked (LOOP P-21), as agentspecs
|
|
873
|
+
// refuses it: a kind at least, each a type said once, sizes it takes.
|
|
874
|
+
mapping(ui.uploads, 'interface.uploads', uploads => {
|
|
875
|
+
unknownKeys(uploads, ['kinds', 'max_files'], 'interface.uploads', 'uploads');
|
|
876
|
+
required(uploads, 'kinds', 'interface.uploads');
|
|
877
|
+
const types = new Set();
|
|
878
|
+
records(uploads.kinds, 'interface.uploads.kinds', (kind, where) => {
|
|
879
|
+
required(kind, 'type', where);
|
|
880
|
+
unknownKeys(kind, ['type', 'max_mb'], where, 'a kind of upload');
|
|
881
|
+
if (kind.type !== undefined) {
|
|
882
|
+
if (typeof kind.type !== 'string' || !UPLOAD_KIND.test(kind.type)) {
|
|
883
|
+
at(`${where}.type`, 'is a media type (application/pdf), a family (image/*) or an extension (.csv), lowercase');
|
|
884
|
+
}
|
|
885
|
+
else if (types.has(kind.type)) {
|
|
886
|
+
at(`${where}.type`, `names ${kind.type} a second time`);
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
types.add(kind.type);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const size = kind.max_mb;
|
|
893
|
+
if (size !== undefined &&
|
|
894
|
+
(typeof size !== 'number' || size <= 0 || size > MAX_UPLOAD_MB)) {
|
|
895
|
+
at(`${where}.max_mb`, `is more than 0 and ${MAX_UPLOAD_MB} at most`);
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
if (Array.isArray(uploads.kinds) && uploads.kinds.length === 0) {
|
|
899
|
+
at('interface.uploads.kinds', 'are one at least');
|
|
900
|
+
}
|
|
901
|
+
const most = uploads.max_files;
|
|
902
|
+
if (most !== undefined &&
|
|
903
|
+
(typeof most !== 'number' ||
|
|
904
|
+
!Number.isInteger(most) ||
|
|
905
|
+
most < 1 ||
|
|
906
|
+
most > 20)) {
|
|
907
|
+
at('interface.uploads.max_files', 'is a whole number from 1 to 20');
|
|
908
|
+
}
|
|
730
909
|
});
|
|
731
910
|
mapping(ui.surface, 'interface.surface', surface => {
|
|
732
911
|
records(surface.components, 'interface.surface.components', (item, where) => {
|