@datalayer/agent-runtimes 1.3.58 → 1.3.60
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.d.ts +11 -1
- package/lib/chat/ChatFloating.js +6 -2
- package/lib/chat/assistant/AssistantStage.d.ts +7 -1
- package/lib/chat/assistant/AssistantStage.js +5 -3
- package/lib/chat/assistant/DecisionAsk.d.ts +31 -0
- package/lib/chat/assistant/DecisionAsk.js +96 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +11 -2
- package/lib/chat/assistant/SpeechBalloon.js +5 -4
- package/lib/chat/assistant/decisions.d.ts +104 -0
- package/lib/chat/assistant/decisions.js +161 -0
- package/lib/chat/marks/toolMarks.js +2 -2
- package/lib/config/AgentConfiguration.d.ts +2 -1
- package/lib/config/AgentConfiguration.js +2 -2
- package/lib/examples/AgentDecideExample.d.ts +24 -0
- package/lib/examples/AgentDecideExample.js +73 -0
- package/lib/examples/AgentNotificationsExample.js +1 -1
- package/lib/examples/AgentSkillsExample.js +1 -1
- package/lib/examples/AgentspecsExample.js +3 -3
- package/lib/examples/ChatAssistantGalleryExample.d.ts +12 -0
- package/lib/examples/ChatAssistantGalleryExample.js +181 -0
- package/lib/examples/example-selector.js +2 -0
- package/lib/examples/index.d.ts +2 -0
- package/lib/examples/index.js +2 -0
- package/lib/examples/main.js +5 -0
- package/lib/examples/utils/AssistantGalleryGrid.d.ts +53 -0
- package/lib/examples/utils/AssistantGalleryGrid.js +123 -0
- package/lib/examples/utils/assistantGallery.d.ts +42 -0
- package/lib/examples/utils/assistantGallery.js +102 -0
- package/lib/examples/utils/testSpriteCharacter.d.ts +9 -0
- package/lib/examples/utils/testSpriteCharacter.js +60 -0
- package/lib/loop/apps/appspec.js +4 -4
- package/lib/loop/apps/checks.js +6 -6
- package/lib/loop/apps/rules.js +2 -2
- package/lib/loop/plugins/assistant/index.d.ts +14 -2
- package/lib/loop/plugins/assistant/index.js +13 -4
- package/lib/specs/actions.d.ts +1 -1
- package/lib/specs/actions.js +1 -1
- package/lib/specs/agents/agents.js +197 -192
- package/lib/specs/apps.js +13 -13
- package/lib/specs/appspecSchema.js +3 -3
- package/lib/specs/backendTools.d.ts +31 -0
- package/lib/specs/{tools.js → backendTools.js} +48 -48
- package/lib/specs/cogs.js +8 -7
- package/lib/specs/frames.js +5 -5
- package/lib/specs/index.d.ts +1 -1
- package/lib/specs/index.js +1 -1
- package/lib/stores/agentRuntimeStore.js +2 -2
- package/lib/stories/loop/pictures-main.js +16 -1
- package/lib/types/agentspecs.d.ts +7 -5
- package/lib/types/config.d.ts +2 -2
- package/lib/types/tools.d.ts +3 -3
- package/package.json +1 -1
- package/scripts/codegen/generate_agents.py +23 -12
- package/scripts/codegen/generate_apps.py +3 -2
- package/scripts/codegen/{generate_tools.py → generate_backend_tools.py} +29 -29
- package/scripts/codegen/generate_cogs.py +2 -2
- package/scripts/codegen/generate_frames.py +2 -2
- package/lib/specs/tools.d.ts +0 -31
|
@@ -110,6 +110,16 @@ export interface ChatFloatingProps extends ChatCommonProps {
|
|
|
110
110
|
* @default 'paperclip'
|
|
111
111
|
*/
|
|
112
112
|
assistantCharacter?: string | AssistantCharacter | AssistantCharacterData;
|
|
113
|
+
/**
|
|
114
|
+
* The runtime the floating assistant asks typed decisions at — its
|
|
115
|
+
* `/api/v1/configure/inference/decisions`, as the `decide` tool asks — and
|
|
116
|
+
* the token to ask with, if it needs one: its balloon offers *Ask a
|
|
117
|
+
* decision*, answered there in plain words with its confidence.
|
|
118
|
+
*/
|
|
119
|
+
decisions?: {
|
|
120
|
+
serverUrl: string;
|
|
121
|
+
token?: string;
|
|
122
|
+
};
|
|
113
123
|
/**
|
|
114
124
|
* Callback when the user switches view mode via the header toggle.
|
|
115
125
|
* The parent component receives the new ChatViewMode value.
|
|
@@ -144,5 +154,5 @@ export interface ChatFloatingProps extends ChatCommonProps {
|
|
|
144
154
|
* ChatFloating component
|
|
145
155
|
* A floating chat window built on ChatBase
|
|
146
156
|
*/
|
|
147
|
-
export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, }: ChatFloatingProps): React.JSX.Element;
|
|
157
|
+
export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, decisions, }: ChatFloatingProps): React.JSX.Element;
|
|
148
158
|
export default ChatFloating;
|
package/lib/chat/ChatFloating.js
CHANGED
|
@@ -31,6 +31,7 @@ import { ButtonGlow } from './display/ButtonGlow';
|
|
|
31
31
|
import { useViewportDrag } from './useViewportDrag';
|
|
32
32
|
import { disabledChatViewModes, resolveMountPoint } from './viewModes';
|
|
33
33
|
import { AssistantStage } from './assistant/AssistantStage';
|
|
34
|
+
import { decisionsAskerAt } from './assistant/decisions';
|
|
34
35
|
import { SpeechBalloon } from './assistant/SpeechBalloon';
|
|
35
36
|
import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
|
|
36
37
|
import { assistantStateOf, latestSaying, newestIsAnswer, keepAway, keptAway, ASSISTANT_WORDS, } from './assistant/state';
|
|
@@ -63,7 +64,7 @@ const clamp = (value, max) => Math.max(8, Math.min(value, Math.max(8, max)));
|
|
|
63
64
|
* ChatFloating component
|
|
64
65
|
* A floating chat window built on ChatBase
|
|
65
66
|
*/
|
|
66
|
-
export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, }) {
|
|
67
|
+
export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, decisions, }) {
|
|
67
68
|
// Store-based state
|
|
68
69
|
const storeIsOpen = useChatOpen();
|
|
69
70
|
const storeMessages = useChatMessages();
|
|
@@ -194,6 +195,9 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
194
195
|
},
|
|
195
196
|
}
|
|
196
197
|
: undefined;
|
|
198
|
+
const assistantDecide = useMemo(() => decisions?.serverUrl
|
|
199
|
+
? decisionsAskerAt(decisions.serverUrl, decisions.token)
|
|
200
|
+
: undefined, [decisions?.serverUrl, decisions?.token]);
|
|
197
201
|
const assistantBalloon = assistantState === 'paused'
|
|
198
202
|
? { text: ASSISTANT_WORDS.paused }
|
|
199
203
|
: balloonApproval
|
|
@@ -835,7 +839,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
835
839
|
left: px(stageDrag.position.left),
|
|
836
840
|
top: px(stageDrag.position.top),
|
|
837
841
|
}
|
|
838
|
-
: getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
|
|
842
|
+
: getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef, decide: assistantDecide })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
|
|
839
843
|
...getPositionStyles(),
|
|
840
844
|
}, children: _jsxs(Box, { sx: {
|
|
841
845
|
position: 'relative',
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import type { JSX, RefObject } from 'react';
|
|
18
18
|
import { type AssistantCharacter } from './characters';
|
|
19
19
|
import type { AssistantCharacterData } from './formats/types';
|
|
20
|
+
import type { DecisionAsker } from './decisions';
|
|
20
21
|
import { type AssistantAway, type AssistantState, type BalloonApproval } from './state';
|
|
21
22
|
/**
|
|
22
23
|
* Where the balloon goes so that it stays inside the window (T-23): toward
|
|
@@ -86,6 +87,11 @@ export interface AssistantStageProps {
|
|
|
86
87
|
* steps aside for, nor is the pointer working there (T-27).
|
|
87
88
|
*/
|
|
88
89
|
ownRef?: RefObject<HTMLElement | null>;
|
|
90
|
+
/**
|
|
91
|
+
* Asks a typed decision of the runtime: the balloon offers *Ask a
|
|
92
|
+
* decision*, and stays while its form or its answer is on screen.
|
|
93
|
+
*/
|
|
94
|
+
decide?: DecisionAsker;
|
|
89
95
|
}
|
|
90
|
-
export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, }: AssistantStageProps): JSX.Element;
|
|
96
|
+
export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, decide, }: AssistantStageProps): JSX.Element;
|
|
91
97
|
export default AssistantStage;
|
|
@@ -180,7 +180,7 @@ export function useKeepClear(stageRef, own, paused) {
|
|
|
180
180
|
}, [stageRef, own, paused]);
|
|
181
181
|
return obstructed ? 'obstacle' : pointerBusy ? 'pointer' : undefined;
|
|
182
182
|
}
|
|
183
|
-
export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, }) {
|
|
183
|
+
export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, }) {
|
|
184
184
|
// A shipped one by id, a drawing contributed by a plugin (T-24), or a
|
|
185
185
|
// character read from a file (T-26).
|
|
186
186
|
const shipped = typeof character === 'string'
|
|
@@ -199,7 +199,9 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
199
199
|
// Where the press began: a press that moves is a drag, not a click.
|
|
200
200
|
const pressedAt = useRef(null);
|
|
201
201
|
const aside = useKeepClear(stageRef, ownRef, menuOpen);
|
|
202
|
-
|
|
202
|
+
// A decision being asked, or its answer, keeps the balloon up.
|
|
203
|
+
const [deciding, setDeciding] = useState(false);
|
|
204
|
+
const showBalloon = !aside && !open && !!balloon && (hovered || insist || deciding);
|
|
203
205
|
return (_jsxs(Box, { ref: stageRef, "data-assistant-state": state, "data-assistant-aside": aside, sx: {
|
|
204
206
|
position: 'fixed',
|
|
205
207
|
zIndex: 1002,
|
|
@@ -277,7 +279,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
277
279
|
'@media (prefers-reduced-motion: reduce)': {
|
|
278
280
|
'& *': { animation: 'none !important' },
|
|
279
281
|
},
|
|
280
|
-
}, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
|
|
282
|
+
}, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, decide: decide, onDecisionActive: setDeciding, wide: deciding, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
|
|
281
283
|
pressedAt.current = { x: event.clientX, y: event.clientY };
|
|
282
284
|
onDragStart(event);
|
|
283
285
|
}, onClick: (event) => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* *Ask a decision* in the floating assistant's balloon: a small form — the
|
|
3
|
+
* situation, the question, its type (yes or no; a choice with its options
|
|
4
|
+
* listed; a score with its range) — asked of Jev through the runtime, and
|
|
5
|
+
* the answer said back in the balloon in plain words with its confidence.
|
|
6
|
+
*
|
|
7
|
+
* @module chat/assistant/DecisionAsk
|
|
8
|
+
*/
|
|
9
|
+
import type { JSX } from 'react';
|
|
10
|
+
import { type DecisionAsker } from './decisions';
|
|
11
|
+
/** The balloon's words for a decision. */
|
|
12
|
+
export declare const DECISION_WORDS: {
|
|
13
|
+
readonly ask: "Ask a decision";
|
|
14
|
+
readonly asking: "Asking…";
|
|
15
|
+
readonly submit: "Ask";
|
|
16
|
+
readonly cancel: "Cancel";
|
|
17
|
+
readonly again: "Ask another";
|
|
18
|
+
readonly types: {
|
|
19
|
+
noul: string;
|
|
20
|
+
choice: string;
|
|
21
|
+
score: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export interface DecisionAskProps {
|
|
25
|
+
/** Asks the runtime. */
|
|
26
|
+
ask: DecisionAsker;
|
|
27
|
+
/** The form or its answer is on screen: the balloon must stay. */
|
|
28
|
+
onActiveChange?: (active: boolean) => void;
|
|
29
|
+
}
|
|
30
|
+
export declare function DecisionAsk({ ask, onActiveChange, }: DecisionAskProps): JSX.Element;
|
|
31
|
+
export default DecisionAsk;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Button, FormControl, Select, Text, Textarea, TextInput, } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { DEFAULT_SCORE_RANGE, decisionAskProblem, decisionRequest, decisionResponseWords, } from './decisions';
|
|
6
|
+
/** The balloon's words for a decision. */
|
|
7
|
+
export const DECISION_WORDS = {
|
|
8
|
+
ask: 'Ask a decision',
|
|
9
|
+
asking: 'Asking…',
|
|
10
|
+
submit: 'Ask',
|
|
11
|
+
cancel: 'Cancel',
|
|
12
|
+
again: 'Ask another',
|
|
13
|
+
types: {
|
|
14
|
+
noul: 'Yes or no',
|
|
15
|
+
choice: 'A choice',
|
|
16
|
+
score: 'A score',
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
const LINK_SX = {
|
|
20
|
+
mt: 1,
|
|
21
|
+
p: 0,
|
|
22
|
+
border: 0,
|
|
23
|
+
bg: 'transparent',
|
|
24
|
+
color: 'accent.fg',
|
|
25
|
+
textDecoration: 'underline',
|
|
26
|
+
cursor: 'pointer',
|
|
27
|
+
fontSize: 1,
|
|
28
|
+
};
|
|
29
|
+
export function DecisionAsk({ ask, onActiveChange, }) {
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
const [situation, setSituation] = useState('');
|
|
32
|
+
const [question, setQuestion] = useState('');
|
|
33
|
+
const [name, setName] = useState('');
|
|
34
|
+
const [type, setType] = useState('noul');
|
|
35
|
+
const [options, setOptions] = useState('');
|
|
36
|
+
const [from, setFrom] = useState(String(DEFAULT_SCORE_RANGE.from));
|
|
37
|
+
const [to, setTo] = useState(String(DEFAULT_SCORE_RANGE.to));
|
|
38
|
+
const [problem, setProblem] = useState();
|
|
39
|
+
const [asking, setAsking] = useState(false);
|
|
40
|
+
const [answer, setAnswer] = useState();
|
|
41
|
+
// Gone from the screen — the conversation opened, the balloon closed — it
|
|
42
|
+
// no longer holds the balloon up.
|
|
43
|
+
const activeChange = useRef(onActiveChange);
|
|
44
|
+
activeChange.current = onActiveChange;
|
|
45
|
+
useEffect(() => () => activeChange.current?.(false), []);
|
|
46
|
+
const show = (active) => {
|
|
47
|
+
setOpen(active);
|
|
48
|
+
onActiveChange?.(active);
|
|
49
|
+
};
|
|
50
|
+
const close = () => {
|
|
51
|
+
setAnswer(undefined);
|
|
52
|
+
setProblem(undefined);
|
|
53
|
+
show(false);
|
|
54
|
+
};
|
|
55
|
+
if (!open) {
|
|
56
|
+
return (_jsx(Box, { as: "button", type: "button", "data-balloon-ask-decision": "", onClick: () => show(true), sx: { ...LINK_SX, display: 'block' }, children: DECISION_WORDS.ask }));
|
|
57
|
+
}
|
|
58
|
+
if (answer !== undefined) {
|
|
59
|
+
return (_jsxs(Box, { "data-balloon-decision": "answered", sx: { mt: 2 }, children: [_jsx(Text, { as: "p", "data-decision-answer": "", sx: { m: 0, fontWeight: 'semibold' }, children: answer }), _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", onClick: () => setAnswer(undefined), children: DECISION_WORDS.again }), _jsx(Button, { size: "small", variant: "invisible", onClick: close, children: DECISION_WORDS.cancel })] })] }));
|
|
60
|
+
}
|
|
61
|
+
const submit = async () => {
|
|
62
|
+
const asked = {
|
|
63
|
+
situation,
|
|
64
|
+
question,
|
|
65
|
+
type,
|
|
66
|
+
name,
|
|
67
|
+
options: options.split('\n'),
|
|
68
|
+
range: { from: Number(from), to: Number(to) },
|
|
69
|
+
};
|
|
70
|
+
const refused = decisionAskProblem(asked);
|
|
71
|
+
setProblem(refused);
|
|
72
|
+
if (refused) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const request = decisionRequest(asked);
|
|
76
|
+
setAsking(true);
|
|
77
|
+
try {
|
|
78
|
+
setAnswer(decisionResponseWords(request, await ask(request)));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
setAnswer(`Nothing was decided: ${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
setAsking(false);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return (_jsxs(Box, { as: "form", "data-balloon-decision": "asking", "aria-label": DECISION_WORDS.ask, onSubmit: (event) => {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
void submit();
|
|
90
|
+
}, onKeyDown: (event) => {
|
|
91
|
+
if (event.key === 'Escape') {
|
|
92
|
+
close();
|
|
93
|
+
}
|
|
94
|
+
}, sx: { mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Situation" }), _jsx(Textarea, { block: true, rows: 3, resize: "vertical", name: "situation", placeholder: "Help! My payouts have been failing for 3 days.", value: situation, onChange: event => setSituation(event.target.value) })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Question" }), _jsx(TextInput, { block: true, size: "small", name: "question", placeholder: "Is it urgent?", value: question, onChange: event => setQuestion(event.target.value) })] }), _jsxs(Box, { sx: { display: 'flex', gap: 2 }, children: [_jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "Type" }), _jsx(Select, { block: true, size: "small", name: "type", value: type, onChange: event => setType(event.target.value), children: Object.keys(DECISION_WORDS.types).map(option => (_jsx(Select.Option, { value: option, children: DECISION_WORDS.types[option] }, option))) })] }), _jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "Name" }), _jsx(TextInput, { block: true, size: "small", name: "name", placeholder: "Answer", value: name, onChange: event => setName(event.target.value) })] })] }), type === 'choice' && (_jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Options, one per line" }), _jsx(Textarea, { block: true, rows: 3, resize: "vertical", name: "options", placeholder: 'Billing\nTech\nSales', value: options, onChange: event => setOptions(event.target.value) })] })), type === 'score' && (_jsxs(Box, { sx: { display: 'flex', gap: 2 }, children: [_jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "From" }), _jsx(TextInput, { block: true, size: "small", type: "number", name: "from", value: from, onChange: event => setFrom(event.target.value) })] }), _jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "To" }), _jsx(TextInput, { block: true, size: "small", type: "number", name: "to", value: to, onChange: event => setTo(event.target.value) })] })] })), problem ? (_jsx(Text, { as: "p", role: "alert", sx: { m: 0, color: 'danger.fg', fontSize: 0 }, children: problem })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", type: "submit", disabled: asking, children: asking ? DECISION_WORDS.asking : DECISION_WORDS.submit }), _jsx(Button, { size: "small", type: "button", onClick: close, disabled: asking, children: DECISION_WORDS.cancel })] })] }));
|
|
95
|
+
}
|
|
96
|
+
export default DecisionAsk;
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* assistant's character, or the floating popup's button while the chat is
|
|
4
4
|
* closed: the agent's newest words, as the Office Assistant said them
|
|
5
5
|
* (LOOP T-23), with *Open the conversation* when there is more — and an
|
|
6
|
-
* approval it waits on, answered there with *Approve* or *Deny
|
|
6
|
+
* approval it waits on, answered there with *Approve* or *Deny* — and, where
|
|
7
|
+
* the runtime can be asked, *Ask a decision*: a typed decision asked of Jev,
|
|
8
|
+
* its answer said back in the balloon.
|
|
7
9
|
*
|
|
8
10
|
* @module chat/assistant/SpeechBalloon
|
|
9
11
|
*/
|
|
10
12
|
import type { JSX } from 'react';
|
|
11
13
|
import { type BalloonApproval } from './state';
|
|
14
|
+
import type { DecisionAsker } from './decisions';
|
|
12
15
|
export interface SpeechBalloonProps {
|
|
13
16
|
text: string;
|
|
14
17
|
/** There is more than the balloon holds. */
|
|
@@ -25,6 +28,12 @@ export interface SpeechBalloonProps {
|
|
|
25
28
|
tailAt: number;
|
|
26
29
|
/** An approval it waits on: what is asked, and the two answers. */
|
|
27
30
|
approval?: BalloonApproval;
|
|
31
|
+
/** Asks a typed decision of the runtime: offers *Ask a decision*. */
|
|
32
|
+
decide?: DecisionAsker;
|
|
33
|
+
/** The decision's form or answer is on screen: it stays, and is wider. */
|
|
34
|
+
onDecisionActive?: (active: boolean) => void;
|
|
35
|
+
/** Wider, for the decision's form. */
|
|
36
|
+
wide?: boolean;
|
|
28
37
|
}
|
|
29
|
-
export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, }: SpeechBalloonProps): JSX.Element;
|
|
38
|
+
export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, }: SpeechBalloonProps): JSX.Element;
|
|
30
39
|
export default SpeechBalloon;
|
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Button, Text } from '@primer/react';
|
|
3
3
|
import { Box } from '@datalayer/primer-addons';
|
|
4
4
|
import { ASSISTANT_WORDS } from './state';
|
|
5
|
-
|
|
5
|
+
import { DecisionAsk } from './DecisionAsk';
|
|
6
|
+
export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, }) {
|
|
6
7
|
return (_jsxs(Box, { role: "status", "aria-live": "polite", "data-speech-balloon": "", sx: {
|
|
7
8
|
position: 'absolute',
|
|
8
9
|
// Pixels, as strings: a number here is read as the theme's space
|
|
@@ -11,8 +12,8 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
|
|
|
11
12
|
? { bottom: `${above}px` }
|
|
12
13
|
: { top: `${above}px` }),
|
|
13
14
|
[align]: 0,
|
|
14
|
-
maxWidth: 280,
|
|
15
|
-
width: 'max-content',
|
|
15
|
+
maxWidth: wide ? 300 : 280,
|
|
16
|
+
width: wide ? 300 : 'max-content',
|
|
16
17
|
px: 3,
|
|
17
18
|
py: 2,
|
|
18
19
|
bg: 'canvas.default',
|
|
@@ -37,7 +38,7 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
|
|
|
37
38
|
borderColor: 'border.default',
|
|
38
39
|
transform: side === 'above' ? 'rotate(45deg)' : 'rotate(225deg)',
|
|
39
40
|
},
|
|
40
|
-
}, children: [_jsx(Text, { as: "p", sx: { m: 0 }, children: text }), approval && (_jsxs(Box, { "data-balloon-approval": approval.id, sx: { mt: 1 }, children: [_jsx(Text, { as: "p", sx: { m: 0, fontWeight: 'semibold' }, children: approval.asks }), approval.why ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: approval.why })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approval.deciding, onClick: approval.onApprove, children: ASSISTANT_WORDS.approve }), _jsx(Button, { size: "small", disabled: approval.deciding, onClick: approval.onDeny, children: ASSISTANT_WORDS.deny })] }), approval.others > 0 ? (_jsx(Text, { as: "p", sx: { m: 0, mt: 1, color: 'fg.muted', fontSize: 0 }, children: `${approval.others} more waiting.` })) : null] })), more && (_jsx(Box, { as: "button", type: "button", onClick: onOpen, sx: {
|
|
41
|
+
}, children: [_jsx(Text, { as: "p", sx: { m: 0 }, children: text }), approval && (_jsxs(Box, { "data-balloon-approval": approval.id, sx: { mt: 1 }, children: [_jsx(Text, { as: "p", sx: { m: 0, fontWeight: 'semibold' }, children: approval.asks }), approval.why ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: approval.why })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approval.deciding, onClick: approval.onApprove, children: ASSISTANT_WORDS.approve }), _jsx(Button, { size: "small", disabled: approval.deciding, onClick: approval.onDeny, children: ASSISTANT_WORDS.deny })] }), approval.others > 0 ? (_jsx(Text, { as: "p", sx: { m: 0, mt: 1, color: 'fg.muted', fontSize: 0 }, children: `${approval.others} more waiting.` })) : null] })), decide && _jsx(DecisionAsk, { ask: decide, onActiveChange: onDecisionActive }), more && (_jsx(Box, { as: "button", type: "button", onClick: onOpen, sx: {
|
|
41
42
|
mt: 1,
|
|
42
43
|
p: 0,
|
|
43
44
|
border: 0,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decision asked from the floating assistant's balloon: a situation, a
|
|
3
|
+
* question about it and its type — yes or no, a choice among options, a score
|
|
4
|
+
* on a range — asked of Jev, the typed-decision model, through the runtime's
|
|
5
|
+
* `POST /api/v1/configure/inference/decisions`: the route that asks as the
|
|
6
|
+
* `decide` tool does, with the token the runtime calls its models with.
|
|
7
|
+
*
|
|
8
|
+
* The answer comes back in plain words with its confidence: "Urgent: yes
|
|
9
|
+
* (0.87)", "Team: Billing (0.94)", "Score: 4 of 5 (0.5)".
|
|
10
|
+
*
|
|
11
|
+
* @module chat/assistant/decisions
|
|
12
|
+
*/
|
|
13
|
+
/** The three types of a question, as ai-inference names them. */
|
|
14
|
+
export type DecisionType = 'noul' | 'choice' | 'score';
|
|
15
|
+
/** What the balloon's form asks. */
|
|
16
|
+
export interface DecisionAsk {
|
|
17
|
+
/** The text the question is about: a ticket, a message, a review. */
|
|
18
|
+
situation: string;
|
|
19
|
+
/** The question: a statement (yes or no) or a question (choice, score). */
|
|
20
|
+
question: string;
|
|
21
|
+
/** Its type. */
|
|
22
|
+
type: DecisionType;
|
|
23
|
+
/** A short name the answer is said under ("Urgent", "Team"). */
|
|
24
|
+
name?: string;
|
|
25
|
+
/** A choice's options, at least two. */
|
|
26
|
+
options?: string[];
|
|
27
|
+
/** A score's range, lowest first: from 1 to 5 by default. */
|
|
28
|
+
range?: {
|
|
29
|
+
from: number;
|
|
30
|
+
to: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** One question as the runtime's route takes it (`DecisionQuestion`). */
|
|
34
|
+
export interface DecisionQuestionBody {
|
|
35
|
+
name: string;
|
|
36
|
+
type: DecisionType;
|
|
37
|
+
instructions: string;
|
|
38
|
+
options: string[];
|
|
39
|
+
}
|
|
40
|
+
/** The body of `POST /configure/inference/decisions`. */
|
|
41
|
+
export interface DecisionsRequestBody {
|
|
42
|
+
state: string;
|
|
43
|
+
questions: DecisionQuestionBody[];
|
|
44
|
+
}
|
|
45
|
+
/** One typed answer, as ai-inference gives it. */
|
|
46
|
+
export interface DecisionAnswer {
|
|
47
|
+
type?: DecisionType;
|
|
48
|
+
/** noul: the probability that the statement holds. */
|
|
49
|
+
probability?: number;
|
|
50
|
+
/** choice: the option chosen. */
|
|
51
|
+
choice?: string;
|
|
52
|
+
/** score: the step, from 0. */
|
|
53
|
+
score?: number;
|
|
54
|
+
confidence?: number;
|
|
55
|
+
probabilities?: Record<string, number>;
|
|
56
|
+
/** score: each step's label, by its index. */
|
|
57
|
+
legend?: Record<string, string>;
|
|
58
|
+
}
|
|
59
|
+
/** What the route answers: the answers by name, or why nothing was decided. */
|
|
60
|
+
export interface DecisionsResponseBody {
|
|
61
|
+
model?: string;
|
|
62
|
+
answers?: Record<string, DecisionAnswer>;
|
|
63
|
+
refusal?: string;
|
|
64
|
+
}
|
|
65
|
+
/** The balloon's asker: a request in, the route's answer out. */
|
|
66
|
+
export type DecisionAsker = (body: DecisionsRequestBody) => Promise<DecisionsResponseBody>;
|
|
67
|
+
/** The name an answer is said under when the form gave none. */
|
|
68
|
+
export declare const DEFAULT_DECISION_NAME = "Answer";
|
|
69
|
+
/** The default range of a score. */
|
|
70
|
+
export declare const DEFAULT_SCORE_RANGE: {
|
|
71
|
+
readonly from: 1;
|
|
72
|
+
readonly to: 5;
|
|
73
|
+
};
|
|
74
|
+
/** The steps of a score's range, lowest first: `1..5` is `["1", …, "5"]`. */
|
|
75
|
+
export declare function scoreSteps(range: {
|
|
76
|
+
from: number;
|
|
77
|
+
to: number;
|
|
78
|
+
}): string[];
|
|
79
|
+
/**
|
|
80
|
+
* Why the form cannot be asked as it is, in a sentence, or `undefined`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function decisionAskProblem(ask: DecisionAsk): string | undefined;
|
|
83
|
+
/** The form's ask as the runtime's route takes it: one named question. */
|
|
84
|
+
export declare function decisionRequest(ask: DecisionAsk): DecisionsRequestBody;
|
|
85
|
+
/** A probability or a confidence as said: two places at most, `0.5`, `0.87`. */
|
|
86
|
+
export declare function confidenceWords(value: number): string;
|
|
87
|
+
/**
|
|
88
|
+
* One answer in plain words with its confidence: "Urgent: yes (0.87)" — a
|
|
89
|
+
* no is said with the probability that it does not hold — "Team: Billing
|
|
90
|
+
* (0.94)", "Score: 4 of 5 (0.5)" — a rubric of words by its step,
|
|
91
|
+
* "Severity: blocking (0.7)".
|
|
92
|
+
*/
|
|
93
|
+
export declare function decisionAnswerWords(name: string, answer: DecisionAnswer, options?: readonly string[]): string;
|
|
94
|
+
/**
|
|
95
|
+
* What the route answered, in the balloon's words: the one answer, or the
|
|
96
|
+
* sentence saying why nothing was decided.
|
|
97
|
+
*/
|
|
98
|
+
export declare function decisionResponseWords(request: DecisionsRequestBody, response: DecisionsResponseBody): string;
|
|
99
|
+
/**
|
|
100
|
+
* Ask at a runtime: `POST {serverUrl}/api/v1/configure/inference/decisions`,
|
|
101
|
+
* with the person's token when there is one. A refusal of the route itself
|
|
102
|
+
* comes back as a sentence, never a throw.
|
|
103
|
+
*/
|
|
104
|
+
export declare function decisionsAskerAt(serverUrl: string, token?: string): DecisionAsker;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The name an answer is said under when the form gave none. */
|
|
6
|
+
export const DEFAULT_DECISION_NAME = 'Answer';
|
|
7
|
+
/** The default range of a score. */
|
|
8
|
+
export const DEFAULT_SCORE_RANGE = { from: 1, to: 5 };
|
|
9
|
+
/** The longest range a score may have, in steps. */
|
|
10
|
+
const MAX_SCORE_STEPS = 11;
|
|
11
|
+
/** The steps of a score's range, lowest first: `1..5` is `["1", …, "5"]`. */
|
|
12
|
+
export function scoreSteps(range) {
|
|
13
|
+
const steps = [];
|
|
14
|
+
for (let step = range.from; step <= range.to; step += 1) {
|
|
15
|
+
steps.push(String(step));
|
|
16
|
+
}
|
|
17
|
+
return steps;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Why the form cannot be asked as it is, in a sentence, or `undefined`.
|
|
21
|
+
*/
|
|
22
|
+
export function decisionAskProblem(ask) {
|
|
23
|
+
if (!ask.situation.trim()) {
|
|
24
|
+
return 'Say the situation the question is about.';
|
|
25
|
+
}
|
|
26
|
+
if (!ask.question.trim()) {
|
|
27
|
+
return 'Ask a question.';
|
|
28
|
+
}
|
|
29
|
+
if (ask.type === 'choice') {
|
|
30
|
+
const options = (ask.options ?? []).filter(option => option.trim());
|
|
31
|
+
if (options.length < 2) {
|
|
32
|
+
return 'A choice needs at least two options.';
|
|
33
|
+
}
|
|
34
|
+
if (new Set(options.map(option => option.trim())).size !== options.length) {
|
|
35
|
+
return 'Two options are the same.';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (ask.type === 'score') {
|
|
39
|
+
const range = ask.range ?? DEFAULT_SCORE_RANGE;
|
|
40
|
+
if (!Number.isInteger(range.from) || !Number.isInteger(range.to)) {
|
|
41
|
+
return 'A score goes from a whole number to another.';
|
|
42
|
+
}
|
|
43
|
+
if (range.to <= range.from) {
|
|
44
|
+
return 'A score goes from a lower number to a higher one.';
|
|
45
|
+
}
|
|
46
|
+
if (range.to - range.from + 1 > MAX_SCORE_STEPS) {
|
|
47
|
+
return `A score has at most ${MAX_SCORE_STEPS} steps.`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/** The form's ask as the runtime's route takes it: one named question. */
|
|
53
|
+
export function decisionRequest(ask) {
|
|
54
|
+
const options = ask.type === 'choice'
|
|
55
|
+
? (ask.options ?? []).map(option => option.trim()).filter(Boolean)
|
|
56
|
+
: ask.type === 'score'
|
|
57
|
+
? scoreSteps(ask.range ?? DEFAULT_SCORE_RANGE)
|
|
58
|
+
: [];
|
|
59
|
+
return {
|
|
60
|
+
state: ask.situation.trim(),
|
|
61
|
+
questions: [
|
|
62
|
+
{
|
|
63
|
+
name: ask.name?.trim() || DEFAULT_DECISION_NAME,
|
|
64
|
+
type: ask.type,
|
|
65
|
+
instructions: ask.question.trim(),
|
|
66
|
+
options,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** A probability or a confidence as said: two places at most, `0.5`, `0.87`. */
|
|
72
|
+
export function confidenceWords(value) {
|
|
73
|
+
return String(Number(value.toFixed(2)));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One answer in plain words with its confidence: "Urgent: yes (0.87)" — a
|
|
77
|
+
* no is said with the probability that it does not hold — "Team: Billing
|
|
78
|
+
* (0.94)", "Score: 4 of 5 (0.5)" — a rubric of words by its step,
|
|
79
|
+
* "Severity: blocking (0.7)".
|
|
80
|
+
*/
|
|
81
|
+
export function decisionAnswerWords(name, answer, options = []) {
|
|
82
|
+
if (typeof answer.probability === 'number' && answer.choice === undefined) {
|
|
83
|
+
const holds = answer.probability >= 0.5;
|
|
84
|
+
return `${name}: ${holds ? 'yes' : 'no'} (${confidenceWords(holds ? answer.probability : 1 - answer.probability)})`;
|
|
85
|
+
}
|
|
86
|
+
const confidence = typeof answer.confidence === 'number'
|
|
87
|
+
? ` (${confidenceWords(answer.confidence)})`
|
|
88
|
+
: '';
|
|
89
|
+
if (typeof answer.score === 'number') {
|
|
90
|
+
const steps = options.length
|
|
91
|
+
? options
|
|
92
|
+
: Object.keys(answer.legend ?? {})
|
|
93
|
+
.sort((a, b) => Number(a) - Number(b))
|
|
94
|
+
.map(key => answer.legend?.[key] ?? key);
|
|
95
|
+
const said = answer.legend?.[String(answer.score)] ?? steps[answer.score];
|
|
96
|
+
const last = steps[steps.length - 1];
|
|
97
|
+
// A range of numbers says where on it ("4 of 5"); a rubric of words says
|
|
98
|
+
// its step ("blocking").
|
|
99
|
+
const numeric = steps.length > 0 && steps.every(step => /^-?\d+$/.test(step.trim()));
|
|
100
|
+
return `${name}: ${said ?? answer.score + 1}${numeric && last !== undefined ? ` of ${last}` : ''}${confidence}`;
|
|
101
|
+
}
|
|
102
|
+
if (typeof answer.choice === 'string') {
|
|
103
|
+
return `${name}: ${answer.choice}${confidence}`;
|
|
104
|
+
}
|
|
105
|
+
return `${name}: no answer`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* What the route answered, in the balloon's words: the one answer, or the
|
|
109
|
+
* sentence saying why nothing was decided.
|
|
110
|
+
*/
|
|
111
|
+
export function decisionResponseWords(request, response) {
|
|
112
|
+
if (response.refusal) {
|
|
113
|
+
return response.refusal;
|
|
114
|
+
}
|
|
115
|
+
const [question] = request.questions;
|
|
116
|
+
const answer = response.answers?.[question.name];
|
|
117
|
+
if (!answer) {
|
|
118
|
+
return 'Nothing was decided: the answer did not come back.';
|
|
119
|
+
}
|
|
120
|
+
return decisionAnswerWords(question.name, answer, question.options);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Ask at a runtime: `POST {serverUrl}/api/v1/configure/inference/decisions`,
|
|
124
|
+
* with the person's token when there is one. A refusal of the route itself
|
|
125
|
+
* comes back as a sentence, never a throw.
|
|
126
|
+
*/
|
|
127
|
+
export function decisionsAskerAt(serverUrl, token) {
|
|
128
|
+
const base = serverUrl.replace(/\/+$/, '');
|
|
129
|
+
return async (body) => {
|
|
130
|
+
let response;
|
|
131
|
+
try {
|
|
132
|
+
response = await fetch(`${base}/api/v1/configure/inference/decisions`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify(body),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return {
|
|
143
|
+
refusal: 'Nothing was decided: the runtime could not be reached.',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
let detail = '';
|
|
148
|
+
try {
|
|
149
|
+
const said = (await response.json());
|
|
150
|
+
detail = typeof said.detail === 'string' ? said.detail : '';
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// No body worth saying.
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
refusal: `Nothing was decided: the runtime refused it (${response.status})${detail ? `: ${detail}` : '.'}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return (await response.json());
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { MCP_SERVER_LIBRARY } from '../../specs/mcpServers';
|
|
17
17
|
import { SKILLS_CATALOG } from '../../specs/skills';
|
|
18
18
|
import { FRONTEND_TOOL_CATALOG } from '../../specs/frontendTools';
|
|
19
|
-
import {
|
|
19
|
+
import { BACKEND_TOOL_CATALOG } from '../../specs/backendTools';
|
|
20
20
|
/** The tools the skills area runs a skill through. */
|
|
21
21
|
export const SKILL_TOOLS = new Set([
|
|
22
22
|
'run_skill_script',
|
|
@@ -57,7 +57,7 @@ function frontendTools() {
|
|
|
57
57
|
function runtimeTools() {
|
|
58
58
|
if (!runtimeIndex) {
|
|
59
59
|
runtimeIndex = new Map();
|
|
60
|
-
for (const spec of Object.values(
|
|
60
|
+
for (const spec of Object.values(BACKEND_TOOL_CATALOG)) {
|
|
61
61
|
const method = spec.runtime?.method;
|
|
62
62
|
if (method && !runtimeIndex.has(method)) {
|
|
63
63
|
runtimeIndex.set(method, { id: spec.id, ...marksOf(spec) });
|
|
@@ -19,7 +19,8 @@ export interface LibraryAgentspec {
|
|
|
19
19
|
icon?: string | null;
|
|
20
20
|
color?: string | null;
|
|
21
21
|
skills: string[];
|
|
22
|
-
tools
|
|
22
|
+
/** Backend tools (agentspecs/backend-tools), as the runtime serves them. */
|
|
23
|
+
backendTools?: string[];
|
|
23
24
|
systemPrompt?: string | null;
|
|
24
25
|
systemPromptCodemodeAddons?: string | null;
|
|
25
26
|
suggestions: string[];
|
|
@@ -477,10 +477,10 @@ export const AgentConfiguration = ({ agentLibrary, protocol: transport, extensio
|
|
|
477
477
|
.map(selection => normalizeMcpServerId(selection.id));
|
|
478
478
|
}, [selectedMcpServers, catalogServerIdSet]);
|
|
479
479
|
const resolvedSpecTools = useMemo(() => {
|
|
480
|
-
if (!activeSpec || !Array.isArray(activeSpec.
|
|
480
|
+
if (!activeSpec || !Array.isArray(activeSpec.backendTools)) {
|
|
481
481
|
return [];
|
|
482
482
|
}
|
|
483
|
-
return activeSpec.
|
|
483
|
+
return activeSpec.backendTools
|
|
484
484
|
.map(tool => normalizeToolLabel(tool))
|
|
485
485
|
.filter((tool) => !!tool);
|
|
486
486
|
}, [activeSpec]);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide: typed decisions asked of Jev (`cloudflare:wrk/typesafe/jev`), the
|
|
3
|
+
* typed-decision model, from the chat and from the floating assistant.
|
|
4
|
+
*
|
|
5
|
+
* The *Decide* application of the catalogue (agentspecs `apps/decide.yaml`)
|
|
6
|
+
* in the LOOP workspace, on the Local target: its agent — `example-simple`
|
|
7
|
+
* with the `decide` tool — runs on the local agent-runtimes server, and
|
|
8
|
+
* answers a question about a text by asking a typed decision. The floating
|
|
9
|
+
* assistant beside it offers *Ask a decision* in its balloon: the situation,
|
|
10
|
+
* the question and its type, asked through the same server's
|
|
11
|
+
* `/api/v1/configure/inference/decisions`, the route that asks as `decide`
|
|
12
|
+
* does, with the same token.
|
|
13
|
+
*
|
|
14
|
+
* The server needs ai-inference: start it with
|
|
15
|
+
* `AGENT_RUNTIMES_INFERENCE_PROVIDER_OVERRIDE=datalayer`,
|
|
16
|
+
* `DATALAYER_AI_INFERENCE_URL=https://r1.datalayer.run` and
|
|
17
|
+
* `DATALAYER_AI_INFERENCE_API_KEY` set from `DATALAYER_API_KEY`. Another
|
|
18
|
+
* server than `http://localhost:8765` is named with `?agentRuntimesUrl=`.
|
|
19
|
+
*
|
|
20
|
+
* @module examples/AgentDecideExample
|
|
21
|
+
*/
|
|
22
|
+
import React from 'react';
|
|
23
|
+
declare const AgentDecideExample: React.FC;
|
|
24
|
+
export default AgentDecideExample;
|