@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,350 @@
1
+ import {
2
+ createCompletionModel,
3
+ IProviderRegistry,
4
+ SECRETS_NAMESPACE
5
+ } from '@jupyternaut/agent';
6
+ import type { IAISettingsModel } from '@jupyternaut/agent';
7
+ import {
8
+ CompletionHandler,
9
+ IInlineCompletionContext,
10
+ IInlineCompletionList,
11
+ IInlineCompletionProvider
12
+ } from '@jupyterlab/completer';
13
+ import { NotebookPanel } from '@jupyterlab/notebook';
14
+ import { generateText, type LanguageModel } from 'ai';
15
+ import { ISecretsManager } from 'jupyter-secrets-manager';
16
+
17
+ /**
18
+ * Configuration interface for provider-specific completion behavior
19
+ */
20
+ export interface IProviderCompletionConfig {
21
+ /**
22
+ * Temperature setting for the provider
23
+ */
24
+ temperature?: number;
25
+
26
+ /**
27
+ * Whether the provider supports fill-in-the-middle completion
28
+ */
29
+ supportsFillInMiddle?: boolean;
30
+
31
+ /**
32
+ * Whether to set filterText for this provider
33
+ */
34
+ useFilterText?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Default temperature for code completion (lower than chat for more deterministic results)
39
+ */
40
+ const DEFAULT_COMPLETION_TEMPERATURE = 0.3;
41
+
42
+ /**
43
+ * The generic completion provider to register to the completion provider manager.
44
+ */
45
+ export class AICompletionProvider implements IInlineCompletionProvider {
46
+ /**
47
+ * Construct a new completion provider.
48
+ */
49
+ constructor(options: AICompletionProvider.IOptions) {
50
+ Private.setToken(options.token);
51
+ this._settingsModel = options.settingsModel;
52
+ this._providerRegistry = options.providerRegistry;
53
+ this._secretsManager = options.secretsManager;
54
+ this._settingsModel.stateChanged.connect(() => {
55
+ this._updateModel();
56
+ });
57
+ this._updateModel();
58
+
59
+ // Disable the secrets manager if the token is empty.
60
+ if (!options.token) {
61
+ this._secretsManager = undefined;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * The unique identifier of the provider.
67
+ */
68
+ readonly identifier = '@jupyternaut/persona:completer';
69
+
70
+ /**
71
+ * Get the current completer name based on settings.
72
+ */
73
+ get name(): string {
74
+ const activeProvider = this._settingsModel.getCompleterProvider();
75
+ return activeProvider ? `${activeProvider.provider}-completer` : 'none';
76
+ }
77
+
78
+ /**
79
+ * Get the system prompt for the completion.
80
+ */
81
+ get systemPrompt(): string {
82
+ return this._settingsModel.config.completionSystemPrompt;
83
+ }
84
+
85
+ /**
86
+ * Fetch completion items based on the request and context.
87
+ */
88
+ async fetch(
89
+ request: CompletionHandler.IRequest,
90
+ context: IInlineCompletionContext
91
+ ): Promise<IInlineCompletionList> {
92
+ if (!this._model) {
93
+ return { items: [] };
94
+ }
95
+
96
+ const { text, offset: cursorOffset } = request;
97
+ const prompt = text.slice(0, cursorOffset);
98
+ const suffix = text.slice(cursorOffset);
99
+
100
+ // Get current provider settings
101
+ const activeProvider = this._settingsModel.getCompleterProvider();
102
+ if (!activeProvider) {
103
+ return { items: [] };
104
+ }
105
+
106
+ const provider = activeProvider.provider;
107
+ const providerConfig = this._getProviderCompletionConfig();
108
+
109
+ try {
110
+ let completionPrompt: string;
111
+
112
+ // Check if we're in a notebook or file and handle context accordingly
113
+ if (context.widget instanceof NotebookPanel) {
114
+ // Extract notebook context with surrounding cells
115
+ const contextString = this._extractNotebookContext(context, request);
116
+ completionPrompt = contextString;
117
+ } else {
118
+ // For files, use simpler approach
119
+ completionPrompt = prompt.trim();
120
+ // Apply fill-in-middle formatting if supported and suffix exists
121
+ if (providerConfig.supportsFillInMiddle && suffix.trim()) {
122
+ completionPrompt = `<PRE>${prompt}<SUF>${suffix}<MID>`;
123
+ }
124
+ }
125
+
126
+ const { text: completion } = await generateText({
127
+ model: this._model,
128
+ prompt: completionPrompt,
129
+ instructions: this.systemPrompt,
130
+ temperature: providerConfig.temperature || 0.3
131
+ });
132
+
133
+ // Clean up FIM tags and code block markers
134
+ const cleanCompletion = completion
135
+ .replace(/<PRE>/g, '')
136
+ .replace(/<SUF>/g, '')
137
+ .replace(/<MID>/g, '')
138
+ .replace(/```[\s\S]*?```/g, '')
139
+ .trim();
140
+
141
+ const items = [
142
+ {
143
+ insertText: cleanCompletion,
144
+ filterText: providerConfig.useFilterText
145
+ ? prompt.substring(completionPrompt.length)
146
+ : undefined
147
+ }
148
+ ];
149
+
150
+ return { items };
151
+ } catch (error) {
152
+ console.error(`Error fetching completions from ${provider}:`, error);
153
+ return { items: [] };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Update the language model based on current settings.
159
+ */
160
+ private async _updateModel(): Promise<void> {
161
+ const activeProvider = this._settingsModel.getCompleterProvider();
162
+ if (!activeProvider) {
163
+ this._model = null;
164
+ return;
165
+ }
166
+
167
+ const provider = activeProvider.provider;
168
+ const model = activeProvider.model;
169
+ const baseURL = activeProvider.baseURL;
170
+
171
+ let apiKey: string;
172
+ if (this._secretsManager && this._settingsModel.config.useSecretsManager) {
173
+ const token = Private.getToken();
174
+ if (!token) {
175
+ // This should never happen, the secrets manager should be disabled.
176
+ console.error(
177
+ '@jupyternaut/persona::AICompletionProvider error: the settings manager token is not set.\nYou should disable the secrets manager from the AI settings.'
178
+ );
179
+ apiKey = '';
180
+ } else {
181
+ apiKey =
182
+ (
183
+ await this._secretsManager.get(
184
+ token,
185
+ SECRETS_NAMESPACE,
186
+ `${provider}:apiKey`
187
+ )
188
+ )?.value ?? '';
189
+ }
190
+ } else {
191
+ apiKey = this._settingsModel.getApiKey(activeProvider.id);
192
+ }
193
+
194
+ try {
195
+ this._model = createCompletionModel(
196
+ {
197
+ provider,
198
+ model,
199
+ apiKey,
200
+ baseURL
201
+ },
202
+ this._providerRegistry
203
+ );
204
+ } catch (error) {
205
+ console.error(`Error creating model for ${provider}:`, error);
206
+ this._model = null;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Extract context from notebook cells
212
+ */
213
+ private _extractNotebookContext(
214
+ context: IInlineCompletionContext,
215
+ request: CompletionHandler.IRequest
216
+ ): string {
217
+ const { text, offset: cursorOffset } = request;
218
+ let codeBeforeCursor = text.slice(0, cursorOffset);
219
+ let codeAfterCursor = text.slice(cursorOffset);
220
+
221
+ const notebookPanel = context.widget as NotebookPanel;
222
+ const notebook = notebookPanel.content;
223
+ const currentCellIndex = notebook.activeCellIndex;
224
+ const cells = notebook.widgets;
225
+
226
+ // For notebooks, include context from surrounding cells
227
+ const cellsAbove: string[] = [];
228
+ const cellsBelow: string[] = [];
229
+
230
+ // Get content from cells above current cell
231
+ for (let i = 0; i < currentCellIndex; i++) {
232
+ const cell = cells[i];
233
+ if (cell.model.type === 'code') {
234
+ const source = cell.model.sharedModel.source;
235
+ if (source.trim()) {
236
+ cellsAbove.push(source.trim());
237
+ }
238
+ }
239
+ }
240
+
241
+ // Get content from cells below current cell
242
+ for (let i = currentCellIndex + 1; i < cells.length; i++) {
243
+ const cell = cells[i];
244
+ if (cell.model.type === 'code') {
245
+ const source = cell.model.sharedModel.source;
246
+ if (source.trim()) {
247
+ cellsBelow.push(source.trim());
248
+ }
249
+ }
250
+ }
251
+
252
+ // Include cells above in the code before cursor
253
+ if (cellsAbove.length > 0) {
254
+ const cellsAboveText = cellsAbove
255
+ .map((cell, index) => `# Cell ${index + 1}:\n${cell}`)
256
+ .join('\n\n');
257
+ codeBeforeCursor = `${cellsAboveText}\n\n# Current cell:\n${codeBeforeCursor}`;
258
+ }
259
+
260
+ // Include cells below in the code after cursor
261
+ if (cellsBelow.length > 0) {
262
+ const cellsBelowText = cellsBelow
263
+ .map((cell, index) => `# Cell ${index + 1}:\n${cell}`)
264
+ .join('\n\n');
265
+ codeAfterCursor = `${codeAfterCursor}\n\n# Cells below:\n${cellsBelowText}`;
266
+ }
267
+
268
+ const parts: string[] = [];
269
+
270
+ // Add code before cursor
271
+ if (codeBeforeCursor) {
272
+ parts.push('# Code before cursor:');
273
+ parts.push(codeBeforeCursor);
274
+ }
275
+
276
+ // Add completion instruction
277
+ parts.push('# Complete the code at cursor position');
278
+
279
+ // Add code after cursor
280
+ if (codeAfterCursor) {
281
+ parts.push('# Code after cursor:');
282
+ parts.push(codeAfterCursor);
283
+ }
284
+
285
+ return parts.length > 1 ? parts.join('\n\n') + '\n\n' : '';
286
+ }
287
+
288
+ /**
289
+ * Get provider-specific completion configuration
290
+ */
291
+ private _getProviderCompletionConfig(): IProviderCompletionConfig {
292
+ // Get provider-specific completion parameters
293
+ const activeProvider = this._settingsModel.getCompleterProvider();
294
+
295
+ // Use provider-specific temperature or fall back to default
296
+ const temperature =
297
+ activeProvider?.parameters?.temperature ?? DEFAULT_COMPLETION_TEMPERATURE;
298
+ const supportsFillInMiddle =
299
+ activeProvider?.parameters?.supportsFillInMiddle ?? false;
300
+ const useFilterText = activeProvider?.parameters?.useFilterText ?? false;
301
+
302
+ return {
303
+ temperature,
304
+ supportsFillInMiddle,
305
+ useFilterText
306
+ };
307
+ }
308
+
309
+ private _settingsModel: IAISettingsModel;
310
+ private _providerRegistry?: IProviderRegistry;
311
+ private _model: LanguageModel | null = null;
312
+ private _secretsManager?: ISecretsManager;
313
+ }
314
+
315
+ export namespace AICompletionProvider {
316
+ /**
317
+ * The options for the constructor of the completion provider.
318
+ */
319
+ export interface IOptions {
320
+ /**
321
+ * The AI settings model.
322
+ */
323
+ settingsModel: IAISettingsModel;
324
+ /**
325
+ * The provider registry
326
+ */
327
+ providerRegistry?: IProviderRegistry;
328
+ /**
329
+ * The secrets manager.
330
+ */
331
+ secretsManager?: ISecretsManager;
332
+ /**
333
+ * The token used to request the secrets manager.
334
+ */
335
+ token: symbol | null;
336
+ }
337
+ }
338
+
339
+ namespace Private {
340
+ /**
341
+ * The token to use with the secrets manager, setter and getter.
342
+ */
343
+ let secretsToken: symbol | null;
344
+ export function setToken(value: symbol | null): void {
345
+ secretsToken = value;
346
+ }
347
+ export function getToken(): symbol | null {
348
+ return secretsToken;
349
+ }
350
+ }
@@ -0,0 +1 @@
1
+ export * from './completion-provider';
@@ -0,0 +1,93 @@
1
+ import { ReactWidget } from '@jupyterlab/ui-components';
2
+ import type { TranslationBundle } from '@jupyterlab/translation';
3
+ import { jupyternautIcon } from '@jupyternaut/agent';
4
+ import type { IAISettingsModel } from '@jupyternaut/agent';
5
+ import React, { useEffect, useState } from 'react';
6
+
7
+ const COMPLETION_STATUS_CLASS = 'jp-ai-completion-status';
8
+ const COMPLETION_DISABLED_CLASS = 'jp-ai-completion-disabled';
9
+
10
+ /**
11
+ * The completion status props.
12
+ */
13
+ interface ICompletionStatusProps {
14
+ /**
15
+ * The settings model.
16
+ */
17
+ settingsModel: IAISettingsModel;
18
+ /**
19
+ * The application language translator.
20
+ */
21
+ translator: TranslationBundle;
22
+ }
23
+
24
+ /**
25
+ * The completion status component.
26
+ */
27
+ function CompletionStatus(props: ICompletionStatusProps): JSX.Element {
28
+ const { translator: trans } = props;
29
+ const [disabled, setDisabled] = useState<boolean>(true);
30
+ const [title, setTitle] = useState<string>('');
31
+
32
+ /**
33
+ * Handle changes in the settings.
34
+ */
35
+ useEffect(() => {
36
+ const stateChanged = (model: IAISettingsModel) => {
37
+ if (model.config.useSameProviderForChatAndCompleter) {
38
+ setDisabled(false);
39
+ setTitle(
40
+ trans.__(
41
+ 'Completion using %1',
42
+ model.getDefaultProvider()?.model ?? ''
43
+ )
44
+ );
45
+ } else if (model.config.activeCompleterProvider) {
46
+ setDisabled(false);
47
+ setTitle(
48
+ trans.__(
49
+ 'Completion using %1',
50
+ model.getProvider(model.config.activeCompleterProvider)?.model ?? ''
51
+ )
52
+ );
53
+ } else {
54
+ setDisabled(true);
55
+ setTitle(trans.__('No completion'));
56
+ }
57
+ };
58
+
59
+ props.settingsModel.stateChanged.connect(stateChanged);
60
+
61
+ stateChanged(props.settingsModel);
62
+ return () => {
63
+ props.settingsModel.stateChanged.disconnect(stateChanged);
64
+ };
65
+ }, [props.settingsModel, trans]);
66
+
67
+ return (
68
+ <jupyternautIcon.react
69
+ className={disabled ? COMPLETION_DISABLED_CLASS : ''}
70
+ top={'2px'}
71
+ width={'16px'}
72
+ stylesheet={'statusBar'}
73
+ title={title}
74
+ />
75
+ );
76
+ }
77
+
78
+ /**
79
+ * The completion status widget that will be added to the status bar.
80
+ */
81
+ export class CompletionStatusWidget extends ReactWidget {
82
+ constructor(options: ICompletionStatusProps) {
83
+ super();
84
+ this.addClass(COMPLETION_STATUS_CLASS);
85
+ this._props = options;
86
+ }
87
+
88
+ render(): JSX.Element {
89
+ return <CompletionStatus {...this._props} />;
90
+ }
91
+
92
+ private _props: ICompletionStatusProps;
93
+ }
@@ -0,0 +1 @@
1
+ export * from './completion-status';
@@ -0,0 +1,81 @@
1
+ import type {
2
+ IAISettingsModel,
3
+ IDiffManager,
4
+ IShowCellDiffParams,
5
+ IShowFileDiffParams
6
+ } from '@jupyternaut/agent';
7
+ import { CommandRegistry } from '@lumino/commands';
8
+
9
+ /**
10
+ * Command IDs for unified cell diffs
11
+ */
12
+ const UNIFIED_DIFF_COMMAND_ID = 'jupyterlab-diff:unified-cell-diff';
13
+
14
+ /**
15
+ * Command IDs for split cell diffs
16
+ */
17
+ const SPLIT_DIFF_COMMAND_ID = 'jupyterlab-diff:split-cell-diff';
18
+
19
+ /**
20
+ * Command ID for unified file diffs
21
+ */
22
+ const UNIFIED_FILE_DIFF_COMMAND_ID = 'jupyterlab-diff:unified-file-diff';
23
+
24
+ /**
25
+ * Implementation of the diff manager
26
+ */
27
+ export class DiffManager implements IDiffManager {
28
+ /**
29
+ * Construct a new DiffManager
30
+ */
31
+ constructor(options: {
32
+ commands: CommandRegistry;
33
+ settingsModel: IAISettingsModel;
34
+ }) {
35
+ this._commands = options.commands;
36
+ this._settingsModel = options.settingsModel;
37
+ }
38
+
39
+ /**
40
+ * Show diff between original and modified cell content
41
+ */
42
+ async showCellDiff(params: IShowCellDiffParams): Promise<void> {
43
+ if (!this._settingsModel.config.showCellDiff) {
44
+ return;
45
+ }
46
+
47
+ const showDiffCommandId =
48
+ this._settingsModel.config.diffDisplayMode === 'unified'
49
+ ? UNIFIED_DIFF_COMMAND_ID
50
+ : SPLIT_DIFF_COMMAND_ID;
51
+
52
+ await this._commands.execute(showDiffCommandId, {
53
+ originalSource: params.original,
54
+ newSource: params.modified,
55
+ cellId: params.cellId,
56
+ showActionButtons: params.showActionButtons ?? true,
57
+ openDiff: params.openDiff ?? true,
58
+ notebookPath: params.notebookPath
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Show diff between original and modified file content
64
+ */
65
+ async showFileDiff(params: IShowFileDiffParams): Promise<void> {
66
+ if (!this._settingsModel.config.showFileDiff) {
67
+ return;
68
+ }
69
+
70
+ // File diffs only support unified view
71
+ await this._commands.execute(UNIFIED_FILE_DIFF_COMMAND_ID, {
72
+ originalSource: params.original,
73
+ newSource: params.modified,
74
+ filePath: params.filePath,
75
+ showActionButtons: params.showActionButtons ?? true
76
+ });
77
+ }
78
+
79
+ private _commands: CommandRegistry;
80
+ private _settingsModel: IAISettingsModel;
81
+ }