@jupyterlite/ai 0.9.0-a3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -89
- package/lib/agent.d.ts +10 -4
- package/lib/agent.js +30 -17
- package/lib/chat-model.d.ts +6 -0
- package/lib/chat-model.js +144 -17
- package/lib/completion/completion-provider.js +1 -13
- package/lib/components/completion-status.d.ts +20 -0
- package/lib/components/completion-status.js +51 -0
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/components/model-select.js +1 -2
- package/lib/diff-manager.d.ts +25 -0
- package/lib/diff-manager.js +60 -0
- package/lib/icons.d.ts +0 -1
- package/lib/icons.js +2 -6
- package/lib/index.d.ts +2 -2
- package/lib/index.js +54 -23
- package/lib/models/settings-model.d.ts +4 -0
- package/lib/models/settings-model.js +24 -2
- package/lib/providers/built-in-providers.d.ts +0 -4
- package/lib/providers/built-in-providers.js +17 -23
- package/lib/tokens.d.ts +74 -0
- package/lib/tokens.js +4 -0
- package/lib/tools/commands.js +36 -35
- package/lib/tools/file.d.ts +10 -1
- package/lib/tools/file.js +235 -146
- package/lib/tools/notebook.d.ts +2 -3
- package/lib/tools/notebook.js +11 -11
- package/lib/widgets/ai-settings.js +78 -13
- package/lib/widgets/provider-config-dialog.js +15 -8
- package/package.json +5 -3
- package/schema/settings-model.json +25 -0
- package/src/agent.ts +35 -20
- package/src/chat-model.ts +182 -19
- package/src/completion/completion-provider.ts +1 -14
- package/src/components/completion-status.tsx +79 -0
- package/src/components/index.ts +1 -0
- package/src/components/model-select.tsx +0 -3
- package/src/diff-manager.ts +81 -0
- package/src/icons.ts +2 -7
- package/src/index.ts +74 -24
- package/src/models/settings-model.ts +28 -2
- package/src/providers/built-in-providers.ts +17 -24
- package/src/tokens.ts +78 -0
- package/src/tools/commands.ts +45 -40
- package/src/tools/file.ts +295 -164
- package/src/tools/notebook.ts +13 -14
- package/src/widgets/ai-settings.tsx +184 -35
- package/src/widgets/provider-config-dialog.tsx +43 -16
- package/style/base.css +14 -0
package/README.md
CHANGED
|
@@ -52,13 +52,25 @@ The process is different for each provider, so you may refer to their documentat
|
|
|
52
52
|
|
|
53
53
|

|
|
54
54
|
|
|
55
|
+
### Using a generic OpenAI-compatible provider
|
|
56
|
+
|
|
57
|
+
The Generic provider allows you to connect to any OpenAI-compatible API endpoint, including local servers like Ollama and LiteLLM.
|
|
58
|
+
|
|
59
|
+
1. In JupyterLab, open the AI settings panel and go to the **Providers** section
|
|
60
|
+
2. Click on "Add a new provider"
|
|
61
|
+
3. Select the **Generic (OpenAI-compatible)** provider
|
|
62
|
+
4. Configure the following settings:
|
|
63
|
+
- **Base URL**: The base URL of your API endpoint (suggestions are provided for common local servers)
|
|
64
|
+
- **Model**: The model name to use
|
|
65
|
+
- **API Key**: Your API key (if required by the provider)
|
|
66
|
+
|
|
55
67
|
### Using Ollama
|
|
56
68
|
|
|
57
69
|
[Ollama](https://ollama.com/) allows you to run open-weight LLMs locally on your machine.
|
|
58
70
|
|
|
59
71
|
#### Setting up Ollama
|
|
60
72
|
|
|
61
|
-
1. Install Ollama following the instructions at https://ollama.com/download
|
|
73
|
+
1. Install Ollama following the instructions at <https://ollama.com/download>
|
|
62
74
|
2. Pull a model, for example:
|
|
63
75
|
|
|
64
76
|
```bash
|
|
@@ -71,21 +83,11 @@ ollama pull llama3.2
|
|
|
71
83
|
|
|
72
84
|
1. In JupyterLab, open the AI settings panel and go to the **Providers** section
|
|
73
85
|
2. Click on "Add a new provider"
|
|
74
|
-
3. Select the **
|
|
86
|
+
3. Select the **Generic (OpenAI-compatible)** provider
|
|
75
87
|
4. Configure the following settings:
|
|
88
|
+
- **Base URL**: Select `http://localhost:11434/v1` from the suggestions (or enter manually)
|
|
76
89
|
- **Model**: The model name you pulled (e.g., `llama3.2`)
|
|
77
|
-
|
|
78
|
-
### Using a generic OpenAI-compatible provider
|
|
79
|
-
|
|
80
|
-
The Generic provider allows you to connect to any OpenAI-compatible API endpoint.
|
|
81
|
-
|
|
82
|
-
1. In JupyterLab, open the AI settings panel and go to the **Providers** section
|
|
83
|
-
2. Click on "Add a new provider"
|
|
84
|
-
3. Select the **Generic** provider
|
|
85
|
-
4. Configure the following settings:
|
|
86
|
-
- **Base URL**: The base URL of your API endpoint
|
|
87
|
-
- **Model**: The model name to use
|
|
88
|
-
- **API Key**: Your API key (if required by the provider)
|
|
90
|
+
- **API Key**: Leave empty (not required for Ollama)
|
|
89
91
|
|
|
90
92
|
### Using LiteLLM Proxy
|
|
91
93
|
|
|
@@ -97,7 +99,7 @@ Using LiteLLM Proxy with jupyterlite-ai provides flexibility to switch between d
|
|
|
97
99
|
|
|
98
100
|
1. Install LiteLLM:
|
|
99
101
|
|
|
100
|
-
Follow the instructions at https://docs.litellm.ai/docs/simple_proxy
|
|
102
|
+
Follow the instructions at <https://docs.litellm.ai/docs/simple_proxy>.
|
|
101
103
|
|
|
102
104
|
2. Create a `litellm_config.yaml` file with your model configuration:
|
|
103
105
|
|
|
@@ -144,7 +146,7 @@ Providers are based on the [Vercel AI SDK](https://sdk.vercel.ai/docs/introducti
|
|
|
144
146
|
|
|
145
147
|
### Registering a Custom Provider
|
|
146
148
|
|
|
147
|
-
|
|
149
|
+
#### Example: Registering a custom OpenAI-compatible provider
|
|
148
150
|
|
|
149
151
|
```typescript
|
|
150
152
|
import {
|
|
@@ -192,7 +194,7 @@ The provider configuration object requires the following properties:
|
|
|
192
194
|
- `supportsBaseURL`: Whether the provider supports a custom base URL
|
|
193
195
|
- `factory`: Function that creates and returns a language model (the registry automatically wraps it for chat usage)
|
|
194
196
|
|
|
195
|
-
|
|
197
|
+
#### Example: Using a custom fetch function
|
|
196
198
|
|
|
197
199
|
You can provide a custom `fetch` function to the provider, which is useful for adding custom headers, handling authentication, or routing requests through a proxy:
|
|
198
200
|
|
|
@@ -253,75 +255,4 @@ pip uninstall jupyterlite-ai
|
|
|
253
255
|
|
|
254
256
|
## Contributing
|
|
255
257
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
Note: You will need NodeJS to build the extension package.
|
|
259
|
-
|
|
260
|
-
The `jlpm` command is JupyterLab's pinned version of
|
|
261
|
-
[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
|
|
262
|
-
`yarn` or `npm` in lieu of `jlpm` below.
|
|
263
|
-
|
|
264
|
-
```bash
|
|
265
|
-
# Clone the repo to your local environment
|
|
266
|
-
# Change directory to the jupyterlite_ai directory
|
|
267
|
-
# Install package in development mode
|
|
268
|
-
pip install -e "."
|
|
269
|
-
# Link your development version of the extension with JupyterLab
|
|
270
|
-
jupyter labextension develop . --overwrite
|
|
271
|
-
# Rebuild extension Typescript source after making changes
|
|
272
|
-
jlpm build
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
|
|
276
|
-
|
|
277
|
-
```bash
|
|
278
|
-
# Watch the source directory in one terminal, automatically rebuilding when needed
|
|
279
|
-
jlpm watch
|
|
280
|
-
# Run JupyterLab in another terminal
|
|
281
|
-
jupyter lab
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt).
|
|
285
|
-
|
|
286
|
-
By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
|
|
287
|
-
|
|
288
|
-
```bash
|
|
289
|
-
jupyter lab build --minimize=False
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Running UI tests
|
|
293
|
-
|
|
294
|
-
The UI tests use Playwright and can be configured with environment variables:
|
|
295
|
-
|
|
296
|
-
- `PWVIDEO`: Controls video recording during tests (default: `retain-on-failure`)
|
|
297
|
-
- `on`: Record video for all tests
|
|
298
|
-
- `off`: Do not record video
|
|
299
|
-
- `retain-on-failure`: Only keep videos for failed tests
|
|
300
|
-
- `PWSLOWMO`: Adds a delay (in milliseconds) between Playwright actions for debugging (default: `0`)
|
|
301
|
-
|
|
302
|
-
Example usage:
|
|
303
|
-
|
|
304
|
-
```bash
|
|
305
|
-
# Record all test videos
|
|
306
|
-
PWVIDEO=on jlpm playwright test
|
|
307
|
-
|
|
308
|
-
# Slow down test execution by 500ms per action
|
|
309
|
-
PWSLOWMO=500 jlpm playwright test
|
|
310
|
-
|
|
311
|
-
# Combine both options
|
|
312
|
-
PWVIDEO=on PWSLOWMO=1000 jlpm playwright test
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### Development uninstall
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
pip uninstall jupyterlite-ai
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
In development mode, you will also need to remove the symlink created by `jupyter labextension develop`
|
|
322
|
-
command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
|
|
323
|
-
folder is located. Then you can remove the symlink named `@jupyterlite/ai` within that folder.
|
|
324
|
-
|
|
325
|
-
### Packaging the extension
|
|
326
|
-
|
|
327
|
-
See [RELEASE](RELEASE.md)
|
|
258
|
+
See [CONTRIBUTING](CONTRIBUTING.md)
|
package/lib/agent.d.ts
CHANGED
|
@@ -73,18 +73,18 @@ export interface IAgentEventTypeMap {
|
|
|
73
73
|
tool_call_start: {
|
|
74
74
|
callId: string;
|
|
75
75
|
toolName: string;
|
|
76
|
-
input:
|
|
76
|
+
input: string;
|
|
77
77
|
};
|
|
78
78
|
tool_call_complete: {
|
|
79
79
|
callId: string;
|
|
80
80
|
toolName: string;
|
|
81
|
-
output:
|
|
81
|
+
output: string;
|
|
82
82
|
isError: boolean;
|
|
83
83
|
};
|
|
84
84
|
tool_approval_required: {
|
|
85
85
|
interruptionId: string;
|
|
86
86
|
toolName: string;
|
|
87
|
-
toolInput:
|
|
87
|
+
toolInput: string;
|
|
88
88
|
callId?: string;
|
|
89
89
|
};
|
|
90
90
|
grouped_approval_required: {
|
|
@@ -92,7 +92,7 @@ export interface IAgentEventTypeMap {
|
|
|
92
92
|
approvals: Array<{
|
|
93
93
|
interruptionId: string;
|
|
94
94
|
toolName: string;
|
|
95
|
-
toolInput:
|
|
95
|
+
toolInput: string;
|
|
96
96
|
}>;
|
|
97
97
|
};
|
|
98
98
|
error: {
|
|
@@ -232,6 +232,12 @@ export declare class AgentManager {
|
|
|
232
232
|
* @param result The async iterable result from agent execution
|
|
233
233
|
*/
|
|
234
234
|
private _processRunResult;
|
|
235
|
+
/**
|
|
236
|
+
* Formats tool input for display by pretty-printing JSON strings.
|
|
237
|
+
* @param input The tool input string to format
|
|
238
|
+
* @returns Pretty-printed JSON string
|
|
239
|
+
*/
|
|
240
|
+
private _formatToolInput;
|
|
235
241
|
/**
|
|
236
242
|
* Handles the start of a tool call from the model event.
|
|
237
243
|
* @param modelEvent The model event containing tool call information
|
package/lib/agent.js
CHANGED
|
@@ -504,6 +504,22 @@ export class AgentManager {
|
|
|
504
504
|
}
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Formats tool input for display by pretty-printing JSON strings.
|
|
509
|
+
* @param input The tool input string to format
|
|
510
|
+
* @returns Pretty-printed JSON string
|
|
511
|
+
*/
|
|
512
|
+
_formatToolInput(input) {
|
|
513
|
+
try {
|
|
514
|
+
// Parse and re-stringify with formatting
|
|
515
|
+
const parsed = JSON.parse(input);
|
|
516
|
+
return JSON.stringify(parsed, null, 2);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// If parsing fails, return the string as-is
|
|
520
|
+
return input;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
507
523
|
/**
|
|
508
524
|
* Handles the start of a tool call from the model event.
|
|
509
525
|
* @param modelEvent The model event containing tool call information
|
|
@@ -512,19 +528,12 @@ export class AgentManager {
|
|
|
512
528
|
const toolCallId = modelEvent.toolCallId;
|
|
513
529
|
const toolName = modelEvent.toolName;
|
|
514
530
|
const toolInput = modelEvent.input;
|
|
515
|
-
let parsedToolInput;
|
|
516
|
-
try {
|
|
517
|
-
parsedToolInput = JSON.parse(toolInput);
|
|
518
|
-
}
|
|
519
|
-
catch (error) {
|
|
520
|
-
parsedToolInput = {};
|
|
521
|
-
}
|
|
522
531
|
this._agentEvent.emit({
|
|
523
532
|
type: 'tool_call_start',
|
|
524
533
|
data: {
|
|
525
534
|
callId: toolCallId,
|
|
526
535
|
toolName,
|
|
527
|
-
input:
|
|
536
|
+
input: this._formatToolInput(toolInput)
|
|
528
537
|
}
|
|
529
538
|
});
|
|
530
539
|
}
|
|
@@ -540,7 +549,7 @@ export class AgentManager {
|
|
|
540
549
|
? toolCallOutput.output
|
|
541
550
|
: JSON.stringify(toolCallOutput.output, null, 2);
|
|
542
551
|
const isError = toolCallOutput.rawItem.type === 'function_call_result' &&
|
|
543
|
-
toolCallOutput.rawItem.
|
|
552
|
+
toolCallOutput.rawItem.status === 'incomplete';
|
|
544
553
|
const toolName = toolCallOutput.rawItem.type === 'function_call_result'
|
|
545
554
|
? toolCallOutput.rawItem.name
|
|
546
555
|
: 'Unknown Tool';
|
|
@@ -559,17 +568,19 @@ export class AgentManager {
|
|
|
559
568
|
* @param interruption The tool approval interruption item
|
|
560
569
|
*/
|
|
561
570
|
async _handleSingleToolApproval(interruption) {
|
|
562
|
-
const toolName = interruption.rawItem
|
|
563
|
-
const toolInput = interruption.rawItem
|
|
571
|
+
const toolName = interruption.rawItem.name || 'Unknown Tool';
|
|
572
|
+
const toolInput = interruption.rawItem.arguments || '{}';
|
|
564
573
|
const interruptionId = `int-${Date.now()}-${Math.random()}`;
|
|
565
|
-
const callId = interruption.rawItem
|
|
574
|
+
const callId = interruption.rawItem.type === 'function_call'
|
|
575
|
+
? interruption.rawItem.callId
|
|
576
|
+
: undefined;
|
|
566
577
|
this._pendingApprovals.set(interruptionId, { interruption });
|
|
567
578
|
this._agentEvent.emit({
|
|
568
579
|
type: 'tool_approval_required',
|
|
569
580
|
data: {
|
|
570
581
|
interruptionId,
|
|
571
582
|
toolName,
|
|
572
|
-
toolInput,
|
|
583
|
+
toolInput: this._formatToolInput(toolInput),
|
|
573
584
|
callId
|
|
574
585
|
}
|
|
575
586
|
});
|
|
@@ -581,14 +592,14 @@ export class AgentManager {
|
|
|
581
592
|
async _handleGroupedToolApprovals(interruptions) {
|
|
582
593
|
const groupId = `group-${Date.now()}-${Math.random()}`;
|
|
583
594
|
const approvals = interruptions.map(interruption => {
|
|
584
|
-
const toolName = interruption.rawItem
|
|
585
|
-
const toolInput = interruption.rawItem
|
|
595
|
+
const toolName = interruption.rawItem.name || 'Unknown Tool';
|
|
596
|
+
const toolInput = interruption.rawItem.arguments || '{}';
|
|
586
597
|
const interruptionId = `int-${Date.now()}-${Math.random()}`;
|
|
587
598
|
this._pendingApprovals.set(interruptionId, { interruption, groupId });
|
|
588
599
|
return {
|
|
589
600
|
interruptionId,
|
|
590
601
|
toolName,
|
|
591
|
-
toolInput
|
|
602
|
+
toolInput: this._formatToolInput(toolInput)
|
|
592
603
|
};
|
|
593
604
|
});
|
|
594
605
|
this._agentEvent.emit({
|
|
@@ -668,7 +679,9 @@ Guidelines:
|
|
|
668
679
|
- Use natural, conversational tone throughout
|
|
669
680
|
|
|
670
681
|
COMMAND DISCOVERY:
|
|
671
|
-
- When you want to execute JupyterLab commands, ALWAYS use the 'discover_commands' tool first to find available commands and their metadata.
|
|
682
|
+
- When you want to execute JupyterLab commands, ALWAYS use the 'discover_commands' tool first to find available commands and their metadata, with the optional query parameter.
|
|
683
|
+
- The query should typically be a single word, e.g., 'terminal', 'notebook', 'cell', 'file', 'edit', 'view', 'run', etc, to find relevant commands.
|
|
684
|
+
- If searching with a query does not yield the desired command, try again with a different query or use an empty query to list all commands.
|
|
672
685
|
- This ensures you have complete information about command IDs, descriptions, and required arguments before attempting to execute them. Only after discovering the available commands should you use the 'execute_command' tool with the correct command ID and arguments.
|
|
673
686
|
|
|
674
687
|
TOOL SELECTION GUIDELINES:
|
package/lib/chat-model.d.ts
CHANGED
|
@@ -138,6 +138,12 @@ export declare class AIChatModel extends AbstractChatModel {
|
|
|
138
138
|
* @returns Array of formatted attachment contents
|
|
139
139
|
*/
|
|
140
140
|
private _processAttachments;
|
|
141
|
+
/**
|
|
142
|
+
* Reads the content of a notebook cell.
|
|
143
|
+
* @param attachment The notebook attachment to read
|
|
144
|
+
* @returns Cell content as string or null if unable to read
|
|
145
|
+
*/
|
|
146
|
+
private _readNotebookCells;
|
|
141
147
|
/**
|
|
142
148
|
* Reads the content of a file attachment.
|
|
143
149
|
* @param attachment The file attachment to read
|
package/lib/chat-model.js
CHANGED
|
@@ -106,7 +106,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
106
106
|
time: Date.now() / 1000,
|
|
107
107
|
type: 'msg',
|
|
108
108
|
raw_time: false,
|
|
109
|
-
attachments: this.input.attachments
|
|
109
|
+
attachments: [...this.input.attachments]
|
|
110
110
|
};
|
|
111
111
|
this.messageAdded(userMessage);
|
|
112
112
|
// Check if we have valid configuration
|
|
@@ -127,6 +127,8 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
127
127
|
let enhancedMessage = message.body;
|
|
128
128
|
if (this.input.attachments.length > 0) {
|
|
129
129
|
const attachmentContents = await this._processAttachments(this.input.attachments);
|
|
130
|
+
// Clear attachments right after processing
|
|
131
|
+
this.input.clearAttachments();
|
|
130
132
|
if (attachmentContents.length > 0) {
|
|
131
133
|
enhancedMessage +=
|
|
132
134
|
'\n\n--- Attached Files ---\n' + attachmentContents.join('\n\n');
|
|
@@ -134,8 +136,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
134
136
|
}
|
|
135
137
|
this.updateWriters([{ user: this._getAIUser() }]);
|
|
136
138
|
await this._agentManager.generateResponse(enhancedMessage);
|
|
137
|
-
// Clear attachments after processing
|
|
138
|
-
this.input.clearAttachments();
|
|
139
139
|
}
|
|
140
140
|
catch (error) {
|
|
141
141
|
const errorMessage = {
|
|
@@ -219,10 +219,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
219
219
|
*/
|
|
220
220
|
_onSettingsChanged() {
|
|
221
221
|
const config = this._settingsModel.config;
|
|
222
|
-
this.config = {
|
|
223
|
-
...config,
|
|
224
|
-
enableCodeToolbar: true
|
|
225
|
-
};
|
|
222
|
+
this.config = { ...config, enableCodeToolbar: true };
|
|
226
223
|
// Agent manager handles agent recreation automatically via its own settings listener
|
|
227
224
|
}
|
|
228
225
|
/**
|
|
@@ -312,7 +309,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
312
309
|
<div class="jp-ai-tool-body">
|
|
313
310
|
<div class="jp-ai-tool-section">
|
|
314
311
|
<div class="jp-ai-tool-label">Input</div>
|
|
315
|
-
<pre class="jp-ai-tool-code"><code>${
|
|
312
|
+
<pre class="jp-ai-tool-code"><code>${event.data.input}</code></pre>
|
|
316
313
|
</div>
|
|
317
314
|
</div>
|
|
318
315
|
</details>`,
|
|
@@ -394,7 +391,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
394
391
|
<div class="jp-ai-tool-body">
|
|
395
392
|
<div class="jp-ai-tool-section">
|
|
396
393
|
<div class="jp-ai-tool-label">${assistantName} wants to execute this tool. Do you approve?</div>
|
|
397
|
-
<pre class="jp-ai-tool-code"><code>${
|
|
394
|
+
<pre class="jp-ai-tool-code"><code>${event.data.toolInput}</code></pre>
|
|
398
395
|
</div>
|
|
399
396
|
[APPROVAL_BUTTONS:${event.data.interruptionId}]
|
|
400
397
|
</div>
|
|
@@ -414,7 +411,9 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
414
411
|
|
|
415
412
|
${assistantName} wants to execute this tool. Do you approve?
|
|
416
413
|
|
|
417
|
-
|
|
414
|
+
\`\`\`json
|
|
415
|
+
${event.data.toolInput}
|
|
416
|
+
\`\`\`
|
|
418
417
|
|
|
419
418
|
[APPROVAL_BUTTONS:${event.data.interruptionId}]`,
|
|
420
419
|
sender: this._getAIUser(),
|
|
@@ -434,7 +433,7 @@ ${JSON.stringify(event.data.toolInput, null, 2)}
|
|
|
434
433
|
const assistantName = this._getAIUser().display_name;
|
|
435
434
|
const approvalMessageId = UUID.uuid4();
|
|
436
435
|
const toolsList = event.data.approvals
|
|
437
|
-
.map((info, index) => `**${index + 1}. ${info.toolName}**\n${
|
|
436
|
+
.map((info, index) => `**${index + 1}. ${info.toolName}**\n\`\`\`json\n${info.toolInput}\n\`\`\`\n`)
|
|
438
437
|
.join('\n\n');
|
|
439
438
|
const approvalMessage = {
|
|
440
439
|
body: `**🤖 Multiple Tool Approvals Required**
|
|
@@ -477,12 +476,19 @@ ${toolsList}
|
|
|
477
476
|
const contents = [];
|
|
478
477
|
for (const attachment of attachments) {
|
|
479
478
|
try {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
479
|
+
if (attachment.type === 'notebook' && attachment.cells?.length) {
|
|
480
|
+
const cellContents = await this._readNotebookCells(attachment);
|
|
481
|
+
if (cellContents) {
|
|
482
|
+
contents.push(cellContents);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
const fileContent = await this._readFileAttachment(attachment);
|
|
487
|
+
if (fileContent) {
|
|
488
|
+
const fileExtension = PathExt.extname(attachment.value).toLowerCase();
|
|
489
|
+
const language = fileExtension === '.ipynb' ? 'json' : '';
|
|
490
|
+
contents.push(`**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``);
|
|
491
|
+
}
|
|
486
492
|
}
|
|
487
493
|
}
|
|
488
494
|
catch (error) {
|
|
@@ -492,6 +498,127 @@ ${toolsList}
|
|
|
492
498
|
}
|
|
493
499
|
return contents;
|
|
494
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Reads the content of a notebook cell.
|
|
503
|
+
* @param attachment The notebook attachment to read
|
|
504
|
+
* @returns Cell content as string or null if unable to read
|
|
505
|
+
*/
|
|
506
|
+
async _readNotebookCells(attachment) {
|
|
507
|
+
if (attachment.type !== 'notebook' || !attachment.cells) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const model = await this.input.documentManager?.services.contents.get(attachment.value);
|
|
512
|
+
if (!model || model.type !== 'notebook') {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
const kernelLang = model.content?.metadata?.language_info?.name ||
|
|
516
|
+
model.content?.metadata?.kernelspec?.language ||
|
|
517
|
+
'text';
|
|
518
|
+
const selectedCells = attachment.cells
|
|
519
|
+
.map(cellInfo => {
|
|
520
|
+
const cell = model.content.cells.find((c) => c.id === cellInfo.id);
|
|
521
|
+
if (!cell) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
const code = cell.source || '';
|
|
525
|
+
const cellType = cell.cell_type;
|
|
526
|
+
const lang = cellType === 'code' ? kernelLang : cellType;
|
|
527
|
+
const DISPLAY_PRIORITY = [
|
|
528
|
+
'application/vnd.jupyter.widget-view+json',
|
|
529
|
+
'application/javascript',
|
|
530
|
+
'text/html',
|
|
531
|
+
'image/svg+xml',
|
|
532
|
+
'image/png',
|
|
533
|
+
'image/jpeg',
|
|
534
|
+
'text/markdown',
|
|
535
|
+
'text/latex',
|
|
536
|
+
'text/plain'
|
|
537
|
+
];
|
|
538
|
+
function extractDisplay(data) {
|
|
539
|
+
for (const mime of DISPLAY_PRIORITY) {
|
|
540
|
+
if (!(mime in data)) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const value = data[mime];
|
|
544
|
+
if (!value) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
switch (mime) {
|
|
548
|
+
case 'application/vnd.jupyter.widget-view+json':
|
|
549
|
+
return `Widget: ${value.model_id ?? 'unknown model'}`;
|
|
550
|
+
case 'image/png':
|
|
551
|
+
return `}...)`;
|
|
552
|
+
case 'image/jpeg':
|
|
553
|
+
return `}...)`;
|
|
554
|
+
case 'image/svg+xml':
|
|
555
|
+
return String(value).slice(0, 500) + '...\n[svg truncated]';
|
|
556
|
+
case 'text/html':
|
|
557
|
+
return (String(value).slice(0, 1000) +
|
|
558
|
+
(String(value).length > 1000 ? '\n...[truncated]' : ''));
|
|
559
|
+
case 'text/markdown':
|
|
560
|
+
case 'text/latex':
|
|
561
|
+
case 'text/plain': {
|
|
562
|
+
let text = Array.isArray(value)
|
|
563
|
+
? value.join('')
|
|
564
|
+
: String(value);
|
|
565
|
+
if (text.length > 2000) {
|
|
566
|
+
text = text.slice(0, 2000) + '\n...[truncated]';
|
|
567
|
+
}
|
|
568
|
+
return text;
|
|
569
|
+
}
|
|
570
|
+
default:
|
|
571
|
+
return JSON.stringify(value).slice(0, 2000);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return JSON.stringify(data).slice(0, 2000);
|
|
575
|
+
}
|
|
576
|
+
let outputs = '';
|
|
577
|
+
if (cellType === 'code' && Array.isArray(cell.outputs)) {
|
|
578
|
+
outputs = cell.outputs
|
|
579
|
+
.map((output) => {
|
|
580
|
+
if (output.output_type === 'stream') {
|
|
581
|
+
return output.text;
|
|
582
|
+
}
|
|
583
|
+
else if (output.output_type === 'error') {
|
|
584
|
+
const err = output;
|
|
585
|
+
return `${err.ename}: ${err.evalue}\n${(err.traceback || []).join('\n')}`;
|
|
586
|
+
}
|
|
587
|
+
else if (output.output_type === 'execute_result' ||
|
|
588
|
+
output.output_type === 'display_data') {
|
|
589
|
+
const data = output.data;
|
|
590
|
+
if (!data) {
|
|
591
|
+
return '';
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
return extractDisplay(data);
|
|
595
|
+
}
|
|
596
|
+
catch (e) {
|
|
597
|
+
console.error('Cannot extract cell output', e);
|
|
598
|
+
return '';
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return '';
|
|
602
|
+
})
|
|
603
|
+
.filter(Boolean)
|
|
604
|
+
.join('\n---\n');
|
|
605
|
+
if (outputs.length > 2000) {
|
|
606
|
+
outputs = outputs.slice(0, 2000) + '\n...[truncated]';
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return (`**Cell [${cellInfo.id}] (${cellType}):**\n` +
|
|
610
|
+
`\`\`\`${lang}\n${code}\n\`\`\`` +
|
|
611
|
+
(outputs ? `\n**Outputs:**\n\`\`\`text\n${outputs}\n\`\`\`` : ''));
|
|
612
|
+
})
|
|
613
|
+
.filter(Boolean)
|
|
614
|
+
.join('\n\n');
|
|
615
|
+
return `**Notebook: ${attachment.value}**\n${selectedCells}`;
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
console.warn(`Failed to read notebook cells from ${attachment.value}:`, error);
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
495
622
|
/**
|
|
496
623
|
* Reads the content of a file attachment.
|
|
497
624
|
* @param attachment The file attachment to read
|
|
@@ -2,18 +2,6 @@ import { NotebookPanel } from '@jupyterlab/notebook';
|
|
|
2
2
|
import { generateText } from 'ai';
|
|
3
3
|
import { createCompletionModel } from '../providers/models';
|
|
4
4
|
import { SECRETS_NAMESPACE } from '../tokens';
|
|
5
|
-
/**
|
|
6
|
-
* Default system prompt for code completion
|
|
7
|
-
*/
|
|
8
|
-
const DEFAULT_COMPLETION_SYSTEM_PROMPT = `You are an AI code completion assistant. Complete the given code fragment with appropriate code.
|
|
9
|
-
Rules:
|
|
10
|
-
- Return only the completion text, no explanations or comments
|
|
11
|
-
- Do not include code block markers (\`\`\` or similar)
|
|
12
|
-
- Make completions contextually relevant to the surrounding code and notebook context
|
|
13
|
-
- Follow the language-specific conventions and style guidelines for the detected programming language
|
|
14
|
-
- Keep completions concise but functional
|
|
15
|
-
- Do not repeat the existing code that comes before the cursor
|
|
16
|
-
- Use variables, imports, functions, and other definitions from previous notebook cells when relevant`;
|
|
17
5
|
/**
|
|
18
6
|
* Default temperature for code completion (lower than chat for more deterministic results)
|
|
19
7
|
*/
|
|
@@ -50,7 +38,7 @@ export class AICompletionProvider {
|
|
|
50
38
|
* Get the system prompt for the completion.
|
|
51
39
|
*/
|
|
52
40
|
get systemPrompt() {
|
|
53
|
-
return
|
|
41
|
+
return this._settingsModel.config.completionSystemPrompt;
|
|
54
42
|
}
|
|
55
43
|
/**
|
|
56
44
|
* Fetch completion items based on the request and context.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AISettingsModel } from '../models/settings-model';
|
|
2
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
/**
|
|
4
|
+
* The completion status props.
|
|
5
|
+
*/
|
|
6
|
+
interface ICompletionStatusProps {
|
|
7
|
+
/**
|
|
8
|
+
* The settings model.
|
|
9
|
+
*/
|
|
10
|
+
settingsModel: AISettingsModel;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The completion status widget that will be added to the status bar.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CompletionStatusWidget extends ReactWidget {
|
|
16
|
+
constructor(options: ICompletionStatusProps);
|
|
17
|
+
render(): JSX.Element;
|
|
18
|
+
private _props;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
import { jupyternautIcon } from '../icons';
|
|
4
|
+
const COMPLETION_STATUS_CLASS = 'jp-ai-completion-status';
|
|
5
|
+
const COMPLETION_DISABLED_CLASS = 'jp-ai-completion-disabled';
|
|
6
|
+
/**
|
|
7
|
+
* The completion status component.
|
|
8
|
+
*/
|
|
9
|
+
function CompletionStatus(props) {
|
|
10
|
+
const [disabled, setDisabled] = useState(true);
|
|
11
|
+
const [title, setTitle] = useState('');
|
|
12
|
+
/**
|
|
13
|
+
* Handle changes in the settings.
|
|
14
|
+
*/
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const stateChanged = (model) => {
|
|
17
|
+
if (model.config.useSameProviderForChatAndCompleter) {
|
|
18
|
+
setDisabled(false);
|
|
19
|
+
setTitle(`Completion using ${model.getDefaultProvider()?.model}`);
|
|
20
|
+
}
|
|
21
|
+
else if (model.config.activeCompleterProvider) {
|
|
22
|
+
setDisabled(false);
|
|
23
|
+
setTitle(`Completion using ${model.getProvider(model.config.activeCompleterProvider)?.model}`);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
setDisabled(true);
|
|
27
|
+
setTitle('No completion');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
props.settingsModel.stateChanged.connect(stateChanged);
|
|
31
|
+
stateChanged(props.settingsModel);
|
|
32
|
+
return () => {
|
|
33
|
+
props.settingsModel.stateChanged.disconnect(stateChanged);
|
|
34
|
+
};
|
|
35
|
+
}, [props.settingsModel]);
|
|
36
|
+
return (React.createElement(jupyternautIcon.react, { className: disabled ? COMPLETION_DISABLED_CLASS : '', top: '2px', width: '16px', stylesheet: 'statusBar', title: title }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The completion status widget that will be added to the status bar.
|
|
40
|
+
*/
|
|
41
|
+
export class CompletionStatusWidget extends ReactWidget {
|
|
42
|
+
constructor(options) {
|
|
43
|
+
super();
|
|
44
|
+
this.addClass(COMPLETION_STATUS_CLASS);
|
|
45
|
+
this._props = options;
|
|
46
|
+
}
|
|
47
|
+
render() {
|
|
48
|
+
return React.createElement(CompletionStatus, { ...this._props });
|
|
49
|
+
}
|
|
50
|
+
_props;
|
|
51
|
+
}
|