@datalayer/agent-runtimes 1.3.30 → 1.3.32
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/ChatFloating.js +40 -8
- package/lib/chat/assistant/AssistantStage.d.ts +26 -7
- package/lib/chat/assistant/AssistantStage.js +115 -8
- package/lib/chat/assistant/SpeechBalloon.js +7 -3
- package/lib/chat/assistant/characters.d.ts +32 -1
- package/lib/chat/assistant/characters.js +180 -15
- package/lib/chat/assistant/state.d.ts +28 -0
- package/lib/chat/assistant/state.js +31 -0
- package/lib/chat/base/ChatBase.js +34 -19
- package/lib/chat/base/modelChoice.d.ts +27 -1
- package/lib/chat/base/modelChoice.js +29 -15
- package/lib/chat/prompt/InputPrompt.d.ts +7 -1
- package/lib/chat/prompt/InputPrompt.js +2 -2
- package/lib/chat/prompt/InputPromptBase.js +1 -1
- package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
- package/lib/chat/prompt/menus/ModelSelector.js +17 -5
- package/lib/chat/useViewportDrag.d.ts +9 -1
- package/lib/chat/useViewportDrag.js +4 -4
- package/lib/components/a2ui/index.d.ts +2 -0
- package/lib/components/a2ui/index.js +2 -0
- package/lib/components/a2ui/visibility.d.ts +15 -0
- package/lib/components/a2ui/visibility.js +24 -0
- package/lib/components/a2ui/visibleWhen.d.ts +17 -0
- package/lib/components/a2ui/visibleWhen.js +54 -0
- package/lib/examples/ChatAssistantExample.d.ts +4 -0
- package/lib/examples/ChatAssistantExample.js +26 -5
- package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
- package/lib/examples/utils/owlCharacterPlugin.js +44 -0
- package/lib/loop/apps/AppFeedback.d.ts +27 -0
- package/lib/loop/apps/AppFeedback.js +78 -0
- package/lib/loop/apps/AppRenderer.js +17 -2
- package/lib/loop/apps/appspec.js +6 -3
- package/lib/loop/apps/checks.d.ts +10 -2
- package/lib/loop/apps/checks.js +21 -10
- package/lib/loop/apps/feedback.d.ts +51 -0
- package/lib/loop/apps/feedback.js +61 -0
- package/lib/loop/apps/index.d.ts +2 -0
- package/lib/loop/apps/index.js +2 -0
- package/lib/loop/apps/rules.d.ts +14 -0
- package/lib/loop/apps/rules.js +24 -0
- package/lib/loop/core/canvasBlocks.d.ts +16 -0
- package/lib/loop/core/canvasBlocks.js +13 -0
- package/lib/loop/core/index.d.ts +7 -0
- package/lib/loop/core/index.js +3 -0
- package/lib/loop/index.d.ts +1 -1
- package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +4 -2
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +3 -4
- package/lib/loop/plugins/app-page/AppPage.js +1 -1
- package/lib/loop/plugins/app-page/{appPage.d.ts → appPageModel.d.ts} +1 -1
- package/lib/loop/plugins/app-page/index.d.ts +1 -1
- package/lib/loop/plugins/app-page/index.js +2 -2
- package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
- package/lib/loop/plugins/canvas-blocks/index.js +118 -0
- package/lib/loop/plugins/chat/ChatView.js +7 -3
- package/lib/loop/plugins/chat/turnState.d.ts +5 -2
- package/lib/loop/plugins/chat/turnState.js +2 -1
- package/lib/specs/actions.js +1 -0
- package/lib/specs/agents/agents.js +16 -4
- package/lib/specs/apps.js +21 -10
- package/lib/specs/appspecSchema.js +13 -7
- package/lib/specs/models.d.ts +1 -1
- package/lib/specs/models.js +8 -8
- package/lib/specs/tools.d.ts +1 -0
- package/lib/specs/tools.js +19 -0
- package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
- package/lib/stories/loop/LoopReference.stories.js +31 -0
- package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
- package/lib/stories/loop/ReferenceScreens.js +256 -0
- package/lib/stories/loop/pictures-main.d.ts +1 -0
- package/lib/stories/loop/pictures-main.js +64 -0
- package/lib/types/agentspecs.d.ts +6 -1
- package/lib/types/chat.d.ts +5 -0
- package/lib/types/config.d.ts +8 -0
- package/package.json +9 -6
- package/scripts/check-cloudflare-models.py +2 -2
- package/scripts/codegen/generate_apps.py +2 -1
- package/scripts/codegen/generate_models.py +4 -4
- /package/lib/loop/plugins/app-page/{appPage.js → appPageModel.js} +0 -0
package/lib/loop/core/index.js
CHANGED
|
@@ -142,6 +142,9 @@ export const LoopChatHeader = defineContributionPoint('loop.chat.header');
|
|
|
142
142
|
export const LoopAgentBlueprint = defineContributionPoint('loop.agent.blueprint');
|
|
143
143
|
export const LoopChatSuggestion = defineContributionPoint('loop.chat.suggestion');
|
|
144
144
|
export const LoopAssistantCharacter = defineContributionPoint('loop.assistant.character');
|
|
145
|
+
// A block the Canvas may place (LOOP C-12), in a module of its own so that
|
|
146
|
+
// the palette is read without the rest of the contracts.
|
|
147
|
+
export { LoopCanvasBlock } from './canvasBlocks';
|
|
145
148
|
export const LoopChatExtras = defineContributionPoint('loop.chat.extras');
|
|
146
149
|
export const LoopOpeningNotebook = defineContributionPoint('loop.notebook.opening');
|
|
147
150
|
export const LoopOpeningDocument = defineContributionPoint('loop.document.opening');
|
package/lib/loop/index.d.ts
CHANGED
|
@@ -70,4 +70,4 @@ export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSn
|
|
|
70
70
|
export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
|
|
71
71
|
export { loopPlugins, type LoopPresetOptions } from './presets';
|
|
72
72
|
export { DocumentExtension, NotebookExtension } from './extensions';
|
|
73
|
-
export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, type BehaviourOptions, type ToolArguments, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, type AppCheck, defineAppPlugin, type AppRendererProps, type AppInstance, appLayoutOptions, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, } from './apps';
|
|
73
|
+
export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, type BehaviourOptions, type ToolArguments, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, type AppCheck, type CheckContext, defineAppPlugin, type AppRendererProps, type AppInstance, appLayoutOptions, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, } from './apps';
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text } from '@primer/react';
|
|
4
|
-
import { basicCatalog, } from '@a2ui/react/v0_9';
|
|
5
4
|
import { MessageProcessor, } from '@a2ui/web_core/v0_9';
|
|
6
5
|
import { useSignalValue } from '@datalayer/reactor/react';
|
|
7
|
-
import { A2uiSurfaceComposed } from '../../../components/a2ui';
|
|
6
|
+
import { A2uiSurfaceComposed, datalayerCatalog, } from '../../../components/a2ui';
|
|
8
7
|
import { useSandboxService } from '../agents';
|
|
9
8
|
/**
|
|
10
9
|
* A2UI's basic catalog draws from inherited `--a2ui-*` custom properties, so a
|
|
@@ -27,7 +26,7 @@ export default function A2uiView({ workspace }) {
|
|
|
27
26
|
// rather than whatever the sandbox happened to execute last.
|
|
28
27
|
const sourceRef = useRef('');
|
|
29
28
|
const renderRef = useRef(null);
|
|
30
|
-
const processor = useMemo(() => new MessageProcessor([
|
|
29
|
+
const processor = useMemo(() => new MessageProcessor([datalayerCatalog], action => {
|
|
31
30
|
// The round-trip: what the reader did goes back to the code that drew
|
|
32
31
|
// the surface, and the surface it returns replaces this one. Without
|
|
33
32
|
// this a surface is a screenshot with buttons on it.
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* The same renderer draws an application's page (`loop/plugins/app-page`):
|
|
12
12
|
* there the surface is fed live `data` by path as the conversation moves, and
|
|
13
13
|
* an action is handed the surface it came from, so that what a block wrote
|
|
14
|
-
* into its data model can be read when a button is pressed.
|
|
14
|
+
* into its data model can be read when a button is pressed. It draws from
|
|
15
|
+
* Datalayer's catalog: the basic one, any block shown only while its
|
|
16
|
+
* `visible_when` holds.
|
|
15
17
|
*
|
|
16
18
|
* @module loop/plugins/a2ui-surface/InlineSurface
|
|
17
19
|
*/
|
|
18
20
|
import type { JSX } from 'react';
|
|
19
|
-
import {
|
|
21
|
+
import type { ReactComponentImplementation } from '@a2ui/react/v0_9';
|
|
20
22
|
import { type A2uiClientAction, type A2uiMessage, type SurfaceModel } from '@a2ui/web_core/v0_9';
|
|
21
23
|
/** A surface as the renderer holds it: its data model is `dataModel`. */
|
|
22
24
|
export type InlineSurfaceModel = SurfaceModel<ReactComponentImplementation>;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Box } from '@datalayer/primer-addons';
|
|
4
|
-
import { basicCatalog, } from '@a2ui/react/v0_9';
|
|
5
4
|
import { MessageProcessor, } from '@a2ui/web_core/v0_9';
|
|
6
|
-
import { A2UI_RENDER_SCOPE_SX, A2uiMarkdownProvider, A2uiSurfaceComposed, } from '../../../components/a2ui';
|
|
5
|
+
import { A2UI_RENDER_SCOPE_SX, A2uiMarkdownProvider, A2uiSurfaceComposed, datalayerCatalog, } from '../../../components/a2ui';
|
|
7
6
|
/** The catalogue a surface is rewritten to before it is drawn. */
|
|
8
|
-
export const SURFACE_CATALOG_ID =
|
|
7
|
+
export const SURFACE_CATALOG_ID = datalayerCatalog.id;
|
|
9
8
|
const INHERIT_THEME = {
|
|
10
9
|
['--a2ui-color-surface']: 'var(--bgColor-muted)',
|
|
11
10
|
['--a2ui-color-on-surface']: 'var(--fgColor-default)',
|
|
@@ -17,7 +16,7 @@ export function InlineSurface({ messages, onAction, validationError, data, }) {
|
|
|
17
16
|
// was built with must not go stale when the host's does not.
|
|
18
17
|
const onActionRef = useRef(onAction);
|
|
19
18
|
onActionRef.current = onAction;
|
|
20
|
-
const processor = useMemo(() => new MessageProcessor([
|
|
19
|
+
const processor = useMemo(() => new MessageProcessor([datalayerCatalog], action => onActionRef.current(action, processorRef.current?.model.getSurface(action.surfaceId))), []);
|
|
21
20
|
const processorRef = useRef(undefined);
|
|
22
21
|
processorRef.current = processor;
|
|
23
22
|
const [surfaces, setSurfaces] = useState([]);
|
|
@@ -5,7 +5,7 @@ import { signal } from '@datalayer/reactor';
|
|
|
5
5
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
6
6
|
import { LoopChatTurn, } from '../../core';
|
|
7
7
|
import { InlineSurface, SURFACE_CATALOG_ID, } from '../a2ui-surface/InlineSurface';
|
|
8
|
-
import { appPageAction, appPageData, appPageMessages } from './
|
|
8
|
+
import { appPageAction, appPageData, appPageMessages } from './appPageModel';
|
|
9
9
|
/** No chat in the workspace: the page reads a turn that never starts. */
|
|
10
10
|
const NO_TURN = signal({ id: 0, status: 'idle' });
|
|
11
11
|
export function AppPage({ app, workspace }) {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Pure: the plugin (`./index`) and its view (`./AppPage`) do the drawing.
|
|
20
20
|
*
|
|
21
|
-
* @module loop/plugins/app-page/
|
|
21
|
+
* @module loop/plugins/app-page/appPageModel
|
|
22
22
|
*/
|
|
23
23
|
import type { AppSettingSpec, AppSpec } from '../../../types/agentspecs';
|
|
24
24
|
import type { ChatTurnSnapshot } from '../../core';
|
|
@@ -4,5 +4,5 @@ export declare const APP_PAGE_PLUGIN_NAME = "@datalayer/loop-plugin-app-page";
|
|
|
4
4
|
/** The plugin that draws an application's page beside its conversation. */
|
|
5
5
|
export declare function defineAppPagePlugin(app: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
6
6
|
export { AppPage, type AppPageProps } from './AppPage';
|
|
7
|
-
export * from './
|
|
7
|
+
export * from './appPageModel';
|
|
8
8
|
export default defineAppPagePlugin;
|
|
@@ -3,7 +3,7 @@ import { BrowserIcon } from '@primer/octicons-react';
|
|
|
3
3
|
import { contribution, definePlugin, } from '@datalayer/reactor';
|
|
4
4
|
import { LoopEditorView } from '../../core';
|
|
5
5
|
import { AppPage } from './AppPage';
|
|
6
|
-
import { APP_PAGE_SURFACE } from './
|
|
6
|
+
import { APP_PAGE_SURFACE } from './appPageModel';
|
|
7
7
|
export const APP_PAGE_PLUGIN_NAME = '@datalayer/loop-plugin-app-page';
|
|
8
8
|
/** The plugin that draws an application's page beside its conversation. */
|
|
9
9
|
export function defineAppPagePlugin(app) {
|
|
@@ -30,5 +30,5 @@ export function defineAppPagePlugin(app) {
|
|
|
30
30
|
return plugin;
|
|
31
31
|
}
|
|
32
32
|
export { AppPage } from './AppPage';
|
|
33
|
-
export * from './
|
|
33
|
+
export * from './appPageModel';
|
|
34
34
|
export default defineAppPagePlugin;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Canvas's palette as a contribution point (LOOP C-12): each UI plugin of
|
|
3
|
+
* the catalogue that renders components (`agentspecs/ui-plugins`, C-13) is a
|
|
4
|
+
* Reactor plugin contributing its components to `loop.canvas.block`. The
|
|
5
|
+
* palette lists what the enabled plugins contribute, and nothing else; a
|
|
6
|
+
* plugin disabled takes its blocks off it, and an application that uses one
|
|
7
|
+
* of them says so in its setup notes (`blockSetupNotes`).
|
|
8
|
+
*
|
|
9
|
+
* A new kind of block arrives as a plugin contributing to the point, never as
|
|
10
|
+
* a change to the Canvas.
|
|
11
|
+
*
|
|
12
|
+
* @module loop/plugins/canvas-blocks
|
|
13
|
+
*/
|
|
14
|
+
import { type ReactorPlatform, type ReactorPlugin } from '@datalayer/reactor';
|
|
15
|
+
import type { AppSpec, ComponentSpec, UIPluginSpec } from '../../../types/agentspecs';
|
|
16
|
+
import { LoopCanvasBlock, type CanvasBlockContribution } from '../../core/canvasBlocks';
|
|
17
|
+
/** What is read of a reactor here: its contributions to the point, and who is enabled. */
|
|
18
|
+
type BlocksReader = {
|
|
19
|
+
getContributions: (point: typeof LoopCanvasBlock) => ReadonlyArray<{
|
|
20
|
+
value: CanvasBlockContribution;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
/** The name of the Reactor plugin that contributes a UI plugin's blocks. */
|
|
24
|
+
export declare const canvasBlocksPluginName: (uiPlugin: string) => string;
|
|
25
|
+
/** A UI plugin of the catalogue as the Reactor plugin contributing its components as blocks. */
|
|
26
|
+
export declare function uiPluginBlocks(uiPlugin: UIPluginSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* The plugins of the catalogue's UI plugins that render components and may
|
|
29
|
+
* be named today (`enabled` in the catalogue).
|
|
30
|
+
*/
|
|
31
|
+
export declare const CANVAS_BLOCK_PLUGINS: ReactorPlugin<Record<string, never>, unknown, unknown>[];
|
|
32
|
+
/**
|
|
33
|
+
* A reactor holding the block plugins given, started, with those named in
|
|
34
|
+
* `disabled` switched off. A name no plugin has is an error.
|
|
35
|
+
*/
|
|
36
|
+
export declare function canvasBlocksReactor(disabled?: readonly string[], plugins?: ReactorPlugin<any, any, any>[]): ReactorPlatform;
|
|
37
|
+
/**
|
|
38
|
+
* The palette of an organization: the block plugins of the UI plugins it has
|
|
39
|
+
* turned off (`plugins_off` in IAM, catalogue ids such as 'a2ui') switched
|
|
40
|
+
* off. A UI plugin of the catalogue that contributes no block takes nothing
|
|
41
|
+
* off; an id the catalogue does not have is said by `unknownPluginsOff`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function canvasBlocksReactorOf(pluginsOff: readonly string[]): ReactorPlatform;
|
|
44
|
+
/** The ids an organization turns off that no UI plugin of the catalogue has. */
|
|
45
|
+
export declare function unknownPluginsOff(pluginsOff: readonly string[]): string[];
|
|
46
|
+
/**
|
|
47
|
+
* What an application's page uses from a UI plugin its organization has
|
|
48
|
+
* turned off, as its setup notes say it (`blockSetupNotes`, on the palette of
|
|
49
|
+
* `canvasBlocksReactorOf`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function pluginsOffSetupNotes(app: Pick<AppSpec, 'interface'>, pluginsOff: readonly string[]): string[];
|
|
52
|
+
/** The blocks the enabled plugins contribute, in contribution order: the palette. */
|
|
53
|
+
export declare function canvasBlocksOf(reactor: BlocksReader): ComponentSpec[];
|
|
54
|
+
/** The components an application's page names: those it may use, and those its surface places. */
|
|
55
|
+
export declare function componentsUsedBy(app: Pick<AppSpec, 'interface'>): string[];
|
|
56
|
+
/**
|
|
57
|
+
* What an application's page uses that no enabled plugin contributes, as its
|
|
58
|
+
* setup notes say it: one sentence per UI plugin of the catalogue whose
|
|
59
|
+
* blocks it uses while its plugin is off. A component the catalogue does not
|
|
60
|
+
* have is not a note but a problem of the instant checks.
|
|
61
|
+
*/
|
|
62
|
+
export declare function blockSetupNotes(app: Pick<AppSpec, 'interface'>, reactor: BlocksReader): string[];
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The Canvas's palette as a contribution point (LOOP C-12): each UI plugin of
|
|
7
|
+
* the catalogue that renders components (`agentspecs/ui-plugins`, C-13) is a
|
|
8
|
+
* Reactor plugin contributing its components to `loop.canvas.block`. The
|
|
9
|
+
* palette lists what the enabled plugins contribute, and nothing else; a
|
|
10
|
+
* plugin disabled takes its blocks off it, and an application that uses one
|
|
11
|
+
* of them says so in its setup notes (`blockSetupNotes`).
|
|
12
|
+
*
|
|
13
|
+
* A new kind of block arrives as a plugin contributing to the point, never as
|
|
14
|
+
* a change to the Canvas.
|
|
15
|
+
*
|
|
16
|
+
* @module loop/plugins/canvas-blocks
|
|
17
|
+
*/
|
|
18
|
+
import { buildReactorFromPlugins, contribution, definePlugin, } from '@datalayer/reactor';
|
|
19
|
+
import { listUIPlugins } from '../../../specs/uiPlugins';
|
|
20
|
+
import { LoopCanvasBlock, } from '../../core/canvasBlocks';
|
|
21
|
+
/** The name of the Reactor plugin that contributes a UI plugin's blocks. */
|
|
22
|
+
export const canvasBlocksPluginName = (uiPlugin) => `@datalayer/loop-plugin-ui-${uiPlugin}`;
|
|
23
|
+
/** A UI plugin of the catalogue as the Reactor plugin contributing its components as blocks. */
|
|
24
|
+
export function uiPluginBlocks(uiPlugin) {
|
|
25
|
+
return definePlugin({
|
|
26
|
+
name: canvasBlocksPluginName(uiPlugin.id),
|
|
27
|
+
displayName: uiPlugin.name,
|
|
28
|
+
description: `The blocks of ${uiPlugin.name} on the Canvas: ${uiPlugin.components.map(component => component.name).join(', ')}.`,
|
|
29
|
+
octicon: 'apps',
|
|
30
|
+
contributes: uiPlugin.components.map(component => contribution(LoopCanvasBlock, { id: component.id, uiPlugin: uiPlugin.id, component }, { id: component.id })),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The plugins of the catalogue's UI plugins that render components and may
|
|
35
|
+
* be named today (`enabled` in the catalogue).
|
|
36
|
+
*/
|
|
37
|
+
export const CANVAS_BLOCK_PLUGINS = listUIPlugins()
|
|
38
|
+
.filter(uiPlugin => uiPlugin.enabled && uiPlugin.components.length > 0)
|
|
39
|
+
.map(uiPluginBlocks);
|
|
40
|
+
/**
|
|
41
|
+
* A reactor holding the block plugins given, started, with those named in
|
|
42
|
+
* `disabled` switched off. A name no plugin has is an error.
|
|
43
|
+
*/
|
|
44
|
+
export function canvasBlocksReactor(disabled = [], plugins = CANVAS_BLOCK_PLUGINS) {
|
|
45
|
+
const reactor = buildReactorFromPlugins(plugins);
|
|
46
|
+
reactor.start();
|
|
47
|
+
for (const name of disabled) {
|
|
48
|
+
if (!reactor.hasPlugin(name)) {
|
|
49
|
+
throw new Error(`No plugin of the Canvas is named "${name}"; they are ${plugins.map(plugin => plugin.name).join(', ') || 'none'}.`);
|
|
50
|
+
}
|
|
51
|
+
reactor.disable(name);
|
|
52
|
+
}
|
|
53
|
+
return reactor;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The palette of an organization: the block plugins of the UI plugins it has
|
|
57
|
+
* turned off (`plugins_off` in IAM, catalogue ids such as 'a2ui') switched
|
|
58
|
+
* off. A UI plugin of the catalogue that contributes no block takes nothing
|
|
59
|
+
* off; an id the catalogue does not have is said by `unknownPluginsOff`.
|
|
60
|
+
*/
|
|
61
|
+
export function canvasBlocksReactorOf(pluginsOff) {
|
|
62
|
+
const blockPlugins = new Set(CANVAS_BLOCK_PLUGINS.map(plugin => plugin.name));
|
|
63
|
+
return canvasBlocksReactor(pluginsOff
|
|
64
|
+
.map(canvasBlocksPluginName)
|
|
65
|
+
.filter(name => blockPlugins.has(name)));
|
|
66
|
+
}
|
|
67
|
+
/** The ids an organization turns off that no UI plugin of the catalogue has. */
|
|
68
|
+
export function unknownPluginsOff(pluginsOff) {
|
|
69
|
+
const known = new Set(listUIPlugins().map(uiPlugin => uiPlugin.id));
|
|
70
|
+
return pluginsOff.filter(id => !known.has(id));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What an application's page uses from a UI plugin its organization has
|
|
74
|
+
* turned off, as its setup notes say it (`blockSetupNotes`, on the palette of
|
|
75
|
+
* `canvasBlocksReactorOf`).
|
|
76
|
+
*/
|
|
77
|
+
export function pluginsOffSetupNotes(app, pluginsOff) {
|
|
78
|
+
return pluginsOff.length > 0
|
|
79
|
+
? blockSetupNotes(app, canvasBlocksReactorOf(pluginsOff))
|
|
80
|
+
: [];
|
|
81
|
+
}
|
|
82
|
+
/** The blocks the enabled plugins contribute, in contribution order: the palette. */
|
|
83
|
+
export function canvasBlocksOf(reactor) {
|
|
84
|
+
return reactor
|
|
85
|
+
.getContributions(LoopCanvasBlock)
|
|
86
|
+
.map(entry => entry.value.component);
|
|
87
|
+
}
|
|
88
|
+
/** The components an application's page names: those it may use, and those its surface places. */
|
|
89
|
+
export function componentsUsedBy(app) {
|
|
90
|
+
return [
|
|
91
|
+
...new Set([
|
|
92
|
+
...(app.interface.components ?? []),
|
|
93
|
+
...(app.interface.surface?.components ?? []).map(node => String(node.component)),
|
|
94
|
+
]),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* What an application's page uses that no enabled plugin contributes, as its
|
|
99
|
+
* setup notes say it: one sentence per UI plugin of the catalogue whose
|
|
100
|
+
* blocks it uses while its plugin is off. A component the catalogue does not
|
|
101
|
+
* have is not a note but a problem of the instant checks.
|
|
102
|
+
*/
|
|
103
|
+
export function blockSetupNotes(app, reactor) {
|
|
104
|
+
const contributed = new Set(reactor.getContributions(LoopCanvasBlock).map(entry => entry.value.id));
|
|
105
|
+
const off = new Map();
|
|
106
|
+
for (const name of componentsUsedBy(app)) {
|
|
107
|
+
if (contributed.has(name)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
for (const uiPlugin of listUIPlugins()) {
|
|
111
|
+
const component = uiPlugin.components.find(each => each.id === name);
|
|
112
|
+
if (component) {
|
|
113
|
+
off.set(uiPlugin, [...(off.get(uiPlugin) ?? []), component.name]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...off.entries()].map(([uiPlugin, names]) => `The UI plugin “${uiPlugin.name}” is not enabled, and its page uses its ${names.length === 1 ? 'block' : 'blocks'} ${names.join(', ')}: ${names.length === 1 ? 'it is' : 'they are'} off the Canvas until it is.`);
|
|
118
|
+
}
|
|
@@ -8,7 +8,7 @@ import { computed, signal } from '@datalayer/reactor';
|
|
|
8
8
|
const NO_STANCE = signal(undefined);
|
|
9
9
|
import { useContributions, useSignalValue, useGate, useReactorPlatform, ReactorLazy, ReactorSlot, useSlotComponents, } from '@datalayer/reactor/react';
|
|
10
10
|
import { ChatBase } from '../../../chat/base/ChatBase';
|
|
11
|
-
import { offeredModels as offeredModelsFor, readServerCatalogue, } from '../../../chat/base/modelChoice';
|
|
11
|
+
import { offeredModels as offeredModelsFor, readServerCatalogue, readServerDecisions, } from '../../../chat/base/modelChoice';
|
|
12
12
|
import { SUGGESTION_CHIP_WIDTH } from '../../../chat/display/EmptyState';
|
|
13
13
|
import { AnonymousKeyExpired } from '@datalayer/core/lib/components/anonymous/AnonymousKeyExpired';
|
|
14
14
|
import { useAnonymousSessionStore } from '../../../runtimes/browser/anonymousToken';
|
|
@@ -393,7 +393,7 @@ export default function ChatView({ workspace }) {
|
|
|
393
393
|
}
|
|
394
394
|
setTransient(null);
|
|
395
395
|
// A new turn: whatever the panel showed is gone, this message is it.
|
|
396
|
-
turnFeedRef.current?.begin(message);
|
|
396
|
+
turnFeedRef.current?.begin(message, controlsRef.current?.thread());
|
|
397
397
|
const outcome = await workspace.submit(message);
|
|
398
398
|
if (!outcome.handled) {
|
|
399
399
|
/*
|
|
@@ -1145,6 +1145,8 @@ export default function ChatView({ workspace }) {
|
|
|
1145
1145
|
const skillActions = useSkillActions(agentId);
|
|
1146
1146
|
// `undefined` until the runtime answers: its answer is then the list.
|
|
1147
1147
|
const [catalogModels, setCatalogModels] = useState(undefined);
|
|
1148
|
+
// The typed-decision models the runtime lists apart: shown, never picked.
|
|
1149
|
+
const [decisions, setDecisions] = useState(undefined);
|
|
1148
1150
|
useEffect(() => {
|
|
1149
1151
|
/*
|
|
1150
1152
|
* Only where there is a server to ask.
|
|
@@ -1171,6 +1173,7 @@ export default function ChatView({ workspace }) {
|
|
|
1171
1173
|
// A runtime that did not answer offers nothing: the agentspecs
|
|
1172
1174
|
// catalogue is not its answer.
|
|
1173
1175
|
setCatalogModels(payload ? readServerCatalogue(payload) : []);
|
|
1176
|
+
setDecisions(payload ? readServerDecisions(payload) : undefined);
|
|
1174
1177
|
}
|
|
1175
1178
|
})
|
|
1176
1179
|
// No catalogue is not an error worth a banner: the menu simply has
|
|
@@ -1193,7 +1196,7 @@ export default function ChatView({ workspace }) {
|
|
|
1193
1196
|
* agentspecs catalogue stands in, filtered to the models marked available.
|
|
1194
1197
|
*/
|
|
1195
1198
|
const offeredModels = useMemo(() => offeredModelsFor(catalogModels, () => Object.values(AI_MODEL_CATALOGUE)
|
|
1196
|
-
// A typed-
|
|
1199
|
+
// A typed-decision model answers typed questions, not a chat.
|
|
1197
1200
|
.filter(model => model.available && isChatModel(model))
|
|
1198
1201
|
.map(model => ({
|
|
1199
1202
|
id: model.id,
|
|
@@ -1335,6 +1338,7 @@ export default function ChatView({ workspace }) {
|
|
|
1335
1338
|
agentUsage: contextUsage ?? undefined,
|
|
1336
1339
|
showModelSelector: true,
|
|
1337
1340
|
models: offeredModels,
|
|
1341
|
+
decisions,
|
|
1338
1342
|
selectedModel: activeModel,
|
|
1339
1343
|
onModelSelect: model => void selectModel(model),
|
|
1340
1344
|
showToolsMenu: true,
|
|
@@ -15,8 +15,11 @@ import type { ChatTurnSnapshot, ChatTurnStatus } from '../../core';
|
|
|
15
15
|
export type TurnFeed = {
|
|
16
16
|
/** The turn, for readers. */
|
|
17
17
|
turn: ReadonlySignal<ChatTurnSnapshot>;
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* A new turn: the previous one is gone, this one holds the message, and
|
|
20
|
+
* the conversation it went to, when the chat knows it.
|
|
21
|
+
*/
|
|
22
|
+
begin: (user: string, thread?: string) => void;
|
|
20
23
|
/** The reply so far. Moves the turn to `streaming` on its first text. */
|
|
21
24
|
assistant: (text: string) => void;
|
|
22
25
|
/** The agent stopped, one way or another. */
|
|
@@ -19,13 +19,14 @@ export function createTurnFeed() {
|
|
|
19
19
|
const turn = signal(IDLE);
|
|
20
20
|
return {
|
|
21
21
|
turn,
|
|
22
|
-
begin: user => {
|
|
22
|
+
begin: (user, thread) => {
|
|
23
23
|
// The window's fill carries over: it is the conversation's, not the
|
|
24
24
|
// turn's, and the footer under a fresh turn should not read empty
|
|
25
25
|
// until the agent reports again.
|
|
26
26
|
turn.value = {
|
|
27
27
|
id: turn.value.id + 1,
|
|
28
28
|
user,
|
|
29
|
+
...(thread ? { thread } : {}),
|
|
29
30
|
status: 'thinking',
|
|
30
31
|
usage: turn.value.usage,
|
|
31
32
|
};
|
package/lib/specs/actions.js
CHANGED
|
@@ -4385,7 +4385,7 @@ export const EXAMPLE_SIMPLE_AGENTSPEC_0_0_1 = {
|
|
|
4385
4385
|
id: 'example-simple',
|
|
4386
4386
|
version: '0.0.1',
|
|
4387
4387
|
name: 'A Simple Agent',
|
|
4388
|
-
description: `A simple conversational agent
|
|
4388
|
+
description: `A simple conversational agent with one tool, decide: it asks Jev typed questions (yes or no with a probability, a choice, a score) through datalayer-ai-inference. No MCP servers, no skills beyond events.`,
|
|
4389
4389
|
tags: ['workflow', 'automation', 'analysis'],
|
|
4390
4390
|
domain: undefined,
|
|
4391
4391
|
enabled: true,
|
|
@@ -4397,7 +4397,7 @@ export const EXAMPLE_SIMPLE_AGENTSPEC_0_0_1 = {
|
|
|
4397
4397
|
? toAgentSkillSpec(SKILL_MAP['events:0.0.1'])
|
|
4398
4398
|
: undefined,
|
|
4399
4399
|
].filter(Boolean),
|
|
4400
|
-
tools: [TOOL_MAP['runtime-echo:0.0.1']],
|
|
4400
|
+
tools: [TOOL_MAP['runtime-echo:0.0.1'], TOOL_MAP['decide:0.0.1']],
|
|
4401
4401
|
frontendTools: [
|
|
4402
4402
|
FRONTEND_TOOL_MAP['jupyter-notebook:0.0.1'],
|
|
4403
4403
|
FRONTEND_TOOL_MAP['lexical-document:0.0.1'],
|
|
@@ -4420,13 +4420,25 @@ export const EXAMPLE_SIMPLE_AGENTSPEC_0_0_1 = {
|
|
|
4420
4420
|
text: 'Summarize the key points of a topic I describe',
|
|
4421
4421
|
summary: 'Summarize a topic',
|
|
4422
4422
|
},
|
|
4423
|
+
{
|
|
4424
|
+
text: "Is this support ticket urgent? 'Payouts have failed for 3 days.'",
|
|
4425
|
+
summary: 'Decide: is it urgent?',
|
|
4426
|
+
},
|
|
4427
|
+
{
|
|
4428
|
+
text: "Which team should handle: 'I was charged twice'? Billing, Tech or Sales.",
|
|
4429
|
+
summary: 'Decide: which team?',
|
|
4430
|
+
},
|
|
4431
|
+
{
|
|
4432
|
+
text: "Score how positive this review is from 1 to 5: 'Setup took an hour, but support answered fast and it works.'",
|
|
4433
|
+
summary: 'Decide: score a review',
|
|
4434
|
+
},
|
|
4423
4435
|
],
|
|
4424
|
-
welcomeMessage: "Hi! I'm a simple assistant. I
|
|
4436
|
+
welcomeMessage: "Hi! I'm a simple assistant. I'm happy to chat, answer questions and help you think through ideas, and I can decide: ask me whether a ticket is urgent, which team should take it, or how positive a review is.\n",
|
|
4425
4437
|
welcomeNotebook: undefined,
|
|
4426
4438
|
welcomeDocument: undefined,
|
|
4427
4439
|
sandboxVariant: 'jupyter-server',
|
|
4428
4440
|
harness: 'pydantic-ai',
|
|
4429
|
-
systemPrompt: `You are a helpful, friendly AI assistant. You
|
|
4441
|
+
systemPrompt: `You are a helpful, friendly AI assistant. You have no MCP servers. You have a decide tool: when asked whether something holds (yes or no), which of named options fits, or where something sits on a scale, call decide with the text as the state and one typed question (noul, choice or score), then answer with what it decided and its probability or confidence. For anything else, answer from your training knowledge, be concise, and let the user know if a question is outside your knowledge.
|
|
4430
4442
|
`,
|
|
4431
4443
|
systemPromptCodemodeAddons: undefined,
|
|
4432
4444
|
goal: undefined,
|
package/lib/specs/apps.js
CHANGED
|
@@ -94,6 +94,7 @@ export const CUSTOMER_INTERVIEW_APP_0_0_1 = {
|
|
|
94
94
|
record: {
|
|
95
95
|
keepFor: '1_years',
|
|
96
96
|
include: ['conversations', 'outputs', 'feedback'],
|
|
97
|
+
suggestTests: false,
|
|
97
98
|
retentionDays: 365,
|
|
98
99
|
},
|
|
99
100
|
checks: {
|
|
@@ -172,6 +173,7 @@ export const DATA_QUALITY_APP_0_0_1 = {
|
|
|
172
173
|
record: {
|
|
173
174
|
keepFor: '1_years',
|
|
174
175
|
include: ['decisions', 'sources', 'checks'],
|
|
176
|
+
suggestTests: false,
|
|
175
177
|
retentionDays: 365,
|
|
176
178
|
},
|
|
177
179
|
checks: {
|
|
@@ -238,7 +240,7 @@ export const DATA_QUALITY_APP_0_0_1 = {
|
|
|
238
240
|
],
|
|
239
241
|
minConfidence: 0.0,
|
|
240
242
|
scenarios: [],
|
|
241
|
-
|
|
243
|
+
decisionModel: 'cloudflare:gtw/typesafe/jev',
|
|
242
244
|
},
|
|
243
245
|
enabled: true,
|
|
244
246
|
tags: ['example', 'decision', 'data-quality'],
|
|
@@ -362,6 +364,7 @@ export const INBOX_TRIAGE_APP_0_0_1 = {
|
|
|
362
364
|
record: {
|
|
363
365
|
keepFor: '1_years',
|
|
364
366
|
include: ['conversations', 'actions', 'decisions', 'approvals', 'checks'],
|
|
367
|
+
suggestTests: false,
|
|
365
368
|
retentionDays: 365,
|
|
366
369
|
},
|
|
367
370
|
checks: {
|
|
@@ -413,7 +416,7 @@ export const MODEL_CHOICE_APP_0_0_1 = {
|
|
|
413
416
|
version: '0.0.1',
|
|
414
417
|
name: 'Model Choice',
|
|
415
418
|
kind: 'decision',
|
|
416
|
-
description: 'Which chat model should this use case run on? For a team choosing a model for one job — a summarizer, a classifier, an agent — from what a benchmark run measured and what a
|
|
419
|
+
description: 'Which chat model should this use case run on? For a team choosing a model for one job — a summarizer, a classifier, an agent — from what a benchmark run measured and what a decision model reads in the answers.',
|
|
417
420
|
owner: 'Datalayer <info@datalayer.io>',
|
|
418
421
|
agent: 'jupyter-data-analyst:0.0.1',
|
|
419
422
|
team: '',
|
|
@@ -463,6 +466,7 @@ export const MODEL_CHOICE_APP_0_0_1 = {
|
|
|
463
466
|
record: {
|
|
464
467
|
keepFor: '1_years',
|
|
465
468
|
include: ['decisions', 'sources', 'checks'],
|
|
469
|
+
suggestTests: false,
|
|
466
470
|
retentionDays: 365,
|
|
467
471
|
},
|
|
468
472
|
checks: {
|
|
@@ -582,7 +586,7 @@ export const MODEL_CHOICE_APP_0_0_1 = {
|
|
|
582
586
|
},
|
|
583
587
|
},
|
|
584
588
|
],
|
|
585
|
-
|
|
589
|
+
decisionModel: 'cloudflare:gtw/typesafe/jev',
|
|
586
590
|
},
|
|
587
591
|
enabled: true,
|
|
588
592
|
tags: ['example', 'decision', 'benchmarks', 'models'],
|
|
@@ -800,6 +804,7 @@ export const PIPELINE_REPORT_APP_0_0_1 = {
|
|
|
800
804
|
'sources',
|
|
801
805
|
'outputs',
|
|
802
806
|
],
|
|
807
|
+
suggestTests: false,
|
|
803
808
|
retentionDays: 2555,
|
|
804
809
|
},
|
|
805
810
|
checks: {
|
|
@@ -1060,6 +1065,7 @@ export const QUOTE_CALCULATOR_APP_0_0_1 = {
|
|
|
1060
1065
|
record: {
|
|
1061
1066
|
keepFor: '90_days',
|
|
1062
1067
|
include: ['actions', 'outputs'],
|
|
1068
|
+
suggestTests: false,
|
|
1063
1069
|
retentionDays: 90,
|
|
1064
1070
|
},
|
|
1065
1071
|
checks: {
|
|
@@ -1273,6 +1279,7 @@ export const REPORT_FROM_A_FILE_APP_0_0_1 = {
|
|
|
1273
1279
|
record: {
|
|
1274
1280
|
keepFor: '90_days',
|
|
1275
1281
|
include: ['actions', 'outputs'],
|
|
1282
|
+
suggestTests: false,
|
|
1276
1283
|
retentionDays: 90,
|
|
1277
1284
|
},
|
|
1278
1285
|
checks: {
|
|
@@ -1355,6 +1362,7 @@ export const SHIP_OR_FIX_APP_0_0_1 = {
|
|
|
1355
1362
|
record: {
|
|
1356
1363
|
keepFor: '1_years',
|
|
1357
1364
|
include: ['decisions', 'sources', 'checks'],
|
|
1365
|
+
suggestTests: false,
|
|
1358
1366
|
retentionDays: 365,
|
|
1359
1367
|
},
|
|
1360
1368
|
checks: {
|
|
@@ -1449,7 +1457,7 @@ export const SHIP_OR_FIX_APP_0_0_1 = {
|
|
|
1449
1457
|
},
|
|
1450
1458
|
},
|
|
1451
1459
|
],
|
|
1452
|
-
|
|
1460
|
+
decisionModel: 'cloudflare:gtw/typesafe/jev',
|
|
1453
1461
|
},
|
|
1454
1462
|
enabled: true,
|
|
1455
1463
|
tags: ['example', 'decision', 'benchmarks'],
|
|
@@ -1512,6 +1520,7 @@ export const SUPPLIER_COMPARISON_APP_0_0_1 = {
|
|
|
1512
1520
|
record: {
|
|
1513
1521
|
keepFor: '1_years',
|
|
1514
1522
|
include: ['decisions', 'sources', 'checks'],
|
|
1523
|
+
suggestTests: false,
|
|
1515
1524
|
retentionDays: 365,
|
|
1516
1525
|
},
|
|
1517
1526
|
checks: {
|
|
@@ -1591,7 +1600,7 @@ export const SUPPLIER_COMPARISON_APP_0_0_1 = {
|
|
|
1591
1600
|
],
|
|
1592
1601
|
minConfidence: 0.0,
|
|
1593
1602
|
scenarios: [],
|
|
1594
|
-
|
|
1603
|
+
decisionModel: 'cloudflare:gtw/typesafe/jev',
|
|
1595
1604
|
},
|
|
1596
1605
|
enabled: true,
|
|
1597
1606
|
tags: ['example', 'decision', 'procurement'],
|
|
@@ -1802,6 +1811,7 @@ export const SUPPORT_DESK_APP_0_0_1 = {
|
|
|
1802
1811
|
record: {
|
|
1803
1812
|
keepFor: '1_years',
|
|
1804
1813
|
include: ['conversations', 'sources', 'feedback'],
|
|
1814
|
+
suggestTests: false,
|
|
1805
1815
|
retentionDays: 365,
|
|
1806
1816
|
},
|
|
1807
1817
|
checks: {
|
|
@@ -1914,6 +1924,7 @@ export const WEB_RESEARCH_APP_0_0_1 = {
|
|
|
1914
1924
|
record: {
|
|
1915
1925
|
keepFor: '90_days',
|
|
1916
1926
|
include: ['conversations', 'sources', 'feedback'],
|
|
1927
|
+
suggestTests: false,
|
|
1917
1928
|
retentionDays: 90,
|
|
1918
1929
|
},
|
|
1919
1930
|
checks: {
|
|
@@ -2111,7 +2122,7 @@ export const APP_SOURCES = {
|
|
|
2111
2122
|
instructions: 'Can the proposed correction be applied without a person checking each row?',
|
|
2112
2123
|
},
|
|
2113
2124
|
],
|
|
2114
|
-
|
|
2125
|
+
decision_model: 'cloudflare:gtw/typesafe/jev',
|
|
2115
2126
|
},
|
|
2116
2127
|
tags: ['example', 'decision', 'data-quality'],
|
|
2117
2128
|
icon: 'filter',
|
|
@@ -2240,7 +2251,7 @@ export const APP_SOURCES = {
|
|
|
2240
2251
|
id: 'model-choice',
|
|
2241
2252
|
name: 'Model Choice',
|
|
2242
2253
|
kind: 'decision',
|
|
2243
|
-
description: 'Which chat model should this use case run on? For a team choosing a model for one job — a summarizer, a classifier, an agent — from what a benchmark run measured and what a
|
|
2254
|
+
description: 'Which chat model should this use case run on? For a team choosing a model for one job — a summarizer, a classifier, an agent — from what a benchmark run measured and what a decision model reads in the answers.',
|
|
2244
2255
|
owner: 'Datalayer <info@datalayer.io>',
|
|
2245
2256
|
agent: 'jupyter-data-analyst:0.0.1',
|
|
2246
2257
|
contents: [
|
|
@@ -2353,7 +2364,7 @@ export const APP_SOURCES = {
|
|
|
2353
2364
|
},
|
|
2354
2365
|
},
|
|
2355
2366
|
],
|
|
2356
|
-
|
|
2367
|
+
decision_model: 'cloudflare:gtw/typesafe/jev',
|
|
2357
2368
|
},
|
|
2358
2369
|
tags: ['example', 'decision', 'benchmarks', 'models'],
|
|
2359
2370
|
icon: 'cpu',
|
|
@@ -3046,7 +3057,7 @@ export const APP_SOURCES = {
|
|
|
3046
3057
|
},
|
|
3047
3058
|
},
|
|
3048
3059
|
],
|
|
3049
|
-
|
|
3060
|
+
decision_model: 'cloudflare:gtw/typesafe/jev',
|
|
3050
3061
|
},
|
|
3051
3062
|
tags: ['example', 'decision', 'benchmarks'],
|
|
3052
3063
|
icon: 'checklist',
|
|
@@ -3124,7 +3135,7 @@ export const APP_SOURCES = {
|
|
|
3124
3135
|
],
|
|
3125
3136
|
},
|
|
3126
3137
|
],
|
|
3127
|
-
|
|
3138
|
+
decision_model: 'cloudflare:gtw/typesafe/jev',
|
|
3128
3139
|
},
|
|
3129
3140
|
tags: ['example', 'decision', 'procurement'],
|
|
3130
3141
|
icon: 'package',
|