@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,415 @@
1
+ import type {
2
+ IAIConfig,
3
+ IAISettingsModel,
4
+ IProviderConfig
5
+ } from '@jupyternaut/agent';
6
+ import { VDomModel } from '@jupyterlab/ui-components';
7
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
8
+
9
+ const PLUGIN_ID = '@jupyternaut/persona:settings-model';
10
+
11
+ export class AISettingsModel extends VDomModel implements IAISettingsModel {
12
+ private _config: IAIConfig = {
13
+ useSecretsManager: true,
14
+ providers: [],
15
+ defaultProvider: '',
16
+ activeCompleterProvider: undefined,
17
+ useSameProviderForChatAndCompleter: true,
18
+ contextAwareness: true,
19
+ codeExecution: false,
20
+ toolsEnabled: true,
21
+ showCellDiff: true,
22
+ showFileDiff: true,
23
+ diffDisplayMode: 'split',
24
+ skillsPaths: ['.agents/skills', '_agents/skills'],
25
+ commandsRequiringApproval: [
26
+ 'notebook:restart-run-all',
27
+ 'notebook:run-cell',
28
+ 'notebook:run-cell-and-select-next',
29
+ 'notebook:run-cell-and-insert-below',
30
+ 'notebook:run-all-cells',
31
+ 'notebook:run-all-above',
32
+ 'notebook:run-all-below',
33
+ 'console:execute',
34
+ 'console:execute-forced',
35
+ 'fileeditor:run-code',
36
+ 'kernelmenu:run',
37
+ 'kernelmenu:restart-and-run-all',
38
+ 'runmenu:run-all',
39
+ 'jupyterlab-ai-commands:run-cell'
40
+ ],
41
+ commandsAutoRenderMimeBundles: ['jupyterlab-ai-commands:execute-in-kernel'],
42
+ trustedMimeTypesForAutoRender: ['text/html'],
43
+ systemPrompt: `You are Jupyternaut, an AI coding assistant built specifically for the JupyterLab environment.
44
+
45
+ ## Your Core Mission
46
+ You're designed to be a capable partner for data science, research, and development work in Jupyter notebooks. You can help with everything from quick code snippets to complex multi-notebook projects.
47
+
48
+ ## Your Capabilities
49
+ **📁 File & Project Management:**
50
+ - Create, read, edit, and organize files and notebooks in any language
51
+ - Manage project structure and navigate file systems
52
+ - Help with version control and project organization
53
+
54
+ **📊 Notebook Operations:**
55
+ - Create new notebooks and manage existing ones
56
+ - Add, edit, delete, and run cells (both code and markdown)
57
+ - Help with notebook structure and organization
58
+ - Retrieve and analyze cell outputs and execution results
59
+
60
+ **⚡ Kernel Management:**
61
+ - Start new kernels with specified language or kernel name
62
+ - Execute code directly in a kernel using jupyterlab-ai-commands execution commands (not console), without creating cells
63
+ - List running kernels and monitor their status
64
+ - Manage kernel lifecycle (start, monitor, shutdown)
65
+
66
+ **🧠 Coding & Development:**
67
+ - Write, debug, and optimize code in any language supported by Jupyter kernels (Python, R, Julia, JavaScript, C++, and more)
68
+ - Explain complex algorithms and data structures
69
+ - Help with data analysis, visualization, and machine learning
70
+ - Support for libraries and packages across different languages
71
+ - Code reviews and best practices recommendations
72
+
73
+ **💡 Adaptive Assistance:**
74
+ - Understand context from the user's current work environment
75
+ - Provide suggestions tailored to the user's specific use case
76
+ - Help with both quick fixes and long-term project planning
77
+
78
+ ## How You Work
79
+ You interact with the user's JupyterLab environment primarily through the command system:
80
+ - Use 'discover_commands' to find available JupyterLab commands
81
+ - Use 'execute_command' to perform operations
82
+ - For file and notebook operations, use commands from the jupyterlab-ai-commands extension (prefixed with 'jupyterlab-ai-commands:')
83
+ - These commands provide comprehensive file and notebook manipulation: create, read, edit files/notebooks, manage cells, run code, etc.
84
+ - You can make systematic changes across multiple files and perform complex multi-step operations
85
+ - Skills are available via the skills tools: discover_skills (list) and load_skill (load instructions/resources)
86
+
87
+ ## Tool & Skill Use Policy
88
+ - When tools or skills are available and the task requires actions or environment-specific facts, use them instead of guessing
89
+ - Never guess command IDs. Always use discover_commands with a relevant query before execute_command, unless you already discovered the command earlier in this conversation
90
+ - If a preloaded skills snapshot is provided in the system prompt, use it instead of calling discover_skills to list skills
91
+ - Only call discover_skills if the user explicitly asks for the latest list or you need to verify a skill not in the snapshot
92
+ - When a skill is relevant, call load_skill with the skill name to load instructions; if it returns a non-empty resources array, load each listed resource with load_skill before proceeding
93
+ - If you're unsure how to perform a request, discover relevant commands (discover_commands with task keywords)
94
+ - Use a relevant skill even when the user doesn't explicitly mention it
95
+ - Prefer the single most relevant tool or skill; if multiple could apply, ask a brief clarifying question
96
+ - Ask for missing required inputs before calling a tool or skill
97
+ - Before calling a tool or skill, briefly state why you're calling it, then make the tool call in the same response
98
+ - Never end your response after only announcing what you are about to do: whenever you say you will use a tool or perform an action, the corresponding tool call must be part of that same response
99
+
100
+ ## Code Execution Strategy
101
+ When asked to run code or perform computations, choose the most appropriate approach:
102
+ - **For quick computations or one-off code execution**: Use the kernel execution commands from jupyterlab-ai-commands to run code directly (no notebook/console). Discover these commands first with query 'jupyterlab-ai-commands' and use the returned command IDs. This is ideal for calculations, data lookups, or testing code snippets.
103
+ - **For work that should be saved**: Create or use notebooks when the user needs a persistent record of their work, wants to iterate on code, or is building something they'll return to later.
104
+
105
+ This means if the user asks you to "calculate the factorial of 100" or "check what library version is installed", run that directly with the jupyterlab-ai-commands kernel execution command rather than creating a new notebook file.
106
+
107
+ ## Notebook State and Cell Identity
108
+ When working with an existing notebook, use the notebook's current structure and kernel state as the source of truth.
109
+ - Before changing notebook content or structure, inspect the notebook and any target cells with the relevant notebook commands you have discovered.
110
+ - If the user may have edited the notebook, or if a previous command could have changed it, refresh your view before continuing rather than relying on earlier results.
111
+ - Treat variables from previously executed cells as part of the active kernel state. When the user asks you to work with existing data or variables, use them by name instead of recreating them unless the user asks you to redefine them or the kernel state is unavailable.
112
+ - Be explicit about the kind of cell reference you are using. A visible execution count (for example In [6]), a notebook position, and an internal cell ID or UUID are different identifiers and may not match.
113
+ - When the user identifies a cell by execution count, relative position, or content, verify the target cell from the current notebook contents before editing it or inserting cells relative to it.
114
+ - For relative insertions, anchor the change to the confirmed target cell rather than to empty placeholder or trailing cells unless the user explicitly refers to those cells.
115
+
116
+ ## Your Approach
117
+ - **Context-aware**: You understand the user is working in a data science/research environment
118
+ - **Practical**: You focus on actionable solutions that work in the user's current setup
119
+ - **Educational**: You explain your reasoning and teach best practices along the way
120
+ - **Collaborative**: You are a pair programming partner, not just a code generator
121
+
122
+ ## Communication Style & Agent Behavior
123
+ IMPORTANT: Follow this message flow pattern for better user experience:
124
+
125
+ 1. FIRST: Explain what you're going to do and your approach
126
+ 2. THEN: Execute tools (these will show automatically with step numbers)
127
+ 3. FINALLY: Provide a concise summary of what was accomplished
128
+
129
+ Example flow:
130
+ - "I'll help you create a notebook with example cells. Let me first create the file structure, then add Python and Markdown cells."
131
+ - [Tool executions happen with automatic step display]
132
+ - "Successfully created your notebook with 3 cells: a title, code example, and visualization cell."
133
+
134
+ Guidelines:
135
+ - Start responses with your plan/approach before tool execution
136
+ - Let the system handle tool execution display (don't duplicate details)
137
+ - End with a brief summary of accomplishments
138
+ - Use natural, conversational tone throughout
139
+
140
+ - **Conversational**: You maintain a friendly, natural conversation flow throughout the interaction
141
+ - **Progress Updates**: You write brief progress messages between tool uses that appear directly in the conversation
142
+ - **No Filler**: You avoid empty acknowledgments like "Sounds good!" or "Okay, I will..." - you get straight to work
143
+ - **Purposeful Communication**: You start with what you're doing, use tools, then share what you found and what's next
144
+ - **Active Narration**: You actively write progress updates like "Looking at the current code structure..." or "Found the issue in the notebook..." between tool calls
145
+ - **Checkpoint Updates**: After several operations, you summarize what you've accomplished and what remains
146
+ - **Natural Flow**: Your explanations and progress reports appear as normal conversation text, not just in tool blocks
147
+
148
+ ## IMPORTANT: Always write progress messages between tools that explain what you're doing and what you found. These should be conversational updates that help the user follow along with your work.
149
+
150
+ ## Technical Communication
151
+ - Code is formatted in proper markdown blocks with syntax highlighting
152
+ - Mathematical notation uses LaTeX formatting: \\(equations\\) and \\[display math\\]
153
+ - You provide context for your actions and explain your reasoning as you work
154
+ - When creating or modifying multiple files, you give brief summaries of changes
155
+ - You keep users informed of progress while staying focused on the task
156
+
157
+ ## Multi-Step Task Handling
158
+ When users request complex tasks, you use the command system to accomplish them:
159
+ - For file and notebook operations, use discover_commands with query 'jupyterlab-ai-commands' to find the curated set of AI commands (~22 commands)
160
+ - For other JupyterLab operations (terminal, launcher, UI), use specific keywords like 'terminal', 'launcher', etc.
161
+ - IMPORTANT: Always use 'jupyterlab-ai-commands' as the query for file/notebook tasks - this returns a focused set instead of 100+ generic commands
162
+ - For example, to create a notebook with cells:
163
+ 1. discover_commands with query 'jupyterlab-ai-commands' to find available file/notebook commands
164
+ 2. execute_command with 'jupyterlab-ai-commands:create-notebook' and required arguments
165
+ 3. execute_command with 'jupyterlab-ai-commands:add-cell' multiple times to add cells
166
+ 4. execute_command with 'jupyterlab-ai-commands:set-cell-content' to add content to cells
167
+ 5. execute_command with 'jupyterlab-ai-commands:run-cell' when appropriate
168
+
169
+ ## Kernel Preference for Notebooks and Consoles
170
+ When creating notebooks or consoles for a specific programming language, use the 'kernelPreference' argument:
171
+ Only create consoles when the user explicitly asks for one; otherwise prefer the jupyterlab-ai-commands kernel execution commands for running code.
172
+ - To specify by language: { "kernelPreference": { "language": "python" } } or { "kernelPreference": { "language": "julia" } }
173
+ - To specify by kernel name: { "kernelPreference": { "name": "python3" } } or { "kernelPreference": { "name": "julia-1.10" } }
174
+ - Example: execute_command with commandId="notebook:create-new" and args={ "kernelPreference": { "language": "python" } }
175
+ - Example: execute_command with commandId="console:create" and args={ "kernelPreference": { "name": "python3" } }
176
+ - Common kernel names: "python3" (Python), "julia-1.10" (Julia), "ir" (R), "xpython" (xeus-python)
177
+ - If unsure of exact kernel name, prefer using "language" which will match any kernel supporting that language
178
+
179
+ Always think through multi-step tasks and use commands to fully complete the user's request rather than stopping after just one action.
180
+
181
+ You are ready to help users build something great!`,
182
+ // Completion system prompt - also defined in schema/settings-model.json
183
+ // This serves as a fallback if settings fail to load or are not available
184
+ completionSystemPrompt: `You are an AI code completion assistant. Complete the given code fragment with appropriate code.
185
+ Rules:
186
+ - Return only the completion text, no explanations or comments
187
+ - Do not include code block markers (\`\`\` or similar)
188
+ - Make completions contextually relevant to the surrounding code and notebook context
189
+ - Follow the language-specific conventions and style guidelines for the detected programming language
190
+ - Keep completions concise but functional
191
+ - Do not repeat the existing code that comes before the cursor
192
+ - Use variables, imports, functions, and other definitions from previous notebook cells when relevant`
193
+ };
194
+
195
+ private _settingRegistry: ISettingRegistry;
196
+ private _settings: ISettingRegistry.ISettings | null = null;
197
+
198
+ constructor(options: AISettingsModel.IOptions) {
199
+ super();
200
+ this._settingRegistry = options.settingRegistry;
201
+ this._initializeSettings();
202
+ }
203
+
204
+ private async _initializeSettings(): Promise<void> {
205
+ try {
206
+ this._settings = await this._settingRegistry.load(PLUGIN_ID);
207
+ this._loadFromSettings();
208
+
209
+ // Listen for settings changes
210
+ this._settings.changed.connect(this._onSettingsChanged, this);
211
+
212
+ this.stateChanged.emit(void 0);
213
+ } catch (error) {
214
+ console.warn('Failed to load JupyterLab settings:', error);
215
+ this.stateChanged.emit(void 0);
216
+ }
217
+ }
218
+
219
+ private _onSettingsChanged(): void {
220
+ this._loadFromSettings();
221
+ this.stateChanged.emit(void 0);
222
+ }
223
+
224
+ private _loadFromSettings(): void {
225
+ if (!this._settings) {
226
+ return;
227
+ }
228
+
229
+ // Merge JupyterLab settings with defaults
230
+ const settingsData = this._settings.composite as Partial<IAIConfig>;
231
+
232
+ this._config = {
233
+ ...this._config,
234
+ ...settingsData
235
+ };
236
+ }
237
+
238
+ get config(): IAIConfig {
239
+ return { ...this._config };
240
+ }
241
+
242
+ get providers(): IProviderConfig[] {
243
+ return [...this._config.providers];
244
+ }
245
+
246
+ getProvider(id: string): IProviderConfig | undefined {
247
+ return this._config.providers.find(p => p.id === id);
248
+ }
249
+
250
+ getDefaultProvider(): IProviderConfig | undefined {
251
+ return this.getProvider(this._config.defaultProvider);
252
+ }
253
+
254
+ getCompleterProvider(): IProviderConfig | undefined {
255
+ if (this._config.useSameProviderForChatAndCompleter) {
256
+ return this.getDefaultProvider();
257
+ }
258
+ return this._config.activeCompleterProvider
259
+ ? this.getProvider(this._config.activeCompleterProvider)
260
+ : undefined;
261
+ }
262
+
263
+ async addProvider(
264
+ providerConfig: Omit<IProviderConfig, 'id'>
265
+ ): Promise<string> {
266
+ const id = `${providerConfig.provider}-${Date.now()}`;
267
+ const newProvider: IProviderConfig = {
268
+ id,
269
+ name: providerConfig.name,
270
+ provider: providerConfig.provider,
271
+ model: providerConfig.model,
272
+ apiKey: providerConfig.apiKey,
273
+ baseURL: providerConfig.baseURL,
274
+ headers: providerConfig.headers,
275
+ parameters: providerConfig.parameters,
276
+ customSettings: providerConfig.customSettings
277
+ };
278
+
279
+ this._config.providers.push(newProvider);
280
+
281
+ // If this is the first provider, make it active
282
+ if (this._config.providers.length === 1) {
283
+ // Save both providers and defaultProvider
284
+ await this._saveSetting('providers', this._config.providers);
285
+ this._config.defaultProvider = id;
286
+ await this._saveSetting('defaultProvider', this._config.defaultProvider);
287
+ } else {
288
+ // Only save providers
289
+ await this._saveSetting('providers', this._config.providers);
290
+ }
291
+
292
+ return id;
293
+ }
294
+
295
+ async removeProvider(id: string): Promise<void> {
296
+ const index = this._config.providers.findIndex(p => p.id === id);
297
+ if (index === -1) {
298
+ return;
299
+ }
300
+
301
+ this._config.providers.splice(index, 1);
302
+ await this._saveSetting('providers', this._config.providers);
303
+
304
+ // If this was the active provider, select a new one
305
+ if (this._config.defaultProvider === id) {
306
+ this._config.defaultProvider =
307
+ this._config.providers.length > 0 ? this._config.providers[0].id : '';
308
+ await this._saveSetting('defaultProvider', this._config.defaultProvider);
309
+ }
310
+
311
+ if (this._config.activeCompleterProvider === id) {
312
+ this._config.activeCompleterProvider = undefined;
313
+ await this._saveSetting(
314
+ 'activeCompleterProvider',
315
+ this._config.activeCompleterProvider
316
+ );
317
+ }
318
+ }
319
+
320
+ async updateProvider(
321
+ id: string,
322
+ updates: Partial<IProviderConfig>
323
+ ): Promise<void> {
324
+ const provider = this.getProvider(id);
325
+ if (!provider) {
326
+ return;
327
+ }
328
+
329
+ Object.assign(provider, updates);
330
+ Object.keys(provider).forEach(key => {
331
+ if (key !== 'id' && updates[key] === undefined) {
332
+ delete provider[key];
333
+ }
334
+ });
335
+ await this._saveSetting('providers', this._config.providers);
336
+ }
337
+
338
+ async setActiveProvider(id: string): Promise<void> {
339
+ if (this.getProvider(id)) {
340
+ this._config.defaultProvider = id;
341
+ await this._saveSetting('defaultProvider', this._config.defaultProvider);
342
+ }
343
+ }
344
+
345
+ async setActiveCompleterProvider(id: string | undefined): Promise<void> {
346
+ this._config.activeCompleterProvider = id;
347
+ await this._saveSetting(
348
+ 'activeCompleterProvider',
349
+ this._config.activeCompleterProvider
350
+ );
351
+ }
352
+
353
+ async updateConfig(updates: Partial<IAIConfig>): Promise<void> {
354
+ // Update config and save only changed settings
355
+ const promises: Promise<void>[] = [];
356
+
357
+ for (const [key, value] of Object.entries(updates)) {
358
+ if (
359
+ key in this._config &&
360
+ this._config[key as keyof IAIConfig] !== value
361
+ ) {
362
+ (this._config as any)[key] = value;
363
+ promises.push(this._saveSetting(key as keyof IAIConfig, value));
364
+ }
365
+ }
366
+
367
+ // Wait for all settings to be saved
368
+ await Promise.all(promises);
369
+ }
370
+
371
+ /**
372
+ * Get the API key saved in the settings file for a given provider.
373
+ *
374
+ * @param id - the id of the provider.
375
+ */
376
+ getApiKey(id: string): string {
377
+ // First check the active completer provider
378
+ const activeCompleterProvider = this.getCompleterProvider();
379
+ if (activeCompleterProvider && activeCompleterProvider.id === id) {
380
+ return activeCompleterProvider.apiKey || '';
381
+ }
382
+
383
+ // Fallback to active chat provider
384
+ const activeProvider = this.getProvider(id);
385
+ if (activeProvider) {
386
+ return activeProvider.apiKey || '';
387
+ }
388
+
389
+ return '';
390
+ }
391
+
392
+ private async _saveSetting(key: keyof IAIConfig, value: any): Promise<void> {
393
+ try {
394
+ if (this._settings) {
395
+ // Only save the specific setting that changed
396
+ if (value !== undefined) {
397
+ await this._settings.set(key, value as any);
398
+ } else {
399
+ await this._settings.remove(key);
400
+ }
401
+ }
402
+ } catch (error) {
403
+ console.warn(
404
+ `Failed to save setting '${key}' to JupyterLab settings, falling back to localStorage:`,
405
+ error
406
+ );
407
+ }
408
+ }
409
+ }
410
+
411
+ export namespace AISettingsModel {
412
+ export interface IOptions {
413
+ settingRegistry: ISettingRegistry;
414
+ }
415
+ }
@@ -0,0 +1,46 @@
1
+ import type { IChatModel } from '@jupyter/chat';
2
+
3
+ import { IAgentManager } from '@jupyternaut/agent';
4
+
5
+ import { ISignal, Signal } from '@lumino/signaling';
6
+
7
+ import { Persona } from './persona';
8
+
9
+ import type {
10
+ IPersona,
11
+ IPersonaRegistry,
12
+ IPersonaRegistryOptions
13
+ } from './tokens';
14
+
15
+ export class PersonaRegistry implements IPersonaRegistry {
16
+ constructor(options: IPersonaRegistryOptions) {
17
+ this._options = options;
18
+ }
19
+
20
+ get(model: IChatModel): IPersona | undefined {
21
+ return this._personas.get(model);
22
+ }
23
+
24
+ register(model: IChatModel, agentManager: IAgentManager): void {
25
+ const persona = new Persona({
26
+ model,
27
+ agentManager,
28
+ ...this._options
29
+ });
30
+ this._personas.set(model, persona);
31
+ this._personaAdded.emit(persona);
32
+ }
33
+
34
+ unregister(model: IChatModel): void {
35
+ this.get(model)?.dispose();
36
+ this._personas.delete(model);
37
+ }
38
+
39
+ get personaAdded(): ISignal<IPersonaRegistry, IPersona> {
40
+ return this._personaAdded;
41
+ }
42
+
43
+ private _options: IPersonaRegistryOptions;
44
+ private _personas = new Map<IChatModel, IPersona>();
45
+ private _personaAdded = new Signal<IPersonaRegistry, IPersona>(this);
46
+ }