@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.
Files changed (49) hide show
  1. package/lib/chat-commands/mention.d.ts +9 -0
  2. package/lib/chat-commands/mention.js +30 -0
  3. package/lib/completion/completion-provider.d.ts +86 -0
  4. package/lib/completion/completion-provider.js +246 -0
  5. package/lib/completion/index.d.ts +2 -0
  6. package/lib/completion/index.js +1 -0
  7. package/lib/components/completion-status.d.ts +26 -0
  8. package/lib/components/completion-status.js +52 -0
  9. package/lib/components/index.d.ts +2 -0
  10. package/lib/components/index.js +1 -0
  11. package/lib/diff-manager.d.ts +25 -0
  12. package/lib/diff-manager.js +60 -0
  13. package/lib/index.d.ts +8 -0
  14. package/lib/index.js +522 -0
  15. package/lib/models/settings-model.d.ts +36 -0
  16. package/lib/models/settings-model.js +356 -0
  17. package/lib/persona-registry.d.ts +15 -0
  18. package/lib/persona-registry.js +29 -0
  19. package/lib/persona.d.ts +66 -0
  20. package/lib/persona.js +414 -0
  21. package/lib/process-attachments.d.ts +5 -0
  22. package/lib/process-attachments.js +287 -0
  23. package/lib/tokens.d.ts +101 -0
  24. package/lib/tokens.js +20 -0
  25. package/lib/widgets/ai-settings.d.ts +54 -0
  26. package/lib/widgets/ai-settings.js +572 -0
  27. package/lib/widgets/provider-config-dialog.d.ts +16 -0
  28. package/lib/widgets/provider-config-dialog.js +384 -0
  29. package/package.json +111 -7
  30. package/schema/settings-model.json +287 -0
  31. package/src/chat-commands/mention.tsx +46 -0
  32. package/src/completion/completion-provider.ts +350 -0
  33. package/src/completion/index.ts +1 -0
  34. package/src/components/completion-status.tsx +93 -0
  35. package/src/components/index.ts +1 -0
  36. package/src/diff-manager.ts +81 -0
  37. package/src/index.ts +710 -0
  38. package/src/models/settings-model.ts +415 -0
  39. package/src/persona-registry.ts +46 -0
  40. package/src/persona.ts +610 -0
  41. package/src/process-attachments.ts +369 -0
  42. package/src/tokens.ts +121 -0
  43. package/src/widgets/ai-settings.tsx +1308 -0
  44. package/src/widgets/provider-config-dialog.tsx +997 -0
  45. package/style/base.css +14 -0
  46. package/style/index.css +1 -0
  47. package/style/index.js +1 -0
  48. package/README.md +0 -3
  49. package/index.js +0 -1
@@ -0,0 +1,287 @@
1
+ import { PathExt } from '@jupyterlab/coreutils';
2
+ export async function processAttachments(attachments, documentManager, body, supportsImages, supportsPdf, supportsAudio) {
3
+ const textContents = [];
4
+ const includedParts = [];
5
+ const omittedNames = [];
6
+ if (!documentManager) {
7
+ return body;
8
+ }
9
+ for (const attachment of attachments) {
10
+ try {
11
+ if (attachment.type === 'notebook' && attachment.cells?.length) {
12
+ const cellContents = await readNotebookCells(attachment, documentManager);
13
+ if (cellContents) {
14
+ textContents.push(cellContents);
15
+ }
16
+ }
17
+ else {
18
+ let mimetype = attachment.mimetype;
19
+ const fileExtension = PathExt.extname(attachment.value).toLowerCase();
20
+ if (!mimetype) {
21
+ try {
22
+ const diskModel = await documentManager.services.contents.get(attachment.value, { content: false });
23
+ mimetype = diskModel?.mimetype;
24
+ }
25
+ catch (e) {
26
+ console.warn(`Failed to fetch metadata for ${attachment.value}:`, e);
27
+ }
28
+ }
29
+ if (mimetype?.startsWith('image/')) {
30
+ if (supportsImages) {
31
+ const data = await readBinaryAttachment(attachment, documentManager);
32
+ if (data) {
33
+ includedParts.push({
34
+ type: 'file',
35
+ data,
36
+ mediaType: mimetype
37
+ });
38
+ }
39
+ }
40
+ else {
41
+ omittedNames.push(PathExt.basename(attachment.value));
42
+ }
43
+ }
44
+ else if (mimetype === 'application/pdf') {
45
+ if (supportsPdf) {
46
+ const data = await readBinaryAttachment(attachment, documentManager);
47
+ if (data) {
48
+ includedParts.push({
49
+ type: 'file',
50
+ data,
51
+ mediaType: mimetype,
52
+ filename: PathExt.basename(attachment.value)
53
+ });
54
+ }
55
+ }
56
+ else {
57
+ omittedNames.push(PathExt.basename(attachment.value));
58
+ }
59
+ }
60
+ else if (mimetype?.startsWith('audio/')) {
61
+ if (supportsAudio) {
62
+ const data = await readBinaryAttachment(attachment, documentManager);
63
+ if (data) {
64
+ includedParts.push({
65
+ type: 'file',
66
+ data,
67
+ mediaType: mimetype,
68
+ filename: PathExt.basename(attachment.value)
69
+ });
70
+ }
71
+ }
72
+ else {
73
+ omittedNames.push(PathExt.basename(attachment.value));
74
+ }
75
+ }
76
+ else {
77
+ const fileContent = await readFileAttachment(attachment, documentManager);
78
+ if (fileContent) {
79
+ const language = fileExtension === '.ipynb' ||
80
+ mimetype === 'application/x-ipynb+json'
81
+ ? 'json'
82
+ : '';
83
+ textContents.push(`**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ catch (error) {
89
+ console.warn(`Failed to read attachment ${attachment.value}:`, error);
90
+ textContents.push(`**File: ${attachment.value}** (Could not read file)`);
91
+ }
92
+ }
93
+ let textPart = body;
94
+ if (textContents.length > 0) {
95
+ textPart += '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
96
+ }
97
+ if (omittedNames.length > 0) {
98
+ textPart += `\n[Attachments omitted (not supported by this model): ${omittedNames.join(', ')}.]`;
99
+ }
100
+ return includedParts.length > 0
101
+ ? [{ type: 'text', text: textPart }, ...includedParts]
102
+ : textPart;
103
+ }
104
+ async function readBinaryAttachment(attachment, documentManager) {
105
+ try {
106
+ const diskModel = await documentManager.services.contents.get(attachment.value, { content: true });
107
+ if (diskModel?.content && diskModel.format === 'base64') {
108
+ return diskModel.content.replace(/\s/g, '');
109
+ }
110
+ return null;
111
+ }
112
+ catch (error) {
113
+ console.warn(`Failed to read binary attachment ${attachment.value}:`, error);
114
+ return null;
115
+ }
116
+ }
117
+ async function readNotebookCells(attachment, documentManager) {
118
+ if (attachment.type !== 'notebook' || !attachment.cells) {
119
+ return null;
120
+ }
121
+ try {
122
+ const widget = documentManager.findWidget(attachment.value);
123
+ let cellData;
124
+ let kernelLang = 'text';
125
+ const ymodel = widget?.context.model.sharedModel;
126
+ if (ymodel) {
127
+ const nb = ymodel.toJSON();
128
+ cellData = nb.cells;
129
+ const lang = nb.metadata.language_info?.name ||
130
+ nb.metadata.kernelspec?.language ||
131
+ 'text';
132
+ kernelLang = String(lang);
133
+ }
134
+ else {
135
+ const model = await documentManager.services.contents.get(attachment.value, { content: true });
136
+ if (!model || model.type !== 'notebook') {
137
+ return null;
138
+ }
139
+ cellData = model.content.cells ?? [];
140
+ kernelLang =
141
+ model.content.metadata.language_info?.name ||
142
+ model.content.metadata.kernelspec?.language ||
143
+ 'text';
144
+ }
145
+ const selectedCells = attachment.cells
146
+ .map(cellInfo => {
147
+ const cell = cellData.find(c => c.id === cellInfo.id);
148
+ if (!cell) {
149
+ return null;
150
+ }
151
+ const code = cell.source || '';
152
+ const cellType = cell.cell_type;
153
+ const lang = cellType === 'code' ? kernelLang : cellType;
154
+ let outputs = '';
155
+ if (cellType === 'code' && Array.isArray(cell.outputs)) {
156
+ const outputsArray = cell.outputs;
157
+ outputs = outputsArray
158
+ .map(output => {
159
+ if (output.output_type === 'stream') {
160
+ return output.text;
161
+ }
162
+ else if (output.output_type === 'error') {
163
+ const err = output;
164
+ return `${err.ename}: ${err.evalue}\n${(err.traceback || []).join('\n')}`;
165
+ }
166
+ else if (output.output_type === 'execute_result' ||
167
+ output.output_type === 'display_data') {
168
+ const data = output.data;
169
+ if (!data) {
170
+ return '';
171
+ }
172
+ try {
173
+ return extractDisplay(data);
174
+ }
175
+ catch (e) {
176
+ console.error('Cannot extract cell output', e);
177
+ return '';
178
+ }
179
+ }
180
+ return '';
181
+ })
182
+ .filter(Boolean)
183
+ .join('\n---\n');
184
+ if (outputs.length > 2000) {
185
+ outputs = outputs.slice(0, 2000) + '\n...[truncated]';
186
+ }
187
+ }
188
+ return (`**Cell [${cellInfo.id}] (${cellType}):**\n` +
189
+ `\`\`\`${lang}\n${code}\n\`\`\`` +
190
+ (outputs ? `\n**Outputs:**\n\`\`\`text\n${outputs}\n\`\`\`` : ''));
191
+ })
192
+ .filter(Boolean)
193
+ .join('\n\n');
194
+ return `**Notebook: ${attachment.value}**\n${selectedCells}`;
195
+ }
196
+ catch (error) {
197
+ console.warn(`Failed to read notebook cells from ${attachment.value}:`, error);
198
+ return null;
199
+ }
200
+ }
201
+ async function readFileAttachment(attachment, documentManager) {
202
+ if (attachment.type !== 'file' && attachment.type !== 'notebook') {
203
+ return null;
204
+ }
205
+ try {
206
+ const widget = documentManager.findWidget(attachment.value);
207
+ if (widget?.context?.model) {
208
+ const ymodel = widget.context.model.sharedModel;
209
+ if (typeof ymodel.getSource === 'function') {
210
+ const source = ymodel.getSource();
211
+ return typeof source === 'string'
212
+ ? source
213
+ : JSON.stringify(source, null, 2);
214
+ }
215
+ }
216
+ const diskModel = await documentManager.services.contents.get(attachment.value, { content: true });
217
+ if (!diskModel?.content) {
218
+ return null;
219
+ }
220
+ if (diskModel.type === 'file') {
221
+ return diskModel.content;
222
+ }
223
+ if (diskModel.type === 'notebook') {
224
+ const cleaned = {
225
+ ...diskModel,
226
+ cells: diskModel.content.cells.map((cell) => ({
227
+ ...cell,
228
+ outputs: [],
229
+ execution_count: null
230
+ }))
231
+ };
232
+ return JSON.stringify(cleaned);
233
+ }
234
+ return null;
235
+ }
236
+ catch (error) {
237
+ console.warn(`Failed to read file ${attachment.value}:`, error);
238
+ return null;
239
+ }
240
+ }
241
+ function extractDisplay(data) {
242
+ const DISPLAY_PRIORITY = [
243
+ 'application/vnd.jupyter.widget-view+json',
244
+ 'application/javascript',
245
+ 'text/html',
246
+ 'image/svg+xml',
247
+ 'image/png',
248
+ 'image/jpeg',
249
+ 'text/markdown',
250
+ 'text/latex',
251
+ 'text/plain'
252
+ ];
253
+ for (const mime of DISPLAY_PRIORITY) {
254
+ if (!(mime in data)) {
255
+ continue;
256
+ }
257
+ const value = data[mime];
258
+ if (!value) {
259
+ continue;
260
+ }
261
+ switch (mime) {
262
+ case 'application/vnd.jupyter.widget-view+json':
263
+ return `Widget: ${value.model_id ?? 'unknown model'}`;
264
+ case 'image/png':
265
+ return `![image](data:image/png;base64,${String(value).slice(0, 100)}...)`;
266
+ case 'image/jpeg':
267
+ return `![image](data:image/jpeg;base64,${String(value).slice(0, 100)}...)`;
268
+ case 'image/svg+xml':
269
+ return String(value).slice(0, 500) + '...\n[svg truncated]';
270
+ case 'text/html':
271
+ return (String(value).slice(0, 1000) +
272
+ (String(value).length > 1000 ? '\n...[truncated]' : ''));
273
+ case 'text/markdown':
274
+ case 'text/latex':
275
+ case 'text/plain': {
276
+ let text = Array.isArray(value) ? value.join('') : String(value);
277
+ if (text.length > 2000) {
278
+ text = text.slice(0, 2000) + '\n...[truncated]';
279
+ }
280
+ return text;
281
+ }
282
+ default:
283
+ return JSON.stringify(value).slice(0, 2000);
284
+ }
285
+ }
286
+ return JSON.stringify(data).slice(0, 2000);
287
+ }
@@ -0,0 +1,101 @@
1
+ import { IChatModel, IUser } from '@jupyter/chat';
2
+ import { IDocumentManager } from '@jupyterlab/docmanager';
3
+ import type { IAgentManager, IAISettingsModel, IProviderRegistry } from '@jupyternaut/agent';
4
+ import { Token } from '@lumino/coreutils';
5
+ import { ISignal } from '@lumino/signaling';
6
+ export declare const DEFAULT_PERSONA: IUser;
7
+ /**
8
+ * Command IDs namespace
9
+ */
10
+ export declare namespace CommandIds {
11
+ const openSettings = "@jupyternaut/persona:open-settings";
12
+ const refreshSkills = "@jupyternaut/persona:refresh-skills";
13
+ }
14
+ /**
15
+ * Public interface for a persona handler attached to a chat session.
16
+ *
17
+ * A persona handler links an `IAgentManager` to an `IChatModel`, listening for
18
+ * persona mentions and generating AI responses. Third-party extensions can
19
+ * consume `IPersonaHandlerRegistry` to obtain `IPersonaHandler` instances and
20
+ * interact with the agent (e.g. to read the active provider or update tools).
21
+ */
22
+ export interface IPersona {
23
+ /**
24
+ * The agent manager used by this handler to generate AI responses.
25
+ */
26
+ readonly agentManager: IAgentManager;
27
+ /**
28
+ * The chat model this handler is attached to.
29
+ */
30
+ readonly model: IChatModel;
31
+ /**
32
+ * Whether the persona is currently generating a response.
33
+ */
34
+ readonly isBusy: boolean;
35
+ /**
36
+ * A signal emitted when the busy state changes.
37
+ */
38
+ readonly busyChanged: ISignal<IPersona, boolean>;
39
+ /**
40
+ * Whether a mention is required to trigger a response.
41
+ * When false, the persona responds to all non-bot messages.
42
+ * Defaults to true.
43
+ */
44
+ requireMention: boolean;
45
+ /**
46
+ * Dispose of the handler and release its resources.
47
+ */
48
+ dispose(): void;
49
+ /**
50
+ * Rebuilds the agent history from the current chat messages.
51
+ * Called after restoring a saved chat.
52
+ */
53
+ rebuildHistory(): Promise<void>;
54
+ }
55
+ /**
56
+ * Registry mapping chat models to their persona.
57
+ *
58
+ * Provided by `@jupyternaut/persona`. Other extensions (e.g. `@jupyterlite/ai`)
59
+ * can consume this token to access the agent manager associated with a given chat.
60
+ */
61
+ export interface IPersonaRegistry {
62
+ /**
63
+ * Returns the persona registered for a given chat model, if any.
64
+ */
65
+ get(model: IChatModel): IPersona | undefined;
66
+ /**
67
+ * Registers a persona for a given chat model.
68
+ */
69
+ register(model: IChatModel, agentManager: IAgentManager): void;
70
+ /**
71
+ * Removes the persona registered for a given chat model.
72
+ */
73
+ unregister(model: IChatModel): void;
74
+ /**
75
+ * A signal emitting whenever a new persona is registered.
76
+ */
77
+ readonly personaAdded: ISignal<IPersonaRegistry, IPersona>;
78
+ }
79
+ /**
80
+ * The options to build a persona registry.
81
+ */
82
+ export interface IPersonaRegistryOptions {
83
+ /**
84
+ * The persona used by the registry.
85
+ */
86
+ persona: IUser;
87
+ /**
88
+ * The agent settings model, used to process attachments in persona.
89
+ */
90
+ settingsModel: IAISettingsModel;
91
+ /**
92
+ * The optional provider registry, used to process attachments in persona.
93
+ */
94
+ providerRegistry?: IProviderRegistry;
95
+ /**
96
+ * The optional document manager, used to process attachments in persona.
97
+ */
98
+ documentManager?: IDocumentManager;
99
+ }
100
+ export declare const IPersonaRegistry: Token<IPersonaRegistry>;
101
+ //# sourceMappingURL=tokens.d.ts.map
package/lib/tokens.js ADDED
@@ -0,0 +1,20 @@
1
+ import { AI_AVATAR } from '@jupyternaut/agent';
2
+ import { Token } from '@lumino/coreutils';
3
+ export const DEFAULT_PERSONA = {
4
+ username: 'jupyternaut-frontend',
5
+ display_name: 'Jupyternaut',
6
+ initials: 'JF',
7
+ color: '#2196F3',
8
+ avatar_url: AI_AVATAR,
9
+ bot: true,
10
+ mention_name: 'jupyternaut-frontend'
11
+ };
12
+ /**
13
+ * Command IDs namespace
14
+ */
15
+ export var CommandIds;
16
+ (function (CommandIds) {
17
+ CommandIds.openSettings = '@jupyternaut/persona:open-settings';
18
+ CommandIds.refreshSkills = '@jupyternaut/persona:refresh-skills';
19
+ })(CommandIds || (CommandIds = {}));
20
+ export const IPersonaRegistry = new Token('@jupyternaut/persona:IPersonaHandlerRegistry');
@@ -0,0 +1,54 @@
1
+ import type { IAgentManagerFactory, IAISecretsAccess, IAISettingsModel, IProviderRegistry } from '@jupyternaut/agent';
2
+ import { IThemeManager } from '@jupyterlab/apputils';
3
+ import { ReactWidget } from '@jupyterlab/ui-components';
4
+ import type { TranslationBundle } from '@jupyterlab/translation';
5
+ import React from 'react';
6
+ /**
7
+ * A JupyterLab widget for AI settings configuration
8
+ */
9
+ export declare class AISettingsWidget extends ReactWidget {
10
+ /**
11
+ * Construct a new AI settings widget
12
+ * @param options - The options for initializing the widget
13
+ */
14
+ constructor(options: AISettingsWidget.IOptions);
15
+ /**
16
+ * Render the AI settings component
17
+ * @returns A React element containing the AI settings interface
18
+ */
19
+ protected render(): React.ReactElement;
20
+ private _settingsModel;
21
+ private _agentManagerFactory?;
22
+ private _themeManager?;
23
+ private _providerRegistry;
24
+ private _secretsAccess?;
25
+ private _trans;
26
+ private _mcpServerRenderer?;
27
+ }
28
+ /**
29
+ * Namespace for AISettingsWidget types and interfaces
30
+ */
31
+ export declare namespace AISettingsWidget {
32
+ /**
33
+ * Options interface for constructing an AISettingsWidget
34
+ */
35
+ interface IOptions {
36
+ settingsModel: IAISettingsModel;
37
+ agentManagerFactory?: IAgentManagerFactory;
38
+ themeManager?: IThemeManager;
39
+ providerRegistry: IProviderRegistry;
40
+ /**
41
+ * Access to provider secrets in the shared namespace.
42
+ */
43
+ secretsAccess: IAISecretsAccess;
44
+ /**
45
+ * The application language translation bundle.
46
+ */
47
+ trans: TranslationBundle;
48
+ /**
49
+ * The renderer for the MCP settings.
50
+ */
51
+ mcpServerRenderer?: React.ComponentType<any>;
52
+ }
53
+ }
54
+ //# sourceMappingURL=ai-settings.d.ts.map