@datalayer/agent-runtimes 1.3.74 → 1.3.78
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/agents/AgentDetails.d.ts +16 -2
- package/lib/agents/AgentDetails.js +381 -272
- package/lib/chat/ChatFloating.d.ts +27 -1
- package/lib/chat/ChatFloating.js +97 -13
- package/lib/chat/assistant/AgentDetailsDialog.d.ts +21 -0
- package/lib/chat/assistant/AgentDetailsDialog.js +19 -0
- package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
- package/lib/chat/assistant/AssistantContextMenu.js +68 -0
- package/lib/chat/assistant/AssistantStage.d.ts +144 -4
- package/lib/chat/assistant/AssistantStage.js +280 -10
- package/lib/chat/assistant/BalloonParts.d.ts +33 -5
- package/lib/chat/assistant/BalloonParts.js +66 -44
- package/lib/chat/assistant/BalloonVisual.d.ts +10 -2
- package/lib/chat/assistant/BalloonVisual.js +4 -3
- package/lib/chat/assistant/SandboxDetailsDialog.d.ts +44 -0
- package/lib/chat/assistant/SandboxDetailsDialog.js +214 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +54 -6
- package/lib/chat/assistant/SpeechBalloon.js +73 -22
- package/lib/chat/assistant/assistantDetails.d.ts +84 -0
- package/lib/chat/assistant/assistantDetails.js +140 -0
- package/lib/chat/assistant/formats/files.d.ts +23 -0
- package/lib/chat/assistant/formats/files.js +48 -0
- package/lib/chat/assistant/formats/index.d.ts +2 -0
- package/lib/chat/assistant/formats/index.js +1 -0
- package/lib/chat/assistant/state.d.ts +7 -14
- package/lib/chat/assistant/state.js +21 -20
- package/lib/chat/assistant/toolLine.d.ts +16 -0
- package/lib/chat/assistant/toolLine.js +46 -0
- package/lib/chat/base/ChatBase.js +71 -1
- package/lib/chat/base/loopMessage.d.ts +37 -0
- package/lib/chat/base/loopMessage.js +37 -0
- package/lib/chat/indicators/TypingDots.d.ts +13 -0
- package/lib/chat/indicators/TypingDots.js +21 -0
- package/lib/chat/messages/ChatMarkdown.d.ts +16 -0
- package/lib/chat/messages/ChatMarkdown.js +18 -0
- package/lib/chat/messages/ChatMessageList.d.ts +8 -1
- package/lib/chat/messages/ChatMessageList.js +25 -33
- package/lib/chat/styles/streamdownStyles.js +3 -1
- package/lib/chat/tools/ToolCallDisplay.d.ts +8 -1
- package/lib/chat/tools/ToolCallDisplay.js +8 -7
- package/lib/components/code-sandboxes/CodeSandboxCellVariablesDialog.js +5 -11
- package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +8 -0
- package/lib/components/code-sandboxes/CodeSandboxVariables.js +17 -11
- package/lib/components/inspector/AgentInspector.d.ts +59 -0
- package/lib/components/inspector/AgentInspector.js +220 -0
- package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
- package/lib/components/inspector/AgentInspectorDialog.js +9 -0
- package/lib/components/inspector/JsonTree.d.ts +24 -0
- package/lib/components/inspector/JsonTree.js +89 -0
- package/lib/components/inspector/a2aInspect.d.ts +56 -0
- package/lib/components/inspector/a2aInspect.js +478 -0
- package/lib/components/inspector/agentInspector.d.ts +179 -0
- package/lib/components/inspector/agentInspector.js +267 -0
- package/lib/components/inspector/chatInspect.d.ts +38 -0
- package/lib/components/inspector/chatInspect.js +65 -0
- package/lib/components/inspector/classify.d.ts +19 -0
- package/lib/components/inspector/classify.js +42 -0
- package/lib/components/inspector/index.d.ts +14 -0
- package/lib/components/inspector/index.js +18 -0
- package/lib/components/teams/A2ATeamGraph.d.ts +69 -1
- package/lib/components/teams/A2ATeamGraph.js +277 -37
- package/lib/components/teams/NotebookPreviewView.js +9 -1
- package/lib/components/teams/TeamNotebook.d.ts +3 -1
- package/lib/components/teams/TeamNotebook.js +3 -3
- package/lib/components/teams/TeamNotebookView.js +15 -0
- package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
- package/lib/components/teams/jupyterThemeVariables.js +441 -0
- package/lib/components/teams/teamConnections.d.ts +9 -1
- package/lib/components/teams/teamConnections.js +20 -0
- package/lib/components/teams/teamNotebookKernel.d.ts +3 -0
- package/lib/components/teams/teamNotebookKernel.js +17 -0
- package/lib/components/teams/useA2ATeam.d.ts +20 -3
- package/lib/components/teams/useA2ATeam.js +108 -23
- package/lib/examples/AgentA2ATeamExample.d.ts +13 -0
- package/lib/examples/AgentA2ATeamExample.js +67 -13
- package/lib/examples/AssistantExample.js +65 -26
- package/lib/examples/AssistantGalleryExample.js +2 -1
- package/lib/examples/exampleGroups.d.ts +1 -1
- package/lib/examples/exampleGroups.js +1 -1
- package/lib/examples/utils/assistantGallery.d.ts +7 -6
- package/lib/examples/utils/assistantGallery.js +23 -8
- package/lib/jupyter/exec/Python.d.ts +0 -1
- package/lib/jupyter/exec/Python.js +0 -32
- package/lib/jupyter/exec/Snippets.d.ts +4 -12
- package/lib/jupyter/exec/Snippets.js +0 -8
- package/lib/loop/apps/AppFeedback.js +3 -1
- package/lib/loop/apps/AppKept.d.ts +22 -0
- package/lib/loop/apps/AppKept.js +36 -0
- package/lib/loop/apps/AppRenderer.d.ts +11 -1
- package/lib/loop/apps/AppRenderer.js +22 -4
- package/lib/loop/apps/checks.d.ts +7 -0
- package/lib/loop/apps/checks.js +39 -0
- package/lib/loop/apps/index.d.ts +3 -0
- package/lib/loop/apps/index.js +3 -0
- package/lib/loop/apps/kept.d.ts +30 -0
- package/lib/loop/apps/kept.js +52 -0
- package/lib/loop/apps/records.d.ts +12 -4
- package/lib/loop/apps/records.js +13 -5
- package/lib/loop/apps/saved.d.ts +30 -0
- package/lib/loop/apps/saved.js +40 -0
- package/lib/loop/core/index.d.ts +17 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/embed/AppEmbed.d.ts +8 -1
- package/lib/loop/embed/AppEmbed.js +4 -4
- package/lib/loop/embed/LoopEmbed.d.ts +6 -1
- package/lib/loop/embed/LoopEmbed.js +4 -2
- package/lib/loop/embed/element.js +1 -1
- package/lib/loop/plugins/agents/DatalayerAgentBridge.js +12 -6
- package/lib/loop/plugins/agents/browserService.d.ts +7 -1
- package/lib/loop/plugins/agents/browserService.js +4 -0
- package/lib/loop/plugins/agents/plugin.d.ts +16 -0
- package/lib/loop/plugins/agents/switchable.d.ts +3 -0
- package/lib/loop/plugins/agents/switchable.js +1 -0
- package/lib/loop/plugins/app-activity/index.js +3 -0
- package/lib/loop/plugins/app-computer/index.js +3 -0
- package/lib/loop/plugins/app-rules/AppRulesCard.d.ts +5 -0
- package/lib/loop/plugins/app-rules/AppRulesCard.js +25 -6
- package/lib/loop/plugins/app-rules/index.js +3 -0
- package/lib/loop/plugins/assistant/index.d.ts +7 -2
- package/lib/loop/plugins/assistant/index.js +58 -4
- package/lib/loop/plugins/assistant-characters/index.d.ts +7 -0
- package/lib/loop/plugins/assistant-characters/index.js +17 -0
- package/lib/loop/plugins/chat/ChatView.js +4 -1
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +2 -1
- package/lib/loop/plugins/page-layout/SplitLayout.js +4 -0
- package/lib/loop/presets.d.ts +4 -0
- package/lib/loop/presets.js +3 -1
- package/lib/loop/shell/LoopWorkspace.d.ts +7 -0
- package/lib/loop/shell/LoopWorkspace.js +5 -4
- package/lib/loop/shell/SidebarRail.d.ts +53 -0
- package/lib/loop/shell/SidebarRail.js +74 -0
- package/lib/loop/shell/index.d.ts +2 -0
- package/lib/loop/shell/index.js +2 -0
- package/lib/loop/shell/paneMotion.d.ts +21 -0
- package/lib/loop/shell/paneMotion.js +37 -0
- package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
- package/lib/specs/apps.js +8 -5
- package/lib/specs/uiPlugins.js +529 -0
- package/lib/types/agentspecs.d.ts +5 -3
- package/lib/types/chat.d.ts +12 -0
- package/package.json +2 -2
- package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
- package/scripts/codegen/generate_ui_plugins.py +112 -2
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* with whatever `inference` holds — a signed-in person's token, or a
|
|
12
12
|
* visitor's trial key. Its one tool asks the peer (Accounting) with
|
|
13
13
|
* {@link a2aPeerTool}. What each of them does is kept as a persona — the
|
|
14
|
-
* state its character acts and what its balloon says —
|
|
15
|
-
*
|
|
14
|
+
* state its character acts and what its balloon says — the peer's last
|
|
15
|
+
* answer as the report, and the way
|
|
16
16
|
* the link carries a message as the flow ({@link flowAfter}). What the peer
|
|
17
17
|
* gives besides words — the formats the page accepts (`accept`), such as a
|
|
18
18
|
* Jupyter notebook — is kept as its artifacts, and its latest notebook kept
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
* peer calls on its connections (its MCP servers), told over A2A as it calls
|
|
21
21
|
* them, are kept as the calls running now ({@link callsAfter}).
|
|
22
22
|
*
|
|
23
|
+
* Given an Agent Inspector's sink (`inspector`), the entry's turns (its
|
|
24
|
+
* model, its tokens), its call to the peer, and what the peer's events were
|
|
25
|
+
* made of are recorded there; the A2A traffic itself is recorded by the
|
|
26
|
+
* peer's fetch (`connectA2APeer`'s `inspect`).
|
|
27
|
+
*
|
|
23
28
|
* What `A2ATeamGraph` draws, and what a page's composer sends.
|
|
24
29
|
*
|
|
25
30
|
* @module components/teams/useA2ATeam
|
|
@@ -41,7 +46,7 @@ export const NOTEBOOK_AND_WORDS = [
|
|
|
41
46
|
NOTEBOOK_MEDIA_TYPE,
|
|
42
47
|
'text/markdown',
|
|
43
48
|
];
|
|
44
|
-
/** What
|
|
49
|
+
/** What the peer gave, in words: its words, and each of its artifacts. */
|
|
45
50
|
export function answeredLine(artifacts) {
|
|
46
51
|
const besides = artifacts.map(artifact => artifact.mediaType === NOTEBOOK_MEDIA_TYPE
|
|
47
52
|
? `a notebook, ${artifact.name}`
|
|
@@ -61,7 +66,7 @@ export function balloonLine(text, length = 120) {
|
|
|
61
66
|
}
|
|
62
67
|
/** Run a team of two over A2A in the page. */
|
|
63
68
|
export function useA2ATeam(options) {
|
|
64
|
-
const { entry, peerApp, peer, inference, maxSteps = 6, peerConnections = NO_CONNECTIONS, } = options;
|
|
69
|
+
const { entry, peerApp, peer, inference, maxSteps = 6, peerConnections = NO_CONNECTIONS, inspector = null, } = options;
|
|
65
70
|
// By what it names, so that a page passing a new array of the same media
|
|
66
71
|
// types does not make the agent again.
|
|
67
72
|
const acceptKey = options.accept?.join('\n');
|
|
@@ -71,24 +76,56 @@ export function useA2ATeam(options) {
|
|
|
71
76
|
...AT_REST,
|
|
72
77
|
state: 'greeting',
|
|
73
78
|
saying: balloonLine(entry.interface.welcome ?? ''),
|
|
79
|
+
full: entry.interface.welcome ?? '',
|
|
74
80
|
insist: Boolean(entry.interface.welcome),
|
|
75
81
|
}));
|
|
76
82
|
const [peerPersona, setPeerPersona] = useState(AT_REST);
|
|
77
83
|
const [turns, setTurns] = useState([]);
|
|
84
|
+
const [peerHistory, setPeerHistory] = useState([]);
|
|
85
|
+
const told = useRef(0);
|
|
86
|
+
const tell = useCallback((role, text) => {
|
|
87
|
+
told.current += 1;
|
|
88
|
+
const message = {
|
|
89
|
+
id: `peer-${told.current}`,
|
|
90
|
+
role,
|
|
91
|
+
content: text,
|
|
92
|
+
createdAt: new Date(),
|
|
93
|
+
};
|
|
94
|
+
setPeerHistory(prev => [...prev, message]);
|
|
95
|
+
}, []);
|
|
96
|
+
// A tool the peer calls, as the chat holds one: added when it starts,
|
|
97
|
+
// updated when it ends.
|
|
98
|
+
const toolCalled = useCallback((step) => {
|
|
99
|
+
const id = step.id ?? step.name;
|
|
100
|
+
const call = {
|
|
101
|
+
id: `peer-tool:${id}`,
|
|
102
|
+
type: 'tool-call',
|
|
103
|
+
toolCallId: id,
|
|
104
|
+
toolName: toolOwnName(step.name, peerConnections),
|
|
105
|
+
args: {},
|
|
106
|
+
status: step.error ? 'error' : step.ended ? 'complete' : 'executing',
|
|
107
|
+
...(step.error ? { error: step.error } : {}),
|
|
108
|
+
};
|
|
109
|
+
setPeerHistory(prev => prev.some(item => item.id === call.id)
|
|
110
|
+
? prev.map(item => (item.id === call.id ? call : item))
|
|
111
|
+
: [...prev, call]);
|
|
112
|
+
}, [peerConnections]);
|
|
78
113
|
const [report, setReport] = useState(null);
|
|
79
114
|
const [artifacts, setArtifacts] = useState([]);
|
|
80
115
|
const [notebook, setNotebook] = useState(null);
|
|
81
|
-
const [exchange, setExchange] = useState([]);
|
|
82
116
|
const [flow, setFlow] = useState('still');
|
|
83
117
|
const [calls, setCalls] = useState([]);
|
|
84
118
|
const callsNow = useRef([]);
|
|
85
119
|
const callTimer = useRef(undefined);
|
|
86
120
|
const [busy, setBusy] = useState(false);
|
|
87
121
|
const history = useRef([]);
|
|
122
|
+
const turnCount = useRef(0);
|
|
88
123
|
const abort = useRef(null);
|
|
89
124
|
const flowTimer = useRef(undefined);
|
|
90
125
|
// What the peer does, as it does it: its own state and balloon, and the link.
|
|
91
126
|
const onPeerEvent = useCallback((event) => {
|
|
127
|
+
// Linked to the A2A entry it came from (the capture's linker).
|
|
128
|
+
inspector?.note(event);
|
|
92
129
|
const next = flowAfter(event);
|
|
93
130
|
clearTimeout(flowTimer.current);
|
|
94
131
|
setFlow(next.flow);
|
|
@@ -108,26 +145,25 @@ export function useA2ATeam(options) {
|
|
|
108
145
|
}, after.holdMs);
|
|
109
146
|
}
|
|
110
147
|
if (event.phase === 'asked') {
|
|
111
|
-
|
|
112
|
-
...prev,
|
|
113
|
-
`${entry.name} → ${peerApp.name}: ${event.request}`,
|
|
114
|
-
]);
|
|
148
|
+
tell('user', event.request);
|
|
115
149
|
setPeerPersona(prev => ({
|
|
116
150
|
...prev,
|
|
117
151
|
state: 'greeting',
|
|
118
152
|
saying: 'On it. Let me read the books.',
|
|
153
|
+
full: undefined,
|
|
119
154
|
insist: true,
|
|
120
155
|
tool: undefined,
|
|
121
156
|
}));
|
|
122
157
|
}
|
|
123
158
|
else if (event.phase === 'working') {
|
|
124
|
-
if (event.
|
|
125
|
-
|
|
159
|
+
if (event.tool) {
|
|
160
|
+
toolCalled(event.tool);
|
|
126
161
|
}
|
|
127
162
|
setPeerPersona(prev => ({
|
|
128
163
|
...prev,
|
|
129
164
|
state: 'working',
|
|
130
165
|
saying: event.note ? balloonLine(event.note) : prev.saying,
|
|
166
|
+
full: event.note ?? prev.full,
|
|
131
167
|
insist: Boolean(event.note) || Boolean(event.tool) || prev.insist,
|
|
132
168
|
// The tool it calls, in plain words, from the step it told; a
|
|
133
169
|
// step without a tool (its own words) puts the tool line away.
|
|
@@ -137,10 +173,7 @@ export function useA2ATeam(options) {
|
|
|
137
173
|
}));
|
|
138
174
|
}
|
|
139
175
|
else if (event.phase === 'answered') {
|
|
140
|
-
|
|
141
|
-
...prev,
|
|
142
|
-
`${peerApp.name} → ${entry.name}: ${answeredLine(event.artifacts)}`,
|
|
143
|
-
]);
|
|
176
|
+
tell('assistant', event.answer);
|
|
144
177
|
setReport(event.answer);
|
|
145
178
|
setArtifacts(event.artifacts);
|
|
146
179
|
const given = notebookAmong(event.artifacts);
|
|
@@ -151,6 +184,7 @@ export function useA2ATeam(options) {
|
|
|
151
184
|
...prev,
|
|
152
185
|
state: 'speaking',
|
|
153
186
|
saying: balloonLine(event.answer),
|
|
187
|
+
full: event.answer,
|
|
154
188
|
insist: true,
|
|
155
189
|
tool: undefined,
|
|
156
190
|
}));
|
|
@@ -160,19 +194,17 @@ export function useA2ATeam(options) {
|
|
|
160
194
|
}
|
|
161
195
|
}
|
|
162
196
|
else {
|
|
163
|
-
|
|
164
|
-
...prev,
|
|
165
|
-
`${peerApp.name} could not answer: ${event.error}`,
|
|
166
|
-
]);
|
|
197
|
+
tell('assistant', `Could not answer: ${event.error}`);
|
|
167
198
|
setPeerPersona(prev => ({
|
|
168
199
|
...prev,
|
|
169
200
|
state: 'idle',
|
|
170
201
|
saying: balloonLine(event.error),
|
|
202
|
+
full: event.error,
|
|
171
203
|
insist: true,
|
|
172
204
|
tool: undefined,
|
|
173
205
|
}));
|
|
174
206
|
}
|
|
175
|
-
}, [
|
|
207
|
+
}, [peerApp.id, peerConnections, inspector, tell, toolCalled]);
|
|
176
208
|
const agent = useMemo(() => {
|
|
177
209
|
if (!peer) {
|
|
178
210
|
return null;
|
|
@@ -198,7 +230,6 @@ export function useA2ATeam(options) {
|
|
|
198
230
|
setBusy(true);
|
|
199
231
|
setReport(null);
|
|
200
232
|
setArtifacts([]);
|
|
201
|
-
setExchange([]);
|
|
202
233
|
setTurns(prev => [...prev, { role: 'user', text: asked }]);
|
|
203
234
|
setEntryPersona({
|
|
204
235
|
...AT_REST,
|
|
@@ -210,6 +241,19 @@ export function useA2ATeam(options) {
|
|
|
210
241
|
history.current = [...history.current, { role: 'user', content: asked }];
|
|
211
242
|
abort.current = new AbortController();
|
|
212
243
|
let said = '';
|
|
244
|
+
// The turn, in the inspector: the entry's model, from the question to
|
|
245
|
+
// the answer, and its tokens.
|
|
246
|
+
turnCount.current += 1;
|
|
247
|
+
const turnKey = `turn:${entry.id}:${turnCount.current}`;
|
|
248
|
+
inspector?.start({
|
|
249
|
+
key: turnKey,
|
|
250
|
+
actor: entry.name,
|
|
251
|
+
source: 'model',
|
|
252
|
+
kind: 'turn',
|
|
253
|
+
name: entry.model || undefined,
|
|
254
|
+
summary: balloonLine(asked, 160),
|
|
255
|
+
payload: { prompt: asked, model: entry.model || undefined },
|
|
256
|
+
});
|
|
213
257
|
try {
|
|
214
258
|
const result = await agent.stream({
|
|
215
259
|
messages: history.current,
|
|
@@ -218,10 +262,21 @@ export function useA2ATeam(options) {
|
|
|
218
262
|
for await (const part of result.fullStream) {
|
|
219
263
|
if (part.type === 'tool-call' && part.toolName === askTool) {
|
|
220
264
|
const request = String(part.input?.request ?? '');
|
|
265
|
+
// Its tool runs here, in the page, and asks the peer over A2A.
|
|
266
|
+
inspector?.start({
|
|
267
|
+
key: `tool:${part.toolCallId}`,
|
|
268
|
+
actor: entry.name,
|
|
269
|
+
source: 'frontend-tool',
|
|
270
|
+
kind: 'tool-call',
|
|
271
|
+
name: askTool,
|
|
272
|
+
summary: balloonLine(request, 160),
|
|
273
|
+
payload: part.input,
|
|
274
|
+
});
|
|
221
275
|
setEntryPersona(prev => ({
|
|
222
276
|
...prev,
|
|
223
277
|
state: 'waiting',
|
|
224
278
|
saying: balloonLine(`${peerApp.name}, could you: ${request}`),
|
|
279
|
+
full: `${peerApp.name}, could you: ${request}`,
|
|
225
280
|
insist: true,
|
|
226
281
|
// Its one tool, in its own words: it asks its peer.
|
|
227
282
|
tool: {
|
|
@@ -234,10 +289,16 @@ export function useA2ATeam(options) {
|
|
|
234
289
|
}));
|
|
235
290
|
}
|
|
236
291
|
else if (part.type === 'tool-result' && part.toolName === askTool) {
|
|
292
|
+
const output = part.output;
|
|
293
|
+
inspector?.end(`tool:${part.toolCallId}`, {
|
|
294
|
+
result: part.output,
|
|
295
|
+
...(output?.error ? { error: String(output.error) } : {}),
|
|
296
|
+
});
|
|
237
297
|
setEntryPersona(prev => ({
|
|
238
298
|
...prev,
|
|
239
299
|
state: 'thinking',
|
|
240
300
|
saying: 'Thanks!',
|
|
301
|
+
full: undefined,
|
|
241
302
|
tool: undefined,
|
|
242
303
|
}));
|
|
243
304
|
// Accounting has handed its answer over: back at rest, its last
|
|
@@ -250,11 +311,18 @@ export function useA2ATeam(options) {
|
|
|
250
311
|
...prev,
|
|
251
312
|
state: 'speaking',
|
|
252
313
|
saying: balloonLine(said),
|
|
314
|
+
full: said,
|
|
253
315
|
insist: true,
|
|
254
316
|
tool: undefined,
|
|
255
317
|
}));
|
|
256
318
|
}
|
|
257
319
|
else if (part.type === 'tool-error' && part.toolName === askTool) {
|
|
320
|
+
inspector?.end(`tool:${part.toolCallId}`, {
|
|
321
|
+
status: 'failed',
|
|
322
|
+
error: part.error instanceof Error
|
|
323
|
+
? part.error.message
|
|
324
|
+
: String(part.error),
|
|
325
|
+
});
|
|
258
326
|
setEntryPersona(prev => ({
|
|
259
327
|
...prev,
|
|
260
328
|
state: 'thinking',
|
|
@@ -271,15 +339,24 @@ export function useA2ATeam(options) {
|
|
|
271
339
|
...history.current,
|
|
272
340
|
{ role: 'assistant', content: said },
|
|
273
341
|
];
|
|
342
|
+
if (inspector) {
|
|
343
|
+
const usage = await Promise.resolve(result.totalUsage).catch(() => undefined);
|
|
344
|
+
inspector.end(turnKey, {
|
|
345
|
+
result: { text: said, ...(usage ? { usage } : {}) },
|
|
346
|
+
summary: `${balloonLine(asked, 60)} → ${balloonLine(said, 80)}${usage?.totalTokens ? ` · ${usage.totalTokens} tokens` : ''}`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
274
349
|
setTurns(prev => [...prev, { role: 'assistant', text: said }]);
|
|
275
350
|
setEntryPersona(prev => ({
|
|
276
351
|
...prev,
|
|
277
352
|
state: 'idle',
|
|
278
353
|
saying: balloonLine(said),
|
|
354
|
+
full: said,
|
|
279
355
|
}));
|
|
280
356
|
}
|
|
281
357
|
catch (error) {
|
|
282
358
|
const message = error instanceof Error ? error.message : String(error);
|
|
359
|
+
inspector?.end(turnKey, { status: 'failed', error: message });
|
|
283
360
|
setTurns(prev => [
|
|
284
361
|
...prev,
|
|
285
362
|
{ role: 'assistant', text: `Something went wrong: ${message}` },
|
|
@@ -288,6 +365,7 @@ export function useA2ATeam(options) {
|
|
|
288
365
|
...prev,
|
|
289
366
|
state: 'idle',
|
|
290
367
|
saying: balloonLine(`Something went wrong: ${message}`),
|
|
368
|
+
full: `Something went wrong: ${message}`,
|
|
291
369
|
insist: true,
|
|
292
370
|
tool: undefined,
|
|
293
371
|
}));
|
|
@@ -303,7 +381,7 @@ export function useA2ATeam(options) {
|
|
|
303
381
|
? prev
|
|
304
382
|
: { ...prev, state: 'idle', tool: undefined });
|
|
305
383
|
}
|
|
306
|
-
}, [agent, busy, askTool, peerApp.name]);
|
|
384
|
+
}, [agent, busy, askTool, peerApp.name, entry, inspector]);
|
|
307
385
|
const stop = useCallback(() => abort.current?.abort(), []);
|
|
308
386
|
useEffect(() => () => {
|
|
309
387
|
abort.current?.abort();
|
|
@@ -312,13 +390,20 @@ export function useA2ATeam(options) {
|
|
|
312
390
|
}, []);
|
|
313
391
|
const setEntryAway = useCallback((away) => setEntryPersona(prev => ({ ...prev, away })), []);
|
|
314
392
|
const setPeerAway = useCallback((away) => setPeerPersona(prev => ({ ...prev, away })), []);
|
|
393
|
+
const entryHistory = useMemo(() => turns.map((turn, index) => ({
|
|
394
|
+
id: `turn-${index}`,
|
|
395
|
+
role: turn.role,
|
|
396
|
+
content: turn.text,
|
|
397
|
+
createdAt: new Date(0),
|
|
398
|
+
})), [turns]);
|
|
315
399
|
return {
|
|
400
|
+
entryHistory,
|
|
401
|
+
peerHistory,
|
|
316
402
|
entryPersona,
|
|
317
403
|
peerPersona,
|
|
318
404
|
setEntryAway,
|
|
319
405
|
setPeerAway,
|
|
320
406
|
turns,
|
|
321
|
-
exchange,
|
|
322
407
|
report,
|
|
323
408
|
artifacts,
|
|
324
409
|
notebook,
|
|
@@ -19,6 +19,19 @@
|
|
|
19
19
|
* list_invoices…" while Accounting calls Odoo. Then the report appears. The
|
|
20
20
|
* page's own state is `useA2ATeam`'s, which the landing's home page runs too.
|
|
21
21
|
*
|
|
22
|
+
* Under the graph, each member has its own Agent Inspector: Sales' — its
|
|
23
|
+
* model turns, its `ask_accounting` call, the A2A messages it sends and
|
|
24
|
+
* receives — and Accounting's — what it received over A2A, its Odoo MCP
|
|
25
|
+
* calls, `write_notebook`, its status updates and artifacts, as the A2A
|
|
26
|
+
* stream tells them. Both read one record (`useAgentInspector`), fed by the
|
|
27
|
+
* team (`useA2ATeam`'s `inspector`) and by the fetch the A2A client sends
|
|
28
|
+
* with (`inspectA2AFetch`); an A2A message shows in both, sent in one and
|
|
29
|
+
* received in the other. Right-click a member for its menu: *Inspect the
|
|
30
|
+
* agent…* opens its inspector over the page.
|
|
31
|
+
*
|
|
32
|
+
* Sales' suggestions — its Appspec's starters — are chips in its balloon
|
|
33
|
+
* while it waits for a question: one clicked is asked, as if typed.
|
|
34
|
+
*
|
|
22
35
|
* Sales accepts a Jupyter notebook from Accounting besides its words
|
|
23
36
|
* (`acceptedOutputModes`, `NOTEBOOK_AND_WORDS`). When Accounting gives one,
|
|
24
37
|
* Sales' balloon shows it read-only (`NotebookPreview`: no kernel), and it
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Button, Flash, Heading, Text, Textarea } from '@primer/react';
|
|
4
4
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -10,8 +10,9 @@ import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
|
|
|
10
10
|
import { useBrowserInference } from '../hooks/useBrowserInference';
|
|
11
11
|
import { readJwtClaims, tokenLifetimeMs, useAnonymousSessionStore, } from '../runtimes/browser/anonymousToken';
|
|
12
12
|
import { connectA2APeer } from '../runtimes/browser/a2aPeer';
|
|
13
|
-
import { A2ATeamGraph, NOTEBOOK_AND_WORDS, teamConnectionsOf, useA2ATeam, } from '../components/teams';
|
|
13
|
+
import { A2ATeamGraph, NOTEBOOK_AND_WORDS, teamConnectionsOf, teamToolClassifier, useA2ATeam, } from '../components/teams';
|
|
14
14
|
import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
|
|
15
|
+
import { AgentInspector, classifyToolCall, inspectA2AFetch, useAgentInspector, } from '../components/inspector';
|
|
15
16
|
import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
|
|
16
17
|
const TEAM = SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1;
|
|
17
18
|
const SALES = SALES_APP_0_0_1;
|
|
@@ -45,6 +46,8 @@ function AgentA2ATeam() {
|
|
|
45
46
|
}, [accountingKey]);
|
|
46
47
|
const [accountingKeyEnded, setAccountingKeyEnded] = useState(() => accountingKeyClock !== undefined &&
|
|
47
48
|
accountingKeyClock.expiresAt <= Date.now());
|
|
49
|
+
// One record of what both members do, each inspector showing its own.
|
|
50
|
+
const { sink: inspector } = useAgentInspector();
|
|
48
51
|
const [peer, setPeer] = useState(null);
|
|
49
52
|
const [peerError, setPeerError] = useState(null);
|
|
50
53
|
const [draft, setDraft] = useState('');
|
|
@@ -55,14 +58,24 @@ function AgentA2ATeam() {
|
|
|
55
58
|
let current = true;
|
|
56
59
|
setPeer(null);
|
|
57
60
|
setPeerError(null);
|
|
58
|
-
connectA2APeer({
|
|
61
|
+
connectA2APeer({
|
|
62
|
+
url: accountingUrl,
|
|
63
|
+
key: accountingKey,
|
|
64
|
+
// The A2A traffic, recorded: Sales sends, Accounting answers.
|
|
65
|
+
fetch: inspectA2AFetch(globalThis.fetch.bind(globalThis), {
|
|
66
|
+
sink: inspector,
|
|
67
|
+
asker: SALES.name,
|
|
68
|
+
peer: ACCOUNTING.name,
|
|
69
|
+
classifyTool: teamToolClassifier(ACCOUNTING_CONNECTIONS, classifyToolCall),
|
|
70
|
+
}),
|
|
71
|
+
})
|
|
59
72
|
.then(found => current && setPeer(found))
|
|
60
73
|
.catch(error => current &&
|
|
61
74
|
setPeerError(error instanceof Error ? error.message : String(error)));
|
|
62
75
|
return () => {
|
|
63
76
|
current = false;
|
|
64
77
|
};
|
|
65
|
-
}, [accountingUrl, accountingKey]);
|
|
78
|
+
}, [accountingUrl, accountingKey, inspector]);
|
|
66
79
|
// Sales runs here and asks Accounting over A2A: the conversation, the
|
|
67
80
|
// exchange, the report and the way the link carries a message.
|
|
68
81
|
const team = useA2ATeam({
|
|
@@ -74,7 +87,13 @@ function AgentA2ATeam() {
|
|
|
74
87
|
peerConnections: ACCOUNTING_CONNECTIONS,
|
|
75
88
|
// Its words, and a notebook to run here when it gives one.
|
|
76
89
|
accept: NOTEBOOK_AND_WORDS,
|
|
90
|
+
inspector,
|
|
77
91
|
});
|
|
92
|
+
// Sales' starters, in its balloon while it waits for a question.
|
|
93
|
+
const suggestions = useMemo(() => (SALES.interface.starters ?? []).map(starter => ({
|
|
94
|
+
label: starter.label,
|
|
95
|
+
prompt: starter.message,
|
|
96
|
+
})), []);
|
|
78
97
|
const send = (text) => {
|
|
79
98
|
setDraft('');
|
|
80
99
|
void team.send(text);
|
|
@@ -82,7 +101,7 @@ function AgentA2ATeam() {
|
|
|
82
101
|
const accountingWhere = accountingKey
|
|
83
102
|
? `on a runtime, ${new URL(accountingUrl).host}, with a temporary key`
|
|
84
103
|
: `on a runtime, ${new URL(accountingUrl).host}`;
|
|
85
|
-
return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [
|
|
104
|
+
return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: "Agents A2A Team" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: [TEAM.emoji, " ", _jsx("strong", { children: TEAM.name }), ": ", TEAM.description] }), ((anonymous.status === 'active' && anonymous.expiresAt) ||
|
|
86
105
|
(accountingKeyClock && !accountingKeyEnded)) && (_jsxs(Box, { sx: { display: 'flex', gap: 4, flexWrap: 'wrap', mb: 3 }, children: [anonymous.status === 'active' && anonymous.expiresAt && (_jsx(AnonymousKeyTimer, { expiresAt: anonymous.expiresAt, grantedMs: anonymous.grantedMs, label: `${SALES.name}'s trial key`, onExpire: () => useAnonymousSessionStore.getState().expire() })), accountingKeyClock && !accountingKeyEnded && (_jsx(AnonymousKeyTimer, { expiresAt: accountingKeyClock.expiresAt, grantedMs: accountingKeyClock.grantedMs, label: `${ACCOUNTING.name}'s temporary key`, onExpire: () => setAccountingKeyEnded(true) }))] })), anonymous.status === 'expired' && (_jsx(Box, { sx: { mb: 3 }, children: _jsx(AnonymousKeyExpired, { agentName: SALES.name,
|
|
87
106
|
// Sales runs in this page: what ended is its model's key, and
|
|
88
107
|
// signing in gives it the person's own.
|
|
@@ -104,9 +123,22 @@ function AgentA2ATeam() {
|
|
|
104
123
|
: 'on a runtime',
|
|
105
124
|
persona: team.entryPersona,
|
|
106
125
|
onToggle: () => composer.current?.focus(),
|
|
126
|
+
conversationLabel: `Ask ${SALES.name}`,
|
|
107
127
|
onAway: team.setEntryAway,
|
|
128
|
+
history: team.entryHistory,
|
|
108
129
|
expandTarget: notebookArea,
|
|
109
130
|
notebookTitle: `${ACCOUNTING.name}\u2019s notebook`,
|
|
131
|
+
suggestions: team.ready && !team.busy ? suggestions : undefined,
|
|
132
|
+
onSuggestion: suggestion => send(suggestion.prompt),
|
|
133
|
+
inspector,
|
|
134
|
+
onStop: team.busy ? team.stop : undefined,
|
|
135
|
+
about: {
|
|
136
|
+
name: SALES.name,
|
|
137
|
+
spec: `${SALES.id}:${SALES.version}`,
|
|
138
|
+
model: SALES.model || undefined,
|
|
139
|
+
where: 'in your browser',
|
|
140
|
+
description: SALES.description || undefined,
|
|
141
|
+
},
|
|
110
142
|
}, peer: {
|
|
111
143
|
id: ACCOUNTING.id,
|
|
112
144
|
name: ACCOUNTING.name,
|
|
@@ -115,8 +147,30 @@ function AgentA2ATeam() {
|
|
|
115
147
|
where: accountingWhere,
|
|
116
148
|
persona: team.peerPersona,
|
|
117
149
|
onAway: team.setPeerAway,
|
|
150
|
+
history: team.peerHistory,
|
|
118
151
|
connections: ACCOUNTING_CONNECTIONS,
|
|
119
|
-
|
|
152
|
+
inspector,
|
|
153
|
+
about: {
|
|
154
|
+
name: ACCOUNTING.name,
|
|
155
|
+
spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
|
|
156
|
+
model: ACCOUNTING.model || undefined,
|
|
157
|
+
where: accountingWhere,
|
|
158
|
+
// What its A2A card says, once it is reached.
|
|
159
|
+
description: peer?.card.description || ACCOUNTING.description || undefined,
|
|
160
|
+
skills: peer?.card.skills.map(skill => skill.name),
|
|
161
|
+
protocol: peer ? 'a2a' : undefined,
|
|
162
|
+
url: peer ? accountingUrl : undefined,
|
|
163
|
+
},
|
|
164
|
+
}, flow: team.flow, calls: team.calls,
|
|
165
|
+
// Room for the balloons: suggestions, *more* and a notebook.
|
|
166
|
+
balloonRoom: 320, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), _jsx(Box, { ref: notebookArea, sx: { mb: team.notebook ? 3 : 0 }, "data-team-notebook-placement": "under-graph" }), _jsx(Box, { sx: {
|
|
167
|
+
display: 'grid',
|
|
168
|
+
gridTemplateColumns: ['1fr', '1fr', '1fr 1fr'],
|
|
169
|
+
gap: 3,
|
|
170
|
+
mb: 3,
|
|
171
|
+
}, "data-team-inspectors": "", children: [SALES, ACCOUNTING].map(member => (_jsxs(Box, { sx: { minWidth: 0 }, "data-team-inspector": member.id, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: [member.emoji, " ", member.name, "\u2019s Agent Inspector"] }), _jsx(AgentInspector, { sink: inspector, agent: member.name, maxHeight: 360, exportName: `agent-inspector-${member.id}`, emptyText: member === SALES
|
|
172
|
+
? 'Its model turns, its call to Accounting and the A2A messages it sends and receives show here.'
|
|
173
|
+
: 'What it receives over A2A, its Odoo MCP calls and what it sends back show here.' })] }, member.id))) }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: ["Or choose one of ", SALES.name, "\u2019s suggestions in its balloon."] }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
|
|
120
174
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
121
175
|
event.preventDefault();
|
|
122
176
|
send(draft);
|
|
@@ -126,13 +180,13 @@ function AgentA2ATeam() {
|
|
|
126
180
|
mb: 2,
|
|
127
181
|
borderRadius: 2,
|
|
128
182
|
bg: turn.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
|
|
129
|
-
}, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }),
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
}, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }), _jsx(Box, { sx: { flex: '1 1 360px', minWidth: 0 }, children: !team.notebook && (_jsxs(_Fragment, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The report" }), _jsx(Box, { sx: {
|
|
184
|
+
p: 3,
|
|
185
|
+
border: '1px solid',
|
|
186
|
+
borderColor: 'border.default',
|
|
187
|
+
borderRadius: 2,
|
|
188
|
+
minHeight: 120,
|
|
189
|
+
}, "data-team-report": "", children: team.report ? (_jsx(Streamdown, { children: team.report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "The report Accounting returns appears here, as it returned it, or as a notebook to run when it gives one." })) })] })) })] })] }));
|
|
136
190
|
}
|
|
137
191
|
export function AgentA2ATeamExample() {
|
|
138
192
|
return (_jsx(ThemedProvider, { children: _jsx(AgentA2ATeam, {}) }));
|
|
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
* and so are the characters clippy.js publishes, fetched when picked.
|
|
16
16
|
*/
|
|
17
17
|
import { useEffect, useRef, useState } from 'react';
|
|
18
|
+
import { useAgentInspector } from '../components/inspector/agentInspector';
|
|
18
19
|
import { buildReactorFromPlugins } from '@datalayer/reactor';
|
|
19
20
|
import { Button, Heading, Text, ToggleSwitch } from '@primer/react';
|
|
20
21
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -24,7 +25,7 @@ import { AssistantCharactersPlugin, assistantCharacterNamed, assistantCharacters
|
|
|
24
25
|
import { OwlCharacterPlugin } from './utils/owlCharacterPlugin';
|
|
25
26
|
import { readTestSpriteCharacter } from './utils/testSpriteCharacter';
|
|
26
27
|
import { CLIPPY_JS_CHARACTERS, CLIPPY_JS_VERSION, readClippyJsCharacter, } from './utils/clippyJsCharacters';
|
|
27
|
-
import {
|
|
28
|
+
import { CHARACTER_FILES_ACCEPT, readCharacterFiles, } from '../chat/assistant/formats';
|
|
28
29
|
/**
|
|
29
30
|
* The agent-runtimes server the assistant talks to: `?agentRuntimesUrl=`
|
|
30
31
|
* when given, else this machine's on port 8765. It serves the `assistant`
|
|
@@ -34,6 +35,35 @@ const SERVER = ((typeof window === 'undefined'
|
|
|
34
35
|
? null
|
|
35
36
|
: new URLSearchParams(window.location.search).get('agentRuntimesUrl')) ||
|
|
36
37
|
'http://127.0.0.1:8765').replace(/\/+$/, '');
|
|
38
|
+
/** The agent the assistant talks to on that server. */
|
|
39
|
+
const AGENT_ID = 'assistant';
|
|
40
|
+
/**
|
|
41
|
+
* Make sure the server has the assistant's agent: `npm run examples` starts
|
|
42
|
+
* the server with none, so the page creates it from the
|
|
43
|
+
* `example-agentic-chat` spec the first time, over AG-UI, the spec's own
|
|
44
|
+
* protocol.
|
|
45
|
+
*/
|
|
46
|
+
async function ensureAssistantAgent() {
|
|
47
|
+
const found = await fetch(`${SERVER}/api/v1/agents/${AGENT_ID}`);
|
|
48
|
+
if (found.ok) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (found.status !== 404) {
|
|
52
|
+
throw new Error(`The server could not say whether the ${AGENT_ID} agent exists (${found.status}).`);
|
|
53
|
+
}
|
|
54
|
+
const created = await fetch(`${SERVER}/api/v1/agents`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'content-type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
name: AGENT_ID,
|
|
59
|
+
agent_spec_id: 'example-agentic-chat',
|
|
60
|
+
transport: 'ag-ui',
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
if (!created.ok && created.status !== 409) {
|
|
64
|
+
throw new Error(`The server could not create the ${AGENT_ID} agent (${created.status}).`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
37
67
|
/** The plugins this page enables: Datalayer's characters and the owl's. */
|
|
38
68
|
const reactor = buildReactorFromPlugins([
|
|
39
69
|
AssistantCharactersPlugin,
|
|
@@ -45,27 +75,6 @@ const CATALOGUE = assistantCharactersOf(reactor).map(entry => ({
|
|
|
45
75
|
id: entry.id,
|
|
46
76
|
name: entry.character.name,
|
|
47
77
|
}));
|
|
48
|
-
/**
|
|
49
|
-
* A character file a person picked, read in the page (T-26): one `.acs`, or
|
|
50
|
-
* a clippy.js character's `agent.js` and `map.png` (and its sounds file).
|
|
51
|
-
*/
|
|
52
|
-
async function readPicked(files) {
|
|
53
|
-
const acs = files.find(file => file.name.toLowerCase().endsWith('.acs'));
|
|
54
|
-
if (acs) {
|
|
55
|
-
return readAcsCharacter(await acs.arrayBuffer());
|
|
56
|
-
}
|
|
57
|
-
const agentJs = files.find(file => file.name.toLowerCase() === 'agent.js');
|
|
58
|
-
const mapPng = files.find(file => /\.(png|gif|webp|jpe?g)$/i.test(file.name));
|
|
59
|
-
const soundsJs = files.find(file => /^sounds-.*\.js$/i.test(file.name));
|
|
60
|
-
if (!agentJs || !mapPng) {
|
|
61
|
-
throw new Error('Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).');
|
|
62
|
-
}
|
|
63
|
-
return readClippyCharacter({
|
|
64
|
-
agentJs: await agentJs.text(),
|
|
65
|
-
mapPng,
|
|
66
|
-
soundsJs: soundsJs ? await soundsJs.text() : undefined,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
78
|
const AssistantExample = () => {
|
|
70
79
|
const [character, setCharacter] = useState('paperclip');
|
|
71
80
|
// A character chosen by id is the one its plugin contributes.
|
|
@@ -73,8 +82,21 @@ const AssistantExample = () => {
|
|
|
73
82
|
? assistantCharacterNamed(reactor, character)
|
|
74
83
|
: character;
|
|
75
84
|
const [loadError, setLoadError] = useState();
|
|
85
|
+
// The agent on the server, made if it is not there yet.
|
|
86
|
+
const [agentReady, setAgentReady] = useState(false);
|
|
87
|
+
const [agentError, setAgentError] = useState();
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
let cancelled = false;
|
|
90
|
+
ensureAssistantAgent().then(() => !cancelled && setAgentReady(true), error => !cancelled &&
|
|
91
|
+
setAgentError(`${error instanceof Error ? error.message : String(error)} The assistant talks to an agent-runtimes server at ${SERVER}.`));
|
|
92
|
+
return () => {
|
|
93
|
+
cancelled = true;
|
|
94
|
+
};
|
|
95
|
+
}, []);
|
|
76
96
|
// How the balloon shows the conversation (T-23): history, or current.
|
|
77
97
|
const [balloon, setBalloon] = useState('history');
|
|
98
|
+
// What the agent does, for the menu's *Inspect the agent…*.
|
|
99
|
+
const { sink: inspector } = useAgentInspector();
|
|
78
100
|
// Pixel, the test sprite, read once through the clippy.js reader.
|
|
79
101
|
const [pixel, setPixel] = useState();
|
|
80
102
|
useEffect(() => {
|
|
@@ -142,13 +164,13 @@ const AssistantExample = () => {
|
|
|
142
164
|
onClick: () => setBalloon(balloon === 'history' ? 'current' : 'history') }), _jsx(Text, { "aria-live": "polite", "data-toggle-state": "", sx: { color: 'fg.muted' }, children: balloon === 'history' ? 'History' : 'Current' })] })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "The clippy.js characters" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["Fetched when picked from the", ' ', _jsxs("code", { children: ["clippyjs@", CLIPPY_JS_VERSION] }), " package on jsDelivr and read as data by the same reader. They are not in this repository: clippy.js\u2019s licence covers its code only, and the characters are Microsoft\u2019s."] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CLIPPY_JS_CHARACTERS.map(name => {
|
|
143
165
|
const picked = clippyJs[name] !== undefined && character === clippyJs[name];
|
|
144
166
|
return (_jsx(Button, { variant: picked ? 'primary' : 'default', onClick: () => void pickClippyJs(name), disabled: fetching !== undefined, "aria-pressed": picked, "data-assistant-character": `clippy-js-${name.toLowerCase()}`, children: fetching === name ? `${name}…` : name }, name));
|
|
145
|
-
}) })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept:
|
|
167
|
+
}) })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: CHARACTER_FILES_ACCEPT, "aria-label": "Character files", onChange: async (event) => {
|
|
146
168
|
const files = Array.from(event.target.files ?? []);
|
|
147
169
|
if (!files.length) {
|
|
148
170
|
return;
|
|
149
171
|
}
|
|
150
172
|
try {
|
|
151
|
-
setCharacter(await
|
|
173
|
+
setCharacter(await readCharacterFiles(files));
|
|
152
174
|
setLoadError(undefined);
|
|
153
175
|
}
|
|
154
176
|
catch (error) {
|
|
@@ -156,8 +178,25 @@ const AssistantExample = () => {
|
|
|
156
178
|
}
|
|
157
179
|
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' &&
|
|
158
180
|
character !== pixel &&
|
|
159
|
-
!isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, balloonDisplay: balloon, protocol: "
|
|
181
|
+
!isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), agentError && (_jsx(Box, { sx: { maxWidth: 720, mx: 'auto', mt: 3 }, children: _jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg' }, children: agentError }) })), agentReady && (_jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, balloonDisplay: balloon, protocol: "ag-ui", endpoint: `${SERVER}/api/v1/ag-ui/${AGENT_ID}/`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
|
|
160
182
|
// Ask a decision beside the composer: Jev, through this server.
|
|
161
|
-
decisions: { serverUrl: SERVER }
|
|
183
|
+
decisions: { serverUrl: SERVER },
|
|
184
|
+
// Right-click the character: its menu.
|
|
185
|
+
inspector: inspector, onBalloonDisplayChange: setBalloon, onChangeCharacter: () => document
|
|
186
|
+
.querySelector('[data-assistant-character]')
|
|
187
|
+
?.focus(), about: {
|
|
188
|
+
name: 'Assistant',
|
|
189
|
+
spec: AGENT_ID,
|
|
190
|
+
where: `on a runtime, ${new URL(SERVER).host}`,
|
|
191
|
+
}, suggestions: [
|
|
192
|
+
{
|
|
193
|
+
title: 'What is on this page?',
|
|
194
|
+
message: 'What is on this page?',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
title: 'Tell me a joke',
|
|
198
|
+
message: 'Tell me a short joke about accountants.',
|
|
199
|
+
},
|
|
200
|
+
] }, typeof character === 'string' ? character : character.sprite))] }) }));
|
|
162
201
|
};
|
|
163
202
|
export default AssistantExample;
|