@jupyternaut/persona 0.20.0 → 0.20.1
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/index.d.ts +3 -0
- package/lib/chat-commands/index.js +2 -0
- package/lib/chat-commands/skills.d.ts +25 -0
- package/lib/chat-commands/skills.js +73 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +74 -3
- package/lib/persona.d.ts +1 -0
- package/lib/persona.js +65 -33
- package/lib/tokens.d.ts +4 -0
- package/package.json +11 -2
- package/src/chat-commands/index.ts +2 -0
- package/src/chat-commands/skills.ts +105 -0
- package/src/index.ts +123 -25
- package/src/persona.ts +68 -41
- package/src/tokens.ts +4 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ChatCommand, IChatCommandProvider, IInputModel } from '@jupyter/chat';
|
|
2
|
+
import type { ISkillRegistry } from '@jupyternaut/agent';
|
|
3
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
4
|
+
export declare class SkillsCommandProvider implements IChatCommandProvider {
|
|
5
|
+
constructor(options: SkillsCommandProvider.IOptions);
|
|
6
|
+
id: string;
|
|
7
|
+
listCommandCompletions(inputModel: IInputModel): Promise<ChatCommand[]>;
|
|
8
|
+
onSubmit(inputModel: IInputModel): Promise<void>;
|
|
9
|
+
private _isActive;
|
|
10
|
+
private _command;
|
|
11
|
+
private _regex;
|
|
12
|
+
private _commands;
|
|
13
|
+
private _skillRegistry;
|
|
14
|
+
private _sendSystemMessage;
|
|
15
|
+
private _isDefault;
|
|
16
|
+
}
|
|
17
|
+
export declare namespace SkillsCommandProvider {
|
|
18
|
+
interface IOptions {
|
|
19
|
+
skillRegistry: ISkillRegistry;
|
|
20
|
+
commands: CommandRegistry;
|
|
21
|
+
sendSystemMessage: (chatName: string, body: string) => void;
|
|
22
|
+
isDefault: (chatName: string) => boolean;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=skills.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CommandIds } from '../tokens';
|
|
2
|
+
export class SkillsCommandProvider {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
this._skillRegistry = options.skillRegistry;
|
|
5
|
+
this._commands = options.commands;
|
|
6
|
+
this._sendSystemMessage = options.sendSystemMessage;
|
|
7
|
+
this._isDefault = options.isDefault;
|
|
8
|
+
}
|
|
9
|
+
id = '@jupyternaut/persona:skills-command';
|
|
10
|
+
async listCommandCompletions(inputModel) {
|
|
11
|
+
if (!this._isActive(inputModel)) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const match = inputModel.currentWord?.match(this._regex)?.[0];
|
|
15
|
+
if (!match) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
if (this._command.name.startsWith(match)) {
|
|
19
|
+
return [this._command];
|
|
20
|
+
}
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
async onSubmit(inputModel) {
|
|
24
|
+
if (!this._isActive(inputModel)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const trimmed = inputModel.value.trim();
|
|
28
|
+
const match = trimmed.match(/^\/skills(?:\s+(.+))?$/);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (this._commands.hasCommand(CommandIds.refreshSkills)) {
|
|
33
|
+
await this._commands.execute(CommandIds.refreshSkills);
|
|
34
|
+
}
|
|
35
|
+
const query = match[1]?.trim();
|
|
36
|
+
const filtered = this._skillRegistry.listSkills(query);
|
|
37
|
+
let body = '';
|
|
38
|
+
if (filtered.length === 0) {
|
|
39
|
+
body = query
|
|
40
|
+
? `No skills found matching "${query}".`
|
|
41
|
+
: 'No skills are currently registered.';
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const heading = query
|
|
45
|
+
? `Skills matching "${query}" (${filtered.length}):`
|
|
46
|
+
: `Available skills (${filtered.length}):`;
|
|
47
|
+
const lines = filtered.map(skill => `- \`${skill.name}\` — ${skill.description}`);
|
|
48
|
+
body = [heading, '', ...lines].join('\n');
|
|
49
|
+
}
|
|
50
|
+
const chatName = inputModel.chatContext?.name;
|
|
51
|
+
if (chatName) {
|
|
52
|
+
this._sendSystemMessage(chatName, body);
|
|
53
|
+
}
|
|
54
|
+
// Prevent the message to be actually sent.
|
|
55
|
+
inputModel.value = '';
|
|
56
|
+
inputModel.clearAttachments();
|
|
57
|
+
inputModel.clearMentions();
|
|
58
|
+
}
|
|
59
|
+
_isActive(inputModel) {
|
|
60
|
+
const chatName = inputModel.chatContext?.name;
|
|
61
|
+
return !!chatName && this._isDefault(chatName);
|
|
62
|
+
}
|
|
63
|
+
_command = {
|
|
64
|
+
name: '/skills',
|
|
65
|
+
providerId: this.id,
|
|
66
|
+
description: 'List available skills'
|
|
67
|
+
};
|
|
68
|
+
_regex = /^\/\w*$/;
|
|
69
|
+
_commands;
|
|
70
|
+
_skillRegistry;
|
|
71
|
+
_sendSystemMessage;
|
|
72
|
+
_isDefault;
|
|
73
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IAgentManagerFactory, IAISettingsModel, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry } from '@jupyternaut/agent';
|
|
2
1
|
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
import { IAgentManagerFactory, IAISettingsModel, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry } from '@jupyternaut/agent';
|
|
3
3
|
import { IPersonaRegistry } from './tokens';
|
|
4
4
|
declare const _default: (JupyterFrontEndPlugin<IProviderRegistry> | JupyterFrontEndPlugin<void> | JupyterFrontEndPlugin<IPersonaRegistry> | JupyterFrontEndPlugin<IAgentManagerFactory> | JupyterFrontEndPlugin<IAISettingsModel> | JupyterFrontEndPlugin<IDiffManager> | JupyterFrontEndPlugin<ISkillRegistry> | JupyterFrontEndPlugin<IToolRegistry>)[];
|
|
5
5
|
export default _default;
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { anthropicProvider, createBrowserFetchTool, createDiscoverCommandsTool, createDiscoverSkillsTool, createExecuteCommandTool, createLoadSkillTool, genericProvider, googleProvider, loadSkillsFromPaths, mistralProvider, openaiProvider, AgentManagerFactory, IAgentManagerFactory, IAISettingsModel, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry, ProviderRegistry, SECRETS_NAMESPACE, SkillRegistry, ToolRegistry } from '@jupyternaut/agent';
|
|
2
|
-
import { ILayoutRestorer } from '@jupyterlab/application';
|
|
3
1
|
import { IChatTracker, IChatCommandRegistry } from '@jupyter/chat';
|
|
2
|
+
import { ILayoutRestorer } from '@jupyterlab/application';
|
|
4
3
|
import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
|
|
5
4
|
import { ICompletionProviderManager } from '@jupyterlab/completer';
|
|
6
5
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
@@ -9,10 +8,12 @@ import { IStatusBar } from '@jupyterlab/statusbar';
|
|
|
9
8
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
10
9
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
11
10
|
import { IFormRendererRegistry, settingsIcon } from '@jupyterlab/ui-components';
|
|
11
|
+
import { anthropicProvider, createBrowserFetchTool, createDiscoverCommandsTool, createDiscoverSkillsTool, createExecuteCommandTool, createLoadSkillTool, genericProvider, googleProvider, loadSkillsFromPaths, mistralProvider, openaiProvider, AgentManagerFactory, IAgentManagerFactory, IAISettingsModel, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry, ProviderRegistry, SECRETS_NAMESPACE, SkillRegistry, ToolRegistry } from '@jupyternaut/agent';
|
|
12
12
|
import { DisposableSet } from '@lumino/disposable';
|
|
13
|
+
import { IComponentsRendererFactory } from 'jupyter-chat-components';
|
|
13
14
|
import { IMcpManager } from 'jupyter-mcp-manager';
|
|
14
15
|
import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
|
|
15
|
-
import { MentionCommandProvider } from './chat-commands
|
|
16
|
+
import { MentionCommandProvider, SkillsCommandProvider } from './chat-commands';
|
|
16
17
|
import { AICompletionProvider } from './completion';
|
|
17
18
|
import { CompletionStatusWidget } from './components';
|
|
18
19
|
import { DiffManager } from './diff-manager';
|
|
@@ -180,6 +181,41 @@ const persona = {
|
|
|
180
181
|
chatTracker?.widgetAdded.connect((_, widget) => attachPersona(widget));
|
|
181
182
|
}
|
|
182
183
|
};
|
|
184
|
+
/**
|
|
185
|
+
* Registers groupedToolCall callbacks to route tool call approval decisions
|
|
186
|
+
* to the correct persona's agent manager.
|
|
187
|
+
*/
|
|
188
|
+
const chatComponentsCallbacks = {
|
|
189
|
+
id: '@jupyternaut/persona:chat-components-callbacks',
|
|
190
|
+
description: 'Register chat components callbacks for the persona.',
|
|
191
|
+
autoStart: true,
|
|
192
|
+
requires: [IPersonaRegistry],
|
|
193
|
+
optional: [IChatTracker, IComponentsRendererFactory],
|
|
194
|
+
activate: (_app, personaRegistry, chatTracker, chatComponentsFactory) => {
|
|
195
|
+
if (!chatComponentsFactory) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const findPersona = (sessionId) => {
|
|
199
|
+
const model = chatTracker?.find(chat => chat.model.name === sessionId)?.model;
|
|
200
|
+
return model ? personaRegistry.get(model) : undefined;
|
|
201
|
+
};
|
|
202
|
+
chatComponentsFactory.groupedToolCallCallbacks = {
|
|
203
|
+
...chatComponentsFactory.groupedToolCallCallbacks,
|
|
204
|
+
toolCallPermissionDecision: (sessionId, toolCallId, optionId) => {
|
|
205
|
+
const agent = findPersona(sessionId)?.agentManager;
|
|
206
|
+
if (!agent) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (optionId === 'approve') {
|
|
210
|
+
agent.approveToolCall(toolCallId);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
agent.rejectToolCall(toolCallId);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
};
|
|
183
219
|
/**
|
|
184
220
|
* Clear chat command plugin.
|
|
185
221
|
*/
|
|
@@ -192,6 +228,39 @@ const mentionCommandPlugin = {
|
|
|
192
228
|
registry.addProvider(new MentionCommandProvider());
|
|
193
229
|
}
|
|
194
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* Skills chat command plugin.
|
|
233
|
+
*/
|
|
234
|
+
const skillsCommandPlugin = {
|
|
235
|
+
id: '@jupyternaut/persona:skills-command',
|
|
236
|
+
description: 'Register the /skills chat command.',
|
|
237
|
+
autoStart: true,
|
|
238
|
+
requires: [IChatCommandRegistry, ISkillRegistry, IPersonaRegistry],
|
|
239
|
+
optional: [IChatTracker],
|
|
240
|
+
activate: (app, registry, skillRegistry, personaRegistry, chatTracker) => {
|
|
241
|
+
const findModel = (chatName) => chatTracker?.find(c => c.model.name === chatName)?.model;
|
|
242
|
+
const isDefault = (chatName) => {
|
|
243
|
+
const model = findModel(chatName);
|
|
244
|
+
if (!model) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
const persona = personaRegistry.get(model);
|
|
248
|
+
return persona ? !persona.requireMention : false;
|
|
249
|
+
};
|
|
250
|
+
const sendSystemMessage = (chatName, body) => {
|
|
251
|
+
const model = findModel(chatName);
|
|
252
|
+
if (model) {
|
|
253
|
+
personaRegistry.get(model)?.sendSystemMessage(body);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
registry.addProvider(new SkillsCommandProvider({
|
|
257
|
+
skillRegistry,
|
|
258
|
+
commands: app.commands,
|
|
259
|
+
isDefault,
|
|
260
|
+
sendSystemMessage
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
};
|
|
195
264
|
/**
|
|
196
265
|
* A plugin to provide the agent manager factory and completion provider.
|
|
197
266
|
* These objects require the secrets manager token with the same namespace.
|
|
@@ -509,7 +578,9 @@ export default [
|
|
|
509
578
|
// Persona
|
|
510
579
|
personaRegistry,
|
|
511
580
|
persona,
|
|
581
|
+
chatComponentsCallbacks,
|
|
512
582
|
mentionCommandPlugin,
|
|
583
|
+
skillsCommandPlugin,
|
|
513
584
|
// Settings
|
|
514
585
|
settingsModel,
|
|
515
586
|
settingsPanelPlugin,
|
package/lib/persona.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export declare class Persona implements IPersona {
|
|
|
34
34
|
private _handleToolApprovalResolved;
|
|
35
35
|
private _handleError;
|
|
36
36
|
private _updateToolCallUI;
|
|
37
|
+
sendSystemMessage(body: string): void;
|
|
37
38
|
/**
|
|
38
39
|
* Whether a mention is required to trigger a response.
|
|
39
40
|
* When false, the persona responds to all non-bot messages.
|
package/lib/persona.js
CHANGED
|
@@ -106,12 +106,15 @@ export class Persona {
|
|
|
106
106
|
this._settingsModel = options.settingsModel;
|
|
107
107
|
this._providerRegistry = options.providerRegistry;
|
|
108
108
|
this._documentManager = options.documentManager;
|
|
109
|
-
for (const message of options.model.messages) {
|
|
110
|
-
this._respondedToIds.add(message.id);
|
|
111
|
-
}
|
|
112
109
|
this._agent.agentEvent.connect(this._onAgentEvent, this);
|
|
113
110
|
this._agent.activeProviderChanged.connect(this._onActiveProviderChanged, this);
|
|
114
|
-
|
|
111
|
+
// Wait for the chat to be ready before connect to message update.
|
|
112
|
+
this._model.ready.then(() => {
|
|
113
|
+
for (const message of this._model.messages) {
|
|
114
|
+
this._respondedToIds.add(message.id);
|
|
115
|
+
}
|
|
116
|
+
this._model.messagesUpdated.connect(this._onMessagesUpdated, this);
|
|
117
|
+
});
|
|
115
118
|
this._model.disposed.connect(this.dispose, this);
|
|
116
119
|
}
|
|
117
120
|
dispose() {
|
|
@@ -241,13 +244,29 @@ export class Persona {
|
|
|
241
244
|
_handleMessageChunk(event) {
|
|
242
245
|
const streamingMessage = this._streamingMessage.get(event.data.messageId);
|
|
243
246
|
if (streamingMessage) {
|
|
244
|
-
|
|
247
|
+
if (!this._model.updateMessage) {
|
|
248
|
+
streamingMessage.update({ body: event.data.fullContent });
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
this._model.updateMessage(streamingMessage.id, {
|
|
252
|
+
...streamingMessage.content,
|
|
253
|
+
body: event.data.fullContent
|
|
254
|
+
});
|
|
255
|
+
}
|
|
245
256
|
}
|
|
246
257
|
}
|
|
247
258
|
_handleMessageComplete(event) {
|
|
248
259
|
const streamingMessage = this._streamingMessage.get(event.data.messageId);
|
|
249
260
|
if (streamingMessage) {
|
|
250
|
-
|
|
261
|
+
if (!this._model.updateMessage) {
|
|
262
|
+
streamingMessage.update({ body: event.data.content });
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
this._model.updateMessage(streamingMessage.id, {
|
|
266
|
+
...streamingMessage.content,
|
|
267
|
+
body: event.data.content
|
|
268
|
+
});
|
|
269
|
+
}
|
|
251
270
|
this._streamingMessage.delete(event.data.messageId);
|
|
252
271
|
}
|
|
253
272
|
}
|
|
@@ -363,34 +382,47 @@ export class Persona {
|
|
|
363
382
|
}
|
|
364
383
|
context.status = status;
|
|
365
384
|
const displayName = context.title ?? context.toolName;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
]
|
|
392
|
-
}
|
|
385
|
+
const mime_model = {
|
|
386
|
+
data: {
|
|
387
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
388
|
+
},
|
|
389
|
+
metadata: {
|
|
390
|
+
toolCalls: [
|
|
391
|
+
{
|
|
392
|
+
toolCallId: context.toolCallId,
|
|
393
|
+
title: context.summary
|
|
394
|
+
? `${displayName} : ${context.summary}`
|
|
395
|
+
: displayName,
|
|
396
|
+
kind: context.toolName,
|
|
397
|
+
status: context.status,
|
|
398
|
+
rawInput: context.input,
|
|
399
|
+
rawOutput: output,
|
|
400
|
+
sessionId: this._model.name,
|
|
401
|
+
permissionStatus: status === 'awaiting_approval' ? 'pending' : 'resolved',
|
|
402
|
+
...(status === 'awaiting_approval' && {
|
|
403
|
+
permissionOptions: [
|
|
404
|
+
{ optionId: 'approve', name: 'Approve', kind: 'allow_once' },
|
|
405
|
+
{ optionId: 'reject', name: 'Reject', kind: 'reject_once' }
|
|
406
|
+
]
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
]
|
|
393
410
|
}
|
|
411
|
+
};
|
|
412
|
+
if (!this._model.updateMessage) {
|
|
413
|
+
message.update({ mime_model });
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
this._model.updateMessage(message.id, {
|
|
417
|
+
...message.content,
|
|
418
|
+
mime_model
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
sendSystemMessage(body) {
|
|
423
|
+
this._model.sendMessage({
|
|
424
|
+
body,
|
|
425
|
+
sender: this._persona
|
|
394
426
|
});
|
|
395
427
|
}
|
|
396
428
|
/**
|
package/lib/tokens.d.ts
CHANGED
|
@@ -51,6 +51,10 @@ export interface IPersona {
|
|
|
51
51
|
* Called after restoring a saved chat.
|
|
52
52
|
*/
|
|
53
53
|
rebuildHistory(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Adds an assistant message to the chat (used by chat commands).
|
|
56
|
+
*/
|
|
57
|
+
sendSystemMessage(body: string): void;
|
|
54
58
|
}
|
|
55
59
|
/**
|
|
56
60
|
* Registry mapping chat models to their persona.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyternaut/persona",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "AI code completions and chat for JupyterLite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@jupyterlab/statusbar": "^4.5.8",
|
|
59
59
|
"@jupyterlab/translation": "^4.5.8",
|
|
60
60
|
"@jupyterlab/ui-components": "^4.5.8",
|
|
61
|
-
"@jupyternaut/agent": "^0.20.
|
|
61
|
+
"@jupyternaut/agent": "^0.20.1",
|
|
62
62
|
"@lumino/commands": "^2.3.2",
|
|
63
63
|
"@lumino/coreutils": "^2.2.1",
|
|
64
64
|
"@lumino/disposable": "^2.1.4",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@mui/icons-material": "^7",
|
|
68
68
|
"@mui/material": "^7",
|
|
69
69
|
"ai": "^7.0.106",
|
|
70
|
+
"jupyter-chat-components": "^0.6.0",
|
|
70
71
|
"jupyter-mcp-manager": "^0.2.0",
|
|
71
72
|
"jupyter-secrets-manager": "^0.5.0",
|
|
72
73
|
"react": "^18.3.1"
|
|
@@ -101,10 +102,18 @@
|
|
|
101
102
|
"bundled": true,
|
|
102
103
|
"singleton": true
|
|
103
104
|
},
|
|
105
|
+
"@jupyter/chat": {
|
|
106
|
+
"bundled": false,
|
|
107
|
+
"singleton": true
|
|
108
|
+
},
|
|
104
109
|
"@jupyternaut/agent": {
|
|
105
110
|
"bundled": true,
|
|
106
111
|
"singleton": true
|
|
107
112
|
},
|
|
113
|
+
"jupyter-chat-components": {
|
|
114
|
+
"bundled": false,
|
|
115
|
+
"singleton": true
|
|
116
|
+
},
|
|
108
117
|
"jupyter-mcp-manager": {
|
|
109
118
|
"bundled": false,
|
|
110
119
|
"singleton": true
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ChatCommand, IChatCommandProvider, IInputModel } from '@jupyter/chat';
|
|
2
|
+
import type { ISkillRegistry } from '@jupyternaut/agent';
|
|
3
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
4
|
+
|
|
5
|
+
import { CommandIds } from '../tokens';
|
|
6
|
+
|
|
7
|
+
export class SkillsCommandProvider implements IChatCommandProvider {
|
|
8
|
+
constructor(options: SkillsCommandProvider.IOptions) {
|
|
9
|
+
this._skillRegistry = options.skillRegistry;
|
|
10
|
+
this._commands = options.commands;
|
|
11
|
+
this._sendSystemMessage = options.sendSystemMessage;
|
|
12
|
+
this._isDefault = options.isDefault;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public id: string = '@jupyternaut/persona:skills-command';
|
|
16
|
+
|
|
17
|
+
async listCommandCompletions(
|
|
18
|
+
inputModel: IInputModel
|
|
19
|
+
): Promise<ChatCommand[]> {
|
|
20
|
+
if (!this._isActive(inputModel)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const match = inputModel.currentWord?.match(this._regex)?.[0];
|
|
25
|
+
if (!match) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (this._command.name.startsWith(match)) {
|
|
30
|
+
return [this._command];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async onSubmit(inputModel: IInputModel): Promise<void> {
|
|
37
|
+
if (!this._isActive(inputModel)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const trimmed = inputModel.value.trim();
|
|
42
|
+
const match = trimmed.match(/^\/skills(?:\s+(.+))?$/);
|
|
43
|
+
if (!match) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (this._commands.hasCommand(CommandIds.refreshSkills)) {
|
|
48
|
+
await this._commands.execute(CommandIds.refreshSkills);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const query = match[1]?.trim();
|
|
52
|
+
const filtered = this._skillRegistry.listSkills(query);
|
|
53
|
+
|
|
54
|
+
let body = '';
|
|
55
|
+
if (filtered.length === 0) {
|
|
56
|
+
body = query
|
|
57
|
+
? `No skills found matching "${query}".`
|
|
58
|
+
: 'No skills are currently registered.';
|
|
59
|
+
} else {
|
|
60
|
+
const heading = query
|
|
61
|
+
? `Skills matching "${query}" (${filtered.length}):`
|
|
62
|
+
: `Available skills (${filtered.length}):`;
|
|
63
|
+
const lines = filtered.map(
|
|
64
|
+
skill => `- \`${skill.name}\` — ${skill.description}`
|
|
65
|
+
);
|
|
66
|
+
body = [heading, '', ...lines].join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const chatName = inputModel.chatContext?.name;
|
|
70
|
+
if (chatName) {
|
|
71
|
+
this._sendSystemMessage(chatName, body);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Prevent the message to be actually sent.
|
|
75
|
+
inputModel.value = '';
|
|
76
|
+
inputModel.clearAttachments();
|
|
77
|
+
inputModel.clearMentions();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private _isActive(inputModel: IInputModel): boolean {
|
|
81
|
+
const chatName = inputModel.chatContext?.name;
|
|
82
|
+
return !!chatName && this._isDefault(chatName);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private _command: ChatCommand = {
|
|
86
|
+
name: '/skills',
|
|
87
|
+
providerId: this.id,
|
|
88
|
+
description: 'List available skills'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
private _regex: RegExp = /^\/\w*$/;
|
|
92
|
+
private _commands: CommandRegistry;
|
|
93
|
+
private _skillRegistry: ISkillRegistry;
|
|
94
|
+
private _sendSystemMessage: (chatName: string, body: string) => void;
|
|
95
|
+
private _isDefault: (chatName: string) => boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export namespace SkillsCommandProvider {
|
|
99
|
+
export interface IOptions {
|
|
100
|
+
skillRegistry: ISkillRegistry;
|
|
101
|
+
commands: CommandRegistry;
|
|
102
|
+
sendSystemMessage: (chatName: string, body: string) => void;
|
|
103
|
+
isDefault: (chatName: string) => boolean;
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
import { IChatTracker, IChatPanel, IChatCommandRegistry } from '@jupyter/chat';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ILayoutRestorer,
|
|
5
|
+
JupyterFrontEnd,
|
|
6
|
+
JupyterFrontEndPlugin
|
|
7
|
+
} from '@jupyterlab/application';
|
|
8
|
+
|
|
9
|
+
import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
|
|
10
|
+
|
|
11
|
+
import { ICompletionProviderManager } from '@jupyterlab/completer';
|
|
12
|
+
|
|
13
|
+
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
14
|
+
|
|
15
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
16
|
+
|
|
17
|
+
import { IStatusBar } from '@jupyterlab/statusbar';
|
|
18
|
+
|
|
19
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
20
|
+
|
|
21
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
22
|
+
|
|
23
|
+
import { IFormRendererRegistry, settingsIcon } from '@jupyterlab/ui-components';
|
|
24
|
+
|
|
1
25
|
import {
|
|
2
26
|
anthropicProvider,
|
|
3
27
|
createBrowserFetchTool,
|
|
@@ -25,37 +49,15 @@ import {
|
|
|
25
49
|
|
|
26
50
|
import type { IAISecretsAccess } from '@jupyternaut/agent';
|
|
27
51
|
|
|
28
|
-
import {
|
|
29
|
-
ILayoutRestorer,
|
|
30
|
-
JupyterFrontEnd,
|
|
31
|
-
JupyterFrontEndPlugin
|
|
32
|
-
} from '@jupyterlab/application';
|
|
33
|
-
|
|
34
|
-
import { IChatTracker, IChatPanel, IChatCommandRegistry } from '@jupyter/chat';
|
|
35
|
-
|
|
36
|
-
import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
|
|
37
|
-
|
|
38
|
-
import { ICompletionProviderManager } from '@jupyterlab/completer';
|
|
39
|
-
|
|
40
|
-
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
41
|
-
|
|
42
|
-
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
43
|
-
|
|
44
|
-
import { IStatusBar } from '@jupyterlab/statusbar';
|
|
45
|
-
|
|
46
|
-
import { PathExt } from '@jupyterlab/coreutils';
|
|
47
|
-
|
|
48
|
-
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
49
|
-
|
|
50
|
-
import { IFormRendererRegistry, settingsIcon } from '@jupyterlab/ui-components';
|
|
51
|
-
|
|
52
52
|
import { DisposableSet } from '@lumino/disposable';
|
|
53
53
|
|
|
54
|
+
import { IComponentsRendererFactory } from 'jupyter-chat-components';
|
|
55
|
+
|
|
54
56
|
import { IMcpManager } from 'jupyter-mcp-manager';
|
|
55
57
|
|
|
56
58
|
import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
|
|
57
59
|
|
|
58
|
-
import { MentionCommandProvider } from './chat-commands
|
|
60
|
+
import { MentionCommandProvider, SkillsCommandProvider } from './chat-commands';
|
|
59
61
|
|
|
60
62
|
import { AICompletionProvider } from './completion';
|
|
61
63
|
|
|
@@ -271,6 +273,54 @@ const persona: JupyterFrontEndPlugin<void> = {
|
|
|
271
273
|
}
|
|
272
274
|
};
|
|
273
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Registers groupedToolCall callbacks to route tool call approval decisions
|
|
278
|
+
* to the correct persona's agent manager.
|
|
279
|
+
*/
|
|
280
|
+
const chatComponentsCallbacks: JupyterFrontEndPlugin<void> = {
|
|
281
|
+
id: '@jupyternaut/persona:chat-components-callbacks',
|
|
282
|
+
description: 'Register chat components callbacks for the persona.',
|
|
283
|
+
autoStart: true,
|
|
284
|
+
requires: [IPersonaRegistry],
|
|
285
|
+
optional: [IChatTracker, IComponentsRendererFactory],
|
|
286
|
+
activate: (
|
|
287
|
+
_app: JupyterFrontEnd,
|
|
288
|
+
personaRegistry: IPersonaRegistry,
|
|
289
|
+
chatTracker: IChatTracker | null,
|
|
290
|
+
chatComponentsFactory?: IComponentsRendererFactory
|
|
291
|
+
): void => {
|
|
292
|
+
if (!chatComponentsFactory) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const findPersona = (sessionId: string) => {
|
|
297
|
+
const model = chatTracker?.find(
|
|
298
|
+
chat => chat.model.name === sessionId
|
|
299
|
+
)?.model;
|
|
300
|
+
return model ? personaRegistry.get(model) : undefined;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
chatComponentsFactory.groupedToolCallCallbacks = {
|
|
304
|
+
...chatComponentsFactory.groupedToolCallCallbacks,
|
|
305
|
+
toolCallPermissionDecision: (
|
|
306
|
+
sessionId: string,
|
|
307
|
+
toolCallId: string,
|
|
308
|
+
optionId: string
|
|
309
|
+
) => {
|
|
310
|
+
const agent = findPersona(sessionId)?.agentManager;
|
|
311
|
+
if (!agent) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (optionId === 'approve') {
|
|
315
|
+
agent.approveToolCall(toolCallId);
|
|
316
|
+
} else {
|
|
317
|
+
agent.rejectToolCall(toolCallId);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
274
324
|
/**
|
|
275
325
|
* Clear chat command plugin.
|
|
276
326
|
*/
|
|
@@ -284,6 +334,52 @@ const mentionCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
284
334
|
}
|
|
285
335
|
};
|
|
286
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Skills chat command plugin.
|
|
339
|
+
*/
|
|
340
|
+
const skillsCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
341
|
+
id: '@jupyternaut/persona:skills-command',
|
|
342
|
+
description: 'Register the /skills chat command.',
|
|
343
|
+
autoStart: true,
|
|
344
|
+
requires: [IChatCommandRegistry, ISkillRegistry, IPersonaRegistry],
|
|
345
|
+
optional: [IChatTracker],
|
|
346
|
+
activate: (
|
|
347
|
+
app: JupyterFrontEnd,
|
|
348
|
+
registry: IChatCommandRegistry,
|
|
349
|
+
skillRegistry: ISkillRegistry,
|
|
350
|
+
personaRegistry: IPersonaRegistry,
|
|
351
|
+
chatTracker: IChatTracker | null
|
|
352
|
+
) => {
|
|
353
|
+
const findModel = (chatName: string) =>
|
|
354
|
+
chatTracker?.find(c => c.model.name === chatName)?.model;
|
|
355
|
+
|
|
356
|
+
const isDefault = (chatName: string) => {
|
|
357
|
+
const model = findModel(chatName);
|
|
358
|
+
if (!model) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
const persona = personaRegistry.get(model);
|
|
362
|
+
return persona ? !persona.requireMention : false;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const sendSystemMessage = (chatName: string, body: string) => {
|
|
366
|
+
const model = findModel(chatName);
|
|
367
|
+
if (model) {
|
|
368
|
+
personaRegistry.get(model)?.sendSystemMessage(body);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
registry.addProvider(
|
|
373
|
+
new SkillsCommandProvider({
|
|
374
|
+
skillRegistry,
|
|
375
|
+
commands: app.commands,
|
|
376
|
+
isDefault,
|
|
377
|
+
sendSystemMessage
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
287
383
|
/**
|
|
288
384
|
* A plugin to provide the agent manager factory and completion provider.
|
|
289
385
|
* These objects require the secrets manager token with the same namespace.
|
|
@@ -695,7 +791,9 @@ export default [
|
|
|
695
791
|
// Persona
|
|
696
792
|
personaRegistry,
|
|
697
793
|
persona,
|
|
794
|
+
chatComponentsCallbacks,
|
|
698
795
|
mentionCommandPlugin,
|
|
796
|
+
skillsCommandPlugin,
|
|
699
797
|
// Settings
|
|
700
798
|
settingsModel,
|
|
701
799
|
settingsPanelPlugin,
|
package/src/persona.ts
CHANGED
|
@@ -25,8 +25,6 @@ import {
|
|
|
25
25
|
modelSupportsPdf
|
|
26
26
|
} from '@jupyternaut/agent';
|
|
27
27
|
|
|
28
|
-
import type { IObservableDisposable } from '@lumino/disposable';
|
|
29
|
-
|
|
30
28
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
31
29
|
|
|
32
30
|
import type { ModelMessage, UserContent } from 'ai';
|
|
@@ -173,20 +171,21 @@ export class Persona implements IPersona {
|
|
|
173
171
|
this._providerRegistry = options.providerRegistry;
|
|
174
172
|
this._documentManager = options.documentManager;
|
|
175
173
|
|
|
176
|
-
for (const message of options.model.messages) {
|
|
177
|
-
this._respondedToIds.add(message.id);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
174
|
this._agent.agentEvent.connect(this._onAgentEvent, this);
|
|
181
175
|
this._agent.activeProviderChanged.connect(
|
|
182
176
|
this._onActiveProviderChanged,
|
|
183
177
|
this
|
|
184
178
|
);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
this
|
|
189
|
-
|
|
179
|
+
|
|
180
|
+
// Wait for the chat to be ready before connect to message update.
|
|
181
|
+
this._model.ready.then(() => {
|
|
182
|
+
for (const message of this._model.messages) {
|
|
183
|
+
this._respondedToIds.add(message.id);
|
|
184
|
+
}
|
|
185
|
+
this._model.messagesUpdated.connect(this._onMessagesUpdated, this);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
this._model.disposed.connect(this.dispose, this);
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
dispose(): void {
|
|
@@ -378,7 +377,14 @@ export class Persona implements IPersona {
|
|
|
378
377
|
): void {
|
|
379
378
|
const streamingMessage = this._streamingMessage.get(event.data.messageId);
|
|
380
379
|
if (streamingMessage) {
|
|
381
|
-
|
|
380
|
+
if (!this._model.updateMessage) {
|
|
381
|
+
streamingMessage.update({ body: event.data.fullContent });
|
|
382
|
+
} else {
|
|
383
|
+
this._model.updateMessage(streamingMessage.id, {
|
|
384
|
+
...streamingMessage.content,
|
|
385
|
+
body: event.data.fullContent
|
|
386
|
+
});
|
|
387
|
+
}
|
|
382
388
|
}
|
|
383
389
|
}
|
|
384
390
|
|
|
@@ -387,7 +393,15 @@ export class Persona implements IPersona {
|
|
|
387
393
|
): void {
|
|
388
394
|
const streamingMessage = this._streamingMessage.get(event.data.messageId);
|
|
389
395
|
if (streamingMessage) {
|
|
390
|
-
|
|
396
|
+
if (!this._model.updateMessage) {
|
|
397
|
+
streamingMessage.update({ body: event.data.content });
|
|
398
|
+
} else {
|
|
399
|
+
this._model.updateMessage(streamingMessage.id, {
|
|
400
|
+
...streamingMessage.content,
|
|
401
|
+
body: event.data.content
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
391
405
|
this._streamingMessage.delete(event.data.messageId);
|
|
392
406
|
}
|
|
393
407
|
}
|
|
@@ -545,35 +559,48 @@ export class Persona implements IPersona {
|
|
|
545
559
|
}
|
|
546
560
|
context.status = status;
|
|
547
561
|
const displayName = context.title ?? context.toolName;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
]
|
|
575
|
-
}
|
|
562
|
+
const mime_model = {
|
|
563
|
+
data: {
|
|
564
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
565
|
+
},
|
|
566
|
+
metadata: {
|
|
567
|
+
toolCalls: [
|
|
568
|
+
{
|
|
569
|
+
toolCallId: context.toolCallId,
|
|
570
|
+
title: context.summary
|
|
571
|
+
? `${displayName} : ${context.summary}`
|
|
572
|
+
: displayName,
|
|
573
|
+
kind: context.toolName,
|
|
574
|
+
status: context.status,
|
|
575
|
+
rawInput: context.input,
|
|
576
|
+
rawOutput: output,
|
|
577
|
+
sessionId: this._model.name,
|
|
578
|
+
permissionStatus:
|
|
579
|
+
status === 'awaiting_approval' ? 'pending' : 'resolved',
|
|
580
|
+
...(status === 'awaiting_approval' && {
|
|
581
|
+
permissionOptions: [
|
|
582
|
+
{ optionId: 'approve', name: 'Approve', kind: 'allow_once' },
|
|
583
|
+
{ optionId: 'reject', name: 'Reject', kind: 'reject_once' }
|
|
584
|
+
]
|
|
585
|
+
})
|
|
586
|
+
}
|
|
587
|
+
]
|
|
576
588
|
}
|
|
589
|
+
};
|
|
590
|
+
if (!this._model.updateMessage) {
|
|
591
|
+
message.update({ mime_model });
|
|
592
|
+
} else {
|
|
593
|
+
this._model.updateMessage(message.id, {
|
|
594
|
+
...message.content,
|
|
595
|
+
mime_model
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
sendSystemMessage(body: string): void {
|
|
601
|
+
this._model.sendMessage({
|
|
602
|
+
body,
|
|
603
|
+
sender: this._persona
|
|
577
604
|
});
|
|
578
605
|
}
|
|
579
606
|
|
package/src/tokens.ts
CHANGED