@jupyternaut/persona 0.0.0 → 0.20.0
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-commands/mention.d.ts +9 -0
- package/lib/chat-commands/mention.js +30 -0
- package/lib/completion/completion-provider.d.ts +86 -0
- package/lib/completion/completion-provider.js +246 -0
- package/lib/completion/index.d.ts +2 -0
- package/lib/completion/index.js +1 -0
- package/lib/components/completion-status.d.ts +26 -0
- package/lib/components/completion-status.js +52 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +1 -0
- package/lib/diff-manager.d.ts +25 -0
- package/lib/diff-manager.js +60 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +522 -0
- package/lib/models/settings-model.d.ts +36 -0
- package/lib/models/settings-model.js +356 -0
- package/lib/persona-registry.d.ts +15 -0
- package/lib/persona-registry.js +29 -0
- package/lib/persona.d.ts +66 -0
- package/lib/persona.js +414 -0
- package/lib/process-attachments.d.ts +5 -0
- package/lib/process-attachments.js +287 -0
- package/lib/tokens.d.ts +101 -0
- package/lib/tokens.js +20 -0
- package/lib/widgets/ai-settings.d.ts +54 -0
- package/lib/widgets/ai-settings.js +572 -0
- package/lib/widgets/provider-config-dialog.d.ts +16 -0
- package/lib/widgets/provider-config-dialog.js +384 -0
- package/package.json +111 -7
- package/schema/settings-model.json +287 -0
- package/src/chat-commands/mention.tsx +46 -0
- package/src/completion/completion-provider.ts +350 -0
- package/src/completion/index.ts +1 -0
- package/src/components/completion-status.tsx +93 -0
- package/src/components/index.ts +1 -0
- package/src/diff-manager.ts +81 -0
- package/src/index.ts +710 -0
- package/src/models/settings-model.ts +415 -0
- package/src/persona-registry.ts +46 -0
- package/src/persona.ts +610 -0
- package/src/process-attachments.ts +369 -0
- package/src/tokens.ts +121 -0
- package/src/widgets/ai-settings.tsx +1308 -0
- package/src/widgets/provider-config-dialog.tsx +997 -0
- package/style/base.css +14 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/README.md +0 -3
- package/index.js +0 -1
package/src/persona.ts
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IAttachment,
|
|
3
|
+
IMessage,
|
|
4
|
+
IMimeModelBody,
|
|
5
|
+
IChatModel,
|
|
6
|
+
IUser,
|
|
7
|
+
INewMessage
|
|
8
|
+
} from '@jupyter/chat';
|
|
9
|
+
|
|
10
|
+
import * as nbformat from '@jupyterlab/nbformat';
|
|
11
|
+
|
|
12
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
13
|
+
|
|
14
|
+
import type { IDocumentManager } from '@jupyterlab/docmanager';
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
IAgentManager,
|
|
18
|
+
IAISettingsModel,
|
|
19
|
+
IProviderRegistry
|
|
20
|
+
} from '@jupyternaut/agent';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
modelSupportsAudio,
|
|
24
|
+
modelSupportsImages,
|
|
25
|
+
modelSupportsPdf
|
|
26
|
+
} from '@jupyternaut/agent';
|
|
27
|
+
|
|
28
|
+
import type { IObservableDisposable } from '@lumino/disposable';
|
|
29
|
+
|
|
30
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
31
|
+
|
|
32
|
+
import type { ModelMessage, UserContent } from 'ai';
|
|
33
|
+
|
|
34
|
+
import { processAttachments } from './process-attachments';
|
|
35
|
+
|
|
36
|
+
import type { IPersona } from './tokens';
|
|
37
|
+
|
|
38
|
+
type ToolStatus =
|
|
39
|
+
| 'pending'
|
|
40
|
+
| 'awaiting_approval'
|
|
41
|
+
| 'approved'
|
|
42
|
+
| 'rejected'
|
|
43
|
+
| 'completed'
|
|
44
|
+
| 'error';
|
|
45
|
+
|
|
46
|
+
interface IToolExecutionContext {
|
|
47
|
+
toolCallId: string;
|
|
48
|
+
messageId: string;
|
|
49
|
+
toolName: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
input: string;
|
|
52
|
+
status: ToolStatus;
|
|
53
|
+
summary?: string;
|
|
54
|
+
shouldAutoRenderMimeBundles?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractToolSummary(toolName: string, input: string): string {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(input);
|
|
60
|
+
switch (toolName) {
|
|
61
|
+
case 'execute_command':
|
|
62
|
+
return parsed.commandId ?? '';
|
|
63
|
+
case 'discover_commands':
|
|
64
|
+
case 'discover_skills':
|
|
65
|
+
case 'web_search':
|
|
66
|
+
return parsed.query ? `query: "${parsed.query}"` : '';
|
|
67
|
+
case 'load_skill':
|
|
68
|
+
return parsed.name
|
|
69
|
+
? parsed.resource
|
|
70
|
+
? `${parsed.name} (${parsed.resource})`
|
|
71
|
+
: parsed.name
|
|
72
|
+
: '';
|
|
73
|
+
case 'browser_fetch':
|
|
74
|
+
case 'web_fetch':
|
|
75
|
+
return parsed.url ?? '';
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// ignore malformed input
|
|
79
|
+
}
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatToolOutput(outputData: unknown): string {
|
|
84
|
+
if (typeof outputData === 'string') {
|
|
85
|
+
return outputData;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return JSON.stringify(outputData, null, 2);
|
|
89
|
+
} catch {
|
|
90
|
+
return '[Complex object - cannot serialize]';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type IDisplayOutput =
|
|
95
|
+
| nbformat.IDisplayData
|
|
96
|
+
| nbformat.IDisplayUpdate
|
|
97
|
+
| nbformat.IExecuteResult;
|
|
98
|
+
|
|
99
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
100
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isDisplayOutput(value: unknown): value is IDisplayOutput {
|
|
104
|
+
if (!isPlainObject(value)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const output = value as nbformat.IOutput;
|
|
108
|
+
return (
|
|
109
|
+
nbformat.isDisplayData(output) ||
|
|
110
|
+
nbformat.isDisplayUpdate(output) ||
|
|
111
|
+
nbformat.isExecuteResult(output)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toDisplayOutputs(value: unknown): IDisplayOutput[] {
|
|
116
|
+
if (isDisplayOutput(value)) {
|
|
117
|
+
return [value];
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
return value.filter(isDisplayOutput);
|
|
121
|
+
}
|
|
122
|
+
if (!isPlainObject(value)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(value.outputs)) {
|
|
126
|
+
return value.outputs.filter(isDisplayOutput);
|
|
127
|
+
}
|
|
128
|
+
if ('result' in value) {
|
|
129
|
+
return toDisplayOutputs(value.result);
|
|
130
|
+
}
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractMimeBundles(
|
|
135
|
+
content: unknown,
|
|
136
|
+
trustedMimeTypes: ReadonlySet<string>
|
|
137
|
+
): IMimeModelBody[] {
|
|
138
|
+
return toDisplayOutputs(content)
|
|
139
|
+
.map((output): IMimeModelBody | null => {
|
|
140
|
+
const data = output.data;
|
|
141
|
+
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
data: data as IRenderMime.IMimeModel['data'],
|
|
146
|
+
...(isPlainObject(output.metadata)
|
|
147
|
+
? {
|
|
148
|
+
metadata: output.metadata as IRenderMime.IMimeModel['metadata']
|
|
149
|
+
}
|
|
150
|
+
: {}),
|
|
151
|
+
...(Object.keys(data).some(m => trustedMimeTypes.has(m))
|
|
152
|
+
? { trusted: true }
|
|
153
|
+
: {})
|
|
154
|
+
};
|
|
155
|
+
})
|
|
156
|
+
.filter((b): b is IMimeModelBody => b !== null);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Links an IAgentManager to an IChatModel for the Jupyternaut persona.
|
|
161
|
+
*
|
|
162
|
+
* Monitors new messages arriving on the chat model and responds when the
|
|
163
|
+
* persona trigger string is mentioned. The handler and its agent stay alive
|
|
164
|
+
* as long as the associated chat widget is open, so conversation history is
|
|
165
|
+
* preserved across multiple mentions.
|
|
166
|
+
*/
|
|
167
|
+
export class Persona implements IPersona {
|
|
168
|
+
constructor(options: Persona.IOptions) {
|
|
169
|
+
this._model = options.model;
|
|
170
|
+
this._agent = options.agentManager;
|
|
171
|
+
this._persona = options.persona;
|
|
172
|
+
this._settingsModel = options.settingsModel;
|
|
173
|
+
this._providerRegistry = options.providerRegistry;
|
|
174
|
+
this._documentManager = options.documentManager;
|
|
175
|
+
|
|
176
|
+
for (const message of options.model.messages) {
|
|
177
|
+
this._respondedToIds.add(message.id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this._agent.agentEvent.connect(this._onAgentEvent, this);
|
|
181
|
+
this._agent.activeProviderChanged.connect(
|
|
182
|
+
this._onActiveProviderChanged,
|
|
183
|
+
this
|
|
184
|
+
);
|
|
185
|
+
this._model.messagesUpdated.connect(this._onMessagesUpdated, this);
|
|
186
|
+
(this._model as unknown as IObservableDisposable).disposed.connect(
|
|
187
|
+
this.dispose,
|
|
188
|
+
this
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
dispose(): void {
|
|
193
|
+
this._agent.agentEvent.disconnect(this._onAgentEvent, this);
|
|
194
|
+
this._agent.activeProviderChanged.disconnect(
|
|
195
|
+
this._onActiveProviderChanged,
|
|
196
|
+
this
|
|
197
|
+
);
|
|
198
|
+
this._model.messagesUpdated.disconnect(this._onMessagesUpdated, this);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get agentManager(): IAgentManager {
|
|
202
|
+
return this._agent;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
get model(): IChatModel {
|
|
206
|
+
return this._model;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
get isBusy(): boolean {
|
|
210
|
+
return this._busy;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
get busyChanged(): ISignal<IPersona, boolean> {
|
|
214
|
+
return this._busyChanged;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async _onMessagesUpdated(): Promise<void> {
|
|
218
|
+
const unhandled = this._model.messages.filter(
|
|
219
|
+
m =>
|
|
220
|
+
!this._respondedToIds.has(m.id) &&
|
|
221
|
+
!m.sender.bot &&
|
|
222
|
+
(!this.requireMention || m.mentions?.includes(this._persona))
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
for (const message of unhandled) {
|
|
226
|
+
this._respondedToIds.add(message.id);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const message of unhandled) {
|
|
230
|
+
const personaMention = `@${this._persona.mention_name}`;
|
|
231
|
+
const body = message.body.replace(personaMention, '').trim();
|
|
232
|
+
await this._respond(body || message.body, message.attachments);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async _respond(
|
|
237
|
+
body: string,
|
|
238
|
+
attachments?: IAttachment[]
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
this._busy = true;
|
|
241
|
+
this._busyChanged.emit(true);
|
|
242
|
+
this._model.updateWriters([{ user: this._persona }]);
|
|
243
|
+
try {
|
|
244
|
+
let content: UserContent = body;
|
|
245
|
+
if (attachments && attachments.length > 0) {
|
|
246
|
+
const providerConfig = this._settingsModel.getProvider(
|
|
247
|
+
this._agent.activeProvider
|
|
248
|
+
);
|
|
249
|
+
content = await processAttachments(
|
|
250
|
+
attachments,
|
|
251
|
+
this._documentManager,
|
|
252
|
+
body,
|
|
253
|
+
modelSupportsImages(providerConfig, this._providerRegistry),
|
|
254
|
+
modelSupportsPdf(providerConfig, this._providerRegistry),
|
|
255
|
+
modelSupportsAudio(providerConfig, this._providerRegistry)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
await this._agent.generateResponse(content);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('Persona: error generating response', error);
|
|
261
|
+
} finally {
|
|
262
|
+
this._busy = false;
|
|
263
|
+
this._busyChanged.emit(false);
|
|
264
|
+
this._model.updateWriters([]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
rebuildHistory(): Promise<void> {
|
|
269
|
+
return this._rebuildHistory();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private _onActiveProviderChanged(): void {
|
|
273
|
+
const providerConfig = this._settingsModel.getProvider(
|
|
274
|
+
this._agent.activeProvider
|
|
275
|
+
);
|
|
276
|
+
const modelKey = providerConfig
|
|
277
|
+
? `${providerConfig.provider}:${providerConfig.model}`
|
|
278
|
+
: undefined;
|
|
279
|
+
if (modelKey && modelKey !== this._currentModelKey) {
|
|
280
|
+
this._currentModelKey = modelKey;
|
|
281
|
+
this._rebuildHistory().catch(e =>
|
|
282
|
+
console.warn('Failed to rebuild history on model change:', e)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async _rebuildHistory(): Promise<void> {
|
|
288
|
+
const providerConfig = this._settingsModel.getProvider(
|
|
289
|
+
this._agent.activeProvider
|
|
290
|
+
);
|
|
291
|
+
const supportsImages = modelSupportsImages(
|
|
292
|
+
providerConfig,
|
|
293
|
+
this._providerRegistry
|
|
294
|
+
);
|
|
295
|
+
const supportsPdf = modelSupportsPdf(
|
|
296
|
+
providerConfig,
|
|
297
|
+
this._providerRegistry
|
|
298
|
+
);
|
|
299
|
+
const supportsAudio = modelSupportsAudio(
|
|
300
|
+
providerConfig,
|
|
301
|
+
this._providerRegistry
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const modelMessages: ModelMessage[] = [];
|
|
305
|
+
for (const msg of this._model.messages) {
|
|
306
|
+
const isAI = msg.sender.bot === true;
|
|
307
|
+
if (!isAI && msg.attachments?.length) {
|
|
308
|
+
const enhancedContent = await processAttachments(
|
|
309
|
+
msg.attachments,
|
|
310
|
+
this._documentManager,
|
|
311
|
+
msg.body,
|
|
312
|
+
supportsImages,
|
|
313
|
+
supportsPdf,
|
|
314
|
+
supportsAudio
|
|
315
|
+
);
|
|
316
|
+
modelMessages.push({ role: 'user', content: enhancedContent });
|
|
317
|
+
} else if (msg.body) {
|
|
318
|
+
modelMessages.push({
|
|
319
|
+
role: isAI ? 'assistant' : 'user',
|
|
320
|
+
content: msg.body
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this._agent.setHistory(modelMessages);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private _onAgentEvent(
|
|
329
|
+
_: IAgentManager,
|
|
330
|
+
event: IAgentManager.IAgentEvent
|
|
331
|
+
): void {
|
|
332
|
+
switch (event.type) {
|
|
333
|
+
case 'message_start':
|
|
334
|
+
this._handleMessageStart(event);
|
|
335
|
+
break;
|
|
336
|
+
case 'message_chunk':
|
|
337
|
+
this._handleMessageChunk(event);
|
|
338
|
+
break;
|
|
339
|
+
case 'message_complete':
|
|
340
|
+
this._handleMessageComplete(event);
|
|
341
|
+
break;
|
|
342
|
+
case 'tool_call_start':
|
|
343
|
+
this._handleToolCallStart(event);
|
|
344
|
+
break;
|
|
345
|
+
case 'tool_call_complete':
|
|
346
|
+
this._handleToolCallComplete(event);
|
|
347
|
+
break;
|
|
348
|
+
case 'tool_approval_request':
|
|
349
|
+
this._handleToolApprovalRequest(event);
|
|
350
|
+
break;
|
|
351
|
+
case 'tool_approval_resolved':
|
|
352
|
+
this._handleToolApprovalResolved(event);
|
|
353
|
+
break;
|
|
354
|
+
case 'error':
|
|
355
|
+
this._handleError(event);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async _handleMessageStart(
|
|
361
|
+
event: IAgentManager.IAgentEvent<'message_start'>
|
|
362
|
+
): Promise<void> {
|
|
363
|
+
const message: INewMessage = {
|
|
364
|
+
body: '',
|
|
365
|
+
sender: this._persona
|
|
366
|
+
};
|
|
367
|
+
const msgId = await this._model.sendMessage(message);
|
|
368
|
+
const streamingMessage =
|
|
369
|
+
this._model.messages.find(m => m.id === msgId) ?? null;
|
|
370
|
+
|
|
371
|
+
if (streamingMessage) {
|
|
372
|
+
this._streamingMessage.set(event.data.messageId, streamingMessage);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private _handleMessageChunk(
|
|
377
|
+
event: IAgentManager.IAgentEvent<'message_chunk'>
|
|
378
|
+
): void {
|
|
379
|
+
const streamingMessage = this._streamingMessage.get(event.data.messageId);
|
|
380
|
+
if (streamingMessage) {
|
|
381
|
+
streamingMessage.update({ body: event.data.fullContent });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private _handleMessageComplete(
|
|
386
|
+
event: IAgentManager.IAgentEvent<'message_complete'>
|
|
387
|
+
): void {
|
|
388
|
+
const streamingMessage = this._streamingMessage.get(event.data.messageId);
|
|
389
|
+
if (streamingMessage) {
|
|
390
|
+
streamingMessage.update({ body: event.data.content });
|
|
391
|
+
this._streamingMessage.delete(event.data.messageId);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async _handleToolCallStart(
|
|
396
|
+
event: IAgentManager.IAgentEvent<'tool_call_start'>
|
|
397
|
+
): Promise<void> {
|
|
398
|
+
const summary = extractToolSummary(event.data.toolName, event.data.input);
|
|
399
|
+
const shouldAutoRenderMimeBundles =
|
|
400
|
+
this._computeShouldAutoRenderMimeBundles(
|
|
401
|
+
event.data.toolName,
|
|
402
|
+
event.data.input
|
|
403
|
+
);
|
|
404
|
+
const context: IToolExecutionContext = {
|
|
405
|
+
toolCallId: event.data.callId,
|
|
406
|
+
messageId: '',
|
|
407
|
+
toolName: event.data.toolName,
|
|
408
|
+
title: event.data.title,
|
|
409
|
+
input: event.data.input,
|
|
410
|
+
status: 'pending',
|
|
411
|
+
summary,
|
|
412
|
+
shouldAutoRenderMimeBundles
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const displayName = context.title ?? context.toolName;
|
|
416
|
+
const messageId = await this._model.sendMessage({
|
|
417
|
+
body: '',
|
|
418
|
+
mime_model: {
|
|
419
|
+
data: {
|
|
420
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
421
|
+
},
|
|
422
|
+
metadata: {
|
|
423
|
+
toolCalls: [
|
|
424
|
+
{
|
|
425
|
+
toolCallId: context.toolCallId,
|
|
426
|
+
title: context.summary
|
|
427
|
+
? `${displayName} : ${context.summary}`
|
|
428
|
+
: displayName,
|
|
429
|
+
kind: context.toolName,
|
|
430
|
+
status: 'in_progress',
|
|
431
|
+
rawInput: context.input
|
|
432
|
+
}
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
sender: this._persona
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
if (messageId) {
|
|
440
|
+
context.messageId = messageId;
|
|
441
|
+
this._toolContexts.set(event.data.callId, context);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private _handleToolCallComplete(
|
|
446
|
+
event: IAgentManager.IAgentEvent<'tool_call_complete'>
|
|
447
|
+
): void {
|
|
448
|
+
const context = this._toolContexts.get(event.data.callId);
|
|
449
|
+
const status = event.data.isError ? 'error' : 'completed';
|
|
450
|
+
this._updateToolCallUI(
|
|
451
|
+
event.data.callId,
|
|
452
|
+
status,
|
|
453
|
+
formatToolOutput(event.data.outputData)
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (!event.data.isError && context?.shouldAutoRenderMimeBundles) {
|
|
457
|
+
const trustedMimeTypes = new Set(
|
|
458
|
+
this._settingsModel.config.trustedMimeTypesForAutoRender
|
|
459
|
+
);
|
|
460
|
+
for (const bundle of extractMimeBundles(
|
|
461
|
+
event.data.outputData,
|
|
462
|
+
trustedMimeTypes
|
|
463
|
+
)) {
|
|
464
|
+
this._model.sendMessage({
|
|
465
|
+
body: '',
|
|
466
|
+
mime_model: bundle,
|
|
467
|
+
sender: this._persona
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this._toolContexts.delete(event.data.callId);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private _computeShouldAutoRenderMimeBundles(
|
|
476
|
+
toolName: string,
|
|
477
|
+
input: string
|
|
478
|
+
): boolean {
|
|
479
|
+
if (toolName !== 'execute_command') {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const parsed = JSON.parse(input);
|
|
484
|
+
return (
|
|
485
|
+
typeof parsed.commandId === 'string' &&
|
|
486
|
+
this._settingsModel.config.commandsAutoRenderMimeBundles.includes(
|
|
487
|
+
parsed.commandId
|
|
488
|
+
)
|
|
489
|
+
);
|
|
490
|
+
} catch {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private _handleToolApprovalRequest(
|
|
496
|
+
event: IAgentManager.IAgentEvent<'tool_approval_request'>
|
|
497
|
+
): void {
|
|
498
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
499
|
+
if (!context) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
context.input = JSON.stringify(event.data.args, null, 2);
|
|
503
|
+
this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private _handleToolApprovalResolved(
|
|
507
|
+
event: IAgentManager.IAgentEvent<'tool_approval_resolved'>
|
|
508
|
+
): void {
|
|
509
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
510
|
+
if (!context) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const status = event.data.approved ? 'approved' : 'rejected';
|
|
514
|
+
this._updateToolCallUI(event.data.toolCallId, status);
|
|
515
|
+
if (!event.data.approved) {
|
|
516
|
+
this._toolContexts.delete(event.data.toolCallId);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private _handleError(event: IAgentManager.IAgentEvent<'error'>): void {
|
|
521
|
+
this._model.sendMessage({
|
|
522
|
+
body: '',
|
|
523
|
+
mime_model: {
|
|
524
|
+
data: { 'application/vnd.jupyter.chat.components': 'error' },
|
|
525
|
+
metadata: {
|
|
526
|
+
errorMessage: `Error generating response: ${event.data.error.message}`
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
sender: this._persona
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private _updateToolCallUI(
|
|
534
|
+
toolCallId: string,
|
|
535
|
+
status: ToolStatus,
|
|
536
|
+
output?: string
|
|
537
|
+
): void {
|
|
538
|
+
const context = this._toolContexts.get(toolCallId);
|
|
539
|
+
if (!context) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const message = this._model.messages.find(m => m.id === context.messageId);
|
|
543
|
+
if (!message) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
context.status = status;
|
|
547
|
+
const displayName = context.title ?? context.toolName;
|
|
548
|
+
message.update({
|
|
549
|
+
mime_model: {
|
|
550
|
+
data: {
|
|
551
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
552
|
+
},
|
|
553
|
+
metadata: {
|
|
554
|
+
toolCalls: [
|
|
555
|
+
{
|
|
556
|
+
toolCallId: context.toolCallId,
|
|
557
|
+
title: context.summary
|
|
558
|
+
? `${displayName} : ${context.summary}`
|
|
559
|
+
: displayName,
|
|
560
|
+
kind: context.toolName,
|
|
561
|
+
status: context.status,
|
|
562
|
+
rawInput: context.input,
|
|
563
|
+
rawOutput: output,
|
|
564
|
+
sessionId: this._model.name,
|
|
565
|
+
permissionStatus:
|
|
566
|
+
status === 'awaiting_approval' ? 'pending' : 'resolved',
|
|
567
|
+
...(status === 'awaiting_approval' && {
|
|
568
|
+
permissionOptions: [
|
|
569
|
+
{ optionId: 'approve', name: 'Approve', kind: 'allow_once' },
|
|
570
|
+
{ optionId: 'reject', name: 'Reject', kind: 'reject_once' }
|
|
571
|
+
]
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
]
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Whether a mention is required to trigger a response.
|
|
582
|
+
* When false, the persona responds to all non-bot messages.
|
|
583
|
+
* Defaults to true.
|
|
584
|
+
*/
|
|
585
|
+
requireMention: boolean = true;
|
|
586
|
+
|
|
587
|
+
private readonly _model: IChatModel;
|
|
588
|
+
private readonly _agent: IAgentManager;
|
|
589
|
+
private readonly _persona: IUser;
|
|
590
|
+
private readonly _settingsModel: IAISettingsModel;
|
|
591
|
+
private readonly _providerRegistry: IProviderRegistry | undefined;
|
|
592
|
+
private readonly _documentManager: IDocumentManager | undefined;
|
|
593
|
+
private _respondedToIds = new Set<string>();
|
|
594
|
+
private _currentModelKey: string | undefined;
|
|
595
|
+
private _busy = false;
|
|
596
|
+
private _busyChanged = new Signal<IPersona, boolean>(this);
|
|
597
|
+
private _streamingMessage = new Map<string, IMessage>();
|
|
598
|
+
private _toolContexts = new Map<string, IToolExecutionContext>();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export namespace Persona {
|
|
602
|
+
export interface IOptions {
|
|
603
|
+
model: IChatModel;
|
|
604
|
+
agentManager: IAgentManager;
|
|
605
|
+
persona: IUser;
|
|
606
|
+
settingsModel: IAISettingsModel;
|
|
607
|
+
providerRegistry?: IProviderRegistry;
|
|
608
|
+
documentManager?: IDocumentManager;
|
|
609
|
+
}
|
|
610
|
+
}
|