@datalayer/agent-runtimes 1.3.83 → 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 +9 -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 +190 -11
- package/lib/loop/apps/checks.d.ts +6 -0
- package/lib/loop/apps/checks.js +170 -1
- 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.js +2 -0
- 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/apps.js +149 -0
- package/lib/specs/appspecSchema.js +439 -2
- package/lib/specs/uiPlugins.js +6 -1
- package/lib/types/agentspecs.d.ts +130 -1
- package/package.json +1 -1
- package/scripts/codegen/generate_apps.py +16 -0
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,6 +127,9 @@ export function emptyAppspec(kind = 'chat') {
|
|
|
124
127
|
starters: [],
|
|
125
128
|
commands: [],
|
|
126
129
|
modes: [],
|
|
130
|
+
profiles: [],
|
|
131
|
+
language: DEFAULT_LANGUAGE,
|
|
132
|
+
translations: {},
|
|
127
133
|
components: [],
|
|
128
134
|
outputs: [],
|
|
129
135
|
},
|
|
@@ -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',
|
|
@@ -193,6 +200,19 @@ const KNOWN_KEYS = [
|
|
|
193
200
|
'avatar',
|
|
194
201
|
'banner',
|
|
195
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
|
+
}
|
|
196
216
|
function parseConnection(data) {
|
|
197
217
|
return {
|
|
198
218
|
server: text(data.server),
|
|
@@ -243,6 +263,67 @@ function parseMode(data) {
|
|
|
243
263
|
}
|
|
244
264
|
return mode;
|
|
245
265
|
}
|
|
266
|
+
/** A starter, its category said only when it has one (LOOP P-20). */
|
|
267
|
+
function parseStarter(data) {
|
|
268
|
+
const starter = {
|
|
269
|
+
label: text(data.label),
|
|
270
|
+
message: text(data.message),
|
|
271
|
+
};
|
|
272
|
+
if (typeof data.category === 'string' && data.category) {
|
|
273
|
+
starter.category = data.category;
|
|
274
|
+
}
|
|
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
|
+
};
|
|
326
|
+
}
|
|
246
327
|
function parseSurface(data) {
|
|
247
328
|
return {
|
|
248
329
|
protocol: text(data.protocol, DEFAULT_PROTOCOL),
|
|
@@ -272,17 +353,27 @@ function parseVoice(data) {
|
|
|
272
353
|
where: oneOf(voice.where, ['auto', 'device', 'server'], DEFAULT_VOICE.where),
|
|
273
354
|
};
|
|
274
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
|
+
}
|
|
275
366
|
function parseInterface(data, kind) {
|
|
276
367
|
const parsed = {
|
|
277
368
|
layout: oneOf(data.layout, ['chat', 'page', 'split'], DEFAULT_LAYOUTS[kind]),
|
|
278
369
|
accent: oneOf(data.accent, APP_ACCENTS, 'green'),
|
|
279
370
|
welcome: text(data.welcome),
|
|
280
|
-
starters: records(data.starters).map(
|
|
281
|
-
label: text(starter.label),
|
|
282
|
-
message: text(starter.message),
|
|
283
|
-
})),
|
|
371
|
+
starters: records(data.starters).map(parseStarter),
|
|
284
372
|
commands: records(data.commands).map(parseCommand),
|
|
285
373
|
modes: records(data.modes).map(parseMode),
|
|
374
|
+
profiles: records(data.profiles).map(parseProfile),
|
|
375
|
+
language: text(data.language, DEFAULT_LANGUAGE),
|
|
376
|
+
translations: mapValues(recordsBy(data.translations), parseTranslation),
|
|
286
377
|
components: texts(data.components),
|
|
287
378
|
voice: parseVoice(data.voice),
|
|
288
379
|
outputs: texts(data.outputs),
|
|
@@ -293,6 +384,14 @@ function parseInterface(data, kind) {
|
|
|
293
384
|
if (isData(data.settings)) {
|
|
294
385
|
parsed.settings = JSON.parse(JSON.stringify(data.settings));
|
|
295
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
|
+
}
|
|
296
395
|
if (isData(data.surface)) {
|
|
297
396
|
parsed.surface = parseSurface(data.surface);
|
|
298
397
|
}
|
|
@@ -416,6 +515,12 @@ export function parseAppspec(document) {
|
|
|
416
515
|
const verified = isData(tests.verified) ? tests.verified : {};
|
|
417
516
|
const record = isData(data.record) ? data.record : {};
|
|
418
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
|
+
}
|
|
419
524
|
const keepFor = text(record.keep_for, DEFAULT_KEEP_FOR);
|
|
420
525
|
if (retentionDays(keepFor) === 0) {
|
|
421
526
|
problems.push(`Cannot read the retention \`${keepFor}\`: write 90_days, 18_months or 1_years.`);
|
|
@@ -435,6 +540,7 @@ export function parseAppspec(document) {
|
|
|
435
540
|
model: text(data.model),
|
|
436
541
|
skills: texts(data.skills),
|
|
437
542
|
backendTools: texts(data.backend_tools),
|
|
543
|
+
tools: records(data.tools).map(parseTool),
|
|
438
544
|
context: texts(data.context),
|
|
439
545
|
contents: texts(data.contents),
|
|
440
546
|
connections: records(data.connections).map(parseConnection),
|
|
@@ -457,6 +563,8 @@ export function parseAppspec(document) {
|
|
|
457
563
|
cases: records(tests.cases).map(testCase => ({
|
|
458
564
|
ask: text(testCase.ask),
|
|
459
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) } : {}),
|
|
460
568
|
})),
|
|
461
569
|
verified: {
|
|
462
570
|
live: texts(verified.live),
|
|
@@ -476,6 +584,11 @@ export function parseAppspec(document) {
|
|
|
476
584
|
guards: texts(checks.guards),
|
|
477
585
|
gates: texts(checks.gates),
|
|
478
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
|
+
})),
|
|
479
592
|
},
|
|
480
593
|
deployment: parseDeployment(isData(data.deployment) ? data.deployment : {}),
|
|
481
594
|
goal: text(data.goal),
|
|
@@ -557,16 +670,44 @@ function dumpSurface(surface) {
|
|
|
557
670
|
.text('composed_by', surface.composedBy)
|
|
558
671
|
.text('composed_at', surface.composedAt).data;
|
|
559
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
|
+
}
|
|
560
704
|
function dumpInterface(spec, kind) {
|
|
561
705
|
const writer = new Writer()
|
|
562
706
|
// A layout is written when it is not the one its kind starts with.
|
|
563
707
|
.value('layout', spec.layout, DEFAULT_LAYOUTS[kind])
|
|
564
708
|
.value('accent', spec.accent, 'green')
|
|
565
709
|
.text('welcome', spec.welcome)
|
|
566
|
-
.list('starters', spec.starters.map(
|
|
567
|
-
label: starter.label,
|
|
568
|
-
message: starter.message,
|
|
569
|
-
})))
|
|
710
|
+
.list('starters', spec.starters.map(dumpStarter))
|
|
570
711
|
.list('commands', spec.commands.map(command => ({
|
|
571
712
|
name: command.name,
|
|
572
713
|
description: command.description,
|
|
@@ -587,8 +728,30 @@ function dumpInterface(spec, kind) {
|
|
|
587
728
|
}
|
|
588
729
|
return written.data;
|
|
589
730
|
}))
|
|
590
|
-
.
|
|
591
|
-
.
|
|
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);
|
|
592
755
|
if (spec.surface) {
|
|
593
756
|
writer.data.surface = dumpSurface(spec.surface);
|
|
594
757
|
}
|
|
@@ -664,6 +827,16 @@ export function dumpAppspec(app) {
|
|
|
664
827
|
.text('model', app.model)
|
|
665
828
|
.list('skills', app.skills)
|
|
666
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
|
+
})))
|
|
667
840
|
.list('context', app.context)
|
|
668
841
|
.list('contents', app.contents)
|
|
669
842
|
.list('connections', app.connections.map(connection => new Writer()
|
|
@@ -692,6 +865,7 @@ export function dumpAppspec(app) {
|
|
|
692
865
|
.list('cases', app.tests.cases.map(testCase => ({
|
|
693
866
|
ask: testCase.ask,
|
|
694
867
|
expect: testCase.expect,
|
|
868
|
+
...(testCase.code ? { code: testCase.code } : {}),
|
|
695
869
|
})))
|
|
696
870
|
.part('verified', new Writer()
|
|
697
871
|
.list('live', app.tests.verified.live)
|
|
@@ -704,7 +878,12 @@ export function dumpAppspec(app) {
|
|
|
704
878
|
.part('checks', new Writer()
|
|
705
879
|
.list('guards', app.checks.guards)
|
|
706
880
|
.list('gates', app.checks.gates)
|
|
707
|
-
.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)
|
|
708
887
|
.part('deployment', new Writer()
|
|
709
888
|
.present('hosted', app.deployment.hosted &&
|
|
710
889
|
new Writer()
|
|
@@ -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 = [];
|
|
@@ -120,6 +171,36 @@ function shapeProblems(app) {
|
|
|
120
171
|
else if (app.triggers.length > 0) {
|
|
121
172
|
problems.push(`A ${app.kind} application starts when somebody opens it: \`triggers\` are a worker's.`);
|
|
122
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));
|
|
123
204
|
// What the host page passes and offers (D-10): named once each, in words
|
|
124
205
|
// its agent's tools can carry, each function saying what it does.
|
|
125
206
|
const host = app.deployment.embedded?.host;
|
|
@@ -154,6 +235,8 @@ function shapeProblems(app) {
|
|
|
154
235
|
}
|
|
155
236
|
}
|
|
156
237
|
}
|
|
238
|
+
// What its code declares (LOOP P-06), as agentspecs refuses it.
|
|
239
|
+
problems.push(...ownCodeProblems(app));
|
|
157
240
|
const servers = app.connections.map(connection => idOf(connection.server));
|
|
158
241
|
if (new Set(servers).size !== servers.length) {
|
|
159
242
|
problems.push('The application connects to the same server twice.');
|
|
@@ -212,6 +295,10 @@ export function formProblems(node) {
|
|
|
212
295
|
if (missing.length > 0) {
|
|
213
296
|
problems.push(`${said} requires ${missing.map(name => `“${String(name)}”`).join(', ')}, which it does not ask.`);
|
|
214
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
|
+
}
|
|
215
302
|
return problems;
|
|
216
303
|
}
|
|
217
304
|
const isPlainRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -278,6 +365,12 @@ function referenceProblems(app, organizationFrames) {
|
|
|
278
365
|
if (app.model && !getModel(app.model)) {
|
|
279
366
|
missing('model', app.model);
|
|
280
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
|
+
}
|
|
281
374
|
// The model a mode runs on is the catalogue's (LOOP P-19).
|
|
282
375
|
for (const mode of app.interface.modes ?? []) {
|
|
283
376
|
for (const option of mode.options) {
|
|
@@ -336,6 +429,9 @@ function referenceProblems(app, organizationFrames) {
|
|
|
336
429
|
if (server === undefined) {
|
|
337
430
|
if (hostTools.includes(target))
|
|
338
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;
|
|
339
435
|
if (!getBackendToolSpec(name)) {
|
|
340
436
|
problems.push(`The rule “${rule.action}” names the tool “${target}”, which the catalogue does not have.`);
|
|
341
437
|
}
|
|
@@ -646,9 +742,44 @@ export function documentShapeProblems(document) {
|
|
|
646
742
|
}
|
|
647
743
|
text(ui.welcome, 'interface.welcome');
|
|
648
744
|
texts(ui.components, 'interface.components');
|
|
649
|
-
|
|
745
|
+
const starter = (item, where) => {
|
|
650
746
|
required(item, 'label', where);
|
|
651
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
|
+
}
|
|
652
783
|
});
|
|
653
784
|
// Its commands and modes in the composer (LOOP P-19), as agentspecs
|
|
654
785
|
// refuses them: names and ids slash-safe and said once, no key unknown.
|
|
@@ -738,6 +869,44 @@ export function documentShapeProblems(document) {
|
|
|
738
869
|
problems.push(...formProblems({ id: 'settings', schema: settings }));
|
|
739
870
|
});
|
|
740
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
|
+
}
|
|
909
|
+
});
|
|
741
910
|
mapping(ui.surface, 'interface.surface', surface => {
|
|
742
911
|
records(surface.components, 'interface.surface.components', (item, where) => {
|
|
743
912
|
required(item, 'id', where);
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Commands and
|
|
3
|
-
* sends,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Commands, modes and profiles in the composer (LOOP P-19, P-20), as rules:
|
|
3
|
+
* what a command sends, which option of each mode a run is in, which profile
|
|
4
|
+
* a conversation is with and what it offers. agentspecs' own
|
|
5
|
+
* (`command_prompt`, `AppInterface.mode_choice`, `mode_effect`,
|
|
6
|
+
* `profile_choice`, `starters_for`), said again for the page.
|
|
6
7
|
*
|
|
7
8
|
* Pure: no React, no network.
|
|
8
9
|
*
|
|
9
10
|
* @module loop/apps/composer
|
|
10
11
|
*/
|
|
11
|
-
import type { AppCommandSpec, AppModeEffect, AppSpec } from '../../types/agentspecs';
|
|
12
|
+
import type { AppCommandSpec, AppModeEffect, AppProfileSpec, AppSpec, AppStarterSpec } from '../../types/agentspecs';
|
|
12
13
|
/** Where a command's prompt takes the words typed after it. */
|
|
13
14
|
export declare const COMMAND_INPUT = "{input}";
|
|
14
15
|
/** What follows the slash: lower-case letters, digits and hyphens, a letter first. */
|
|
@@ -29,9 +30,19 @@ export declare function modeDefaults(app: Pick<AppSpec, 'interface'>): Record<st
|
|
|
29
30
|
*/
|
|
30
31
|
export declare function modeChoice(app: Pick<AppSpec, 'name' | 'interface'>, chosen?: Record<string, string>): Record<string, string>;
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* {@link modeChoice} refuses.
|
|
33
|
+
* The profile a conversation is with (LOOP P-20): the one chosen, else the
|
|
34
|
+
* first; undefined for an application without profiles. One it does not have
|
|
35
|
+
* is refused, in a sentence, as the runtime refuses it.
|
|
36
36
|
*/
|
|
37
|
-
export declare function
|
|
37
|
+
export declare function profileChoice(app: Pick<AppSpec, 'name' | 'interface'>, chosen?: string): AppProfileSpec | undefined;
|
|
38
|
+
/** The starters offered with a profile: its own, else the application's (P-20). */
|
|
39
|
+
export declare function startersFor(app: Pick<AppSpec, 'name' | 'interface'>, profile?: string): AppStarterSpec[];
|
|
40
|
+
/**
|
|
41
|
+
* What a run in the modes chosen, with the profile chosen, is told, and the
|
|
42
|
+
* model it runs on — agentspecs' `mode_effect`, and the runtime's
|
|
43
|
+
* `run_effect`: the profile's instructions first, then those of every option
|
|
44
|
+
* chosen, in the order of the modes; the model of the first option that
|
|
45
|
+
* names one, else the profile's. Refused as {@link modeChoice} and
|
|
46
|
+
* {@link profileChoice} refuse.
|
|
47
|
+
*/
|
|
48
|
+
export declare function modeEffect(app: Pick<AppSpec, 'name' | 'interface'>, chosen?: Record<string, string>, profile?: string): AppModeEffect;
|