@jupyterlite/ai 0.17.0 → 0.18.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.
@@ -45,6 +45,39 @@ export function getProviderModelInfo(providerInfo, model) {
45
45
  return normalizeModelId(candidateId) === normalizedModelId;
46
46
  })?.[1];
47
47
  }
48
+ export function modelSupportsImages(providerConfig, providerRegistry) {
49
+ if (!providerConfig) {
50
+ return true;
51
+ }
52
+ const providerInfo = providerRegistry?.getProviderInfo(providerConfig.provider);
53
+ const modelInfo = getProviderModelInfo(providerInfo, providerConfig.model);
54
+ if (!modelInfo) {
55
+ return true;
56
+ }
57
+ return modelInfo.supportsImages ?? true;
58
+ }
59
+ export function modelSupportsPdf(providerConfig, providerRegistry) {
60
+ if (!providerConfig) {
61
+ return true;
62
+ }
63
+ const providerInfo = providerRegistry?.getProviderInfo(providerConfig.provider);
64
+ const modelInfo = getProviderModelInfo(providerInfo, providerConfig.model);
65
+ if (!modelInfo) {
66
+ return true;
67
+ }
68
+ return modelInfo.supportsPdf ?? true;
69
+ }
70
+ export function modelSupportsAudio(providerConfig, providerRegistry) {
71
+ if (!providerConfig) {
72
+ return true;
73
+ }
74
+ const providerInfo = providerRegistry?.getProviderInfo(providerConfig.provider);
75
+ const modelInfo = getProviderModelInfo(providerInfo, providerConfig.model);
76
+ if (!modelInfo) {
77
+ return true;
78
+ }
79
+ return modelInfo.supportsAudio ?? true;
80
+ }
48
81
  export function getEffectiveContextWindow(providerConfig, providerRegistry) {
49
82
  if (!providerConfig) {
50
83
  return undefined;
package/lib/tokens.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActiveCellManager, IMessage, IMessageContent } from '@jupyter/chat';
1
+ import { ActiveCellManager, IChatModel, IMessage } from '@jupyter/chat';
2
2
  import { VDomRenderer } from '@jupyterlab/apputils';
3
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import { Token } from '@lumino/coreutils';
@@ -7,7 +7,6 @@ import { ISignal } from '@lumino/signaling';
7
7
  import type { Tool, LanguageModel, UserContent, ModelMessage } from 'ai';
8
8
  import { ISecretsManager } from 'jupyter-secrets-manager';
9
9
  import type { IModelOptions } from './providers/models';
10
- import { AIChatModel } from './chat-model';
11
10
  import type { ISkillDefinition, ISkillRegistration, ISkillResourceResult, ISkillSummary } from './skills/types';
12
11
  export type { ISkillDefinition, ISkillRegistration, ISkillResourceResult, ISkillSummary } from './skills/types';
13
12
  /**
@@ -159,6 +158,18 @@ export interface IProviderModelInfo {
159
158
  * Default context window for the model in tokens.
160
159
  */
161
160
  contextWindow?: number;
161
+ /**
162
+ * Whether the model supports image inputs.
163
+ */
164
+ supportsImages?: boolean;
165
+ /**
166
+ * Whether the model supports PDF inputs.
167
+ */
168
+ supportsPdf?: boolean;
169
+ /**
170
+ * Whether the model supports audio inputs.
171
+ */
172
+ supportsAudio?: boolean;
162
173
  }
163
174
  export interface IProviderInfo {
164
175
  /**
@@ -303,6 +314,7 @@ export interface IAIConfig {
303
314
  diffDisplayMode: 'split' | 'unified';
304
315
  skillsPaths: string[];
305
316
  chatBackupDirectory: string;
317
+ autoTitle: boolean;
306
318
  }
307
319
  export interface IAISettingsModel extends VDomRenderer.IModel {
308
320
  readonly config: IAIConfig;
@@ -401,13 +413,12 @@ export declare namespace IAgentManager {
401
413
  isError: boolean;
402
414
  };
403
415
  tool_approval_request: {
404
- approvalId: string;
405
416
  toolCallId: string;
406
417
  toolName: string;
407
418
  args: unknown;
408
419
  };
409
420
  tool_approval_resolved: {
410
- approvalId: string;
421
+ toolCallId: string;
411
422
  approved: boolean;
412
423
  };
413
424
  error: {
@@ -468,26 +479,26 @@ export interface IAgentManager {
468
479
  */
469
480
  clearHistory(): Promise<void>;
470
481
  /**
471
- * Sets the conversation history with a list of messages from the chat.
472
- * @param messages The chat messages to set as history
482
+ * Sets the history from already-processed model messages.
483
+ * @param messages Pre-built model messages (may include binary content)
473
484
  */
474
- setHistory(messages: IMessageContent[]): void;
485
+ setHistory(messages: ModelMessage[]): void;
475
486
  /**
476
487
  * Stops the current streaming response by aborting the request.
477
488
  */
478
489
  stopStreaming(): void;
479
490
  /**
480
491
  * Approves a pending tool call.
481
- * @param approvalId The approval ID to approve
492
+ * @param toolCallId The tool call ID to approve
482
493
  * @param reason Optional reason for approval
483
494
  */
484
- approveToolCall(approvalId: string, reason?: string): void;
495
+ approveToolCall(toolCallId: string, reason?: string): void;
485
496
  /**
486
497
  * Rejects a pending tool call.
487
- * @param approvalId The approval ID to reject
498
+ * @param toolCallId The tool call ID to reject
488
499
  * @param reason Optional reason for rejection
489
500
  */
490
- rejectToolCall(approvalId: string, reason?: string): void;
501
+ rejectToolCall(toolCallId: string, reason?: string): void;
491
502
  /**
492
503
  * Generates AI response to user message using the agent.
493
504
  * Handles the complete execution cycle including tool calls.
@@ -533,6 +544,68 @@ export interface IAgentManagerFactory {
533
544
  getMCPTools(): Promise<ToolMap>;
534
545
  }
535
546
  export declare const IAgentManagerFactory: Token<IAgentManagerFactory>;
547
+ export interface IAIChatModel extends IChatModel {
548
+ /**
549
+ * A signal emitting when the chat name has changed.
550
+ */
551
+ readonly nameChanged: ISignal<IAIChatModel, string>;
552
+ /**
553
+ * The title of the chat.
554
+ */
555
+ title: string | null;
556
+ /**
557
+ * A signal emitting when the chat title has changed.
558
+ */
559
+ readonly titleChanged: ISignal<IAIChatModel, string | null>;
560
+ /**
561
+ * Whether to save the chat automatically.
562
+ */
563
+ autosave: boolean;
564
+ /**
565
+ * A signal emitting when the autosave flag changed.
566
+ */
567
+ readonly autosaveChanged: ISignal<IAIChatModel, boolean>;
568
+ /**
569
+ * Whether save/restore is available.
570
+ */
571
+ readonly saveAvailable: boolean;
572
+ /**
573
+ * A signal emitting when the token usage changed.
574
+ */
575
+ readonly tokenUsageChanged: ISignal<IAgentManager, ITokenUsage>;
576
+ /**
577
+ * The agent manager used in the model.
578
+ */
579
+ readonly agentManager: IAgentManager;
580
+ /**
581
+ * Save the chat as json file.
582
+ */
583
+ save(): Promise<void>;
584
+ /**
585
+ * Restore the chat from a json file.
586
+ *
587
+ * @param silent - Whether a log should be displayed in the console if the
588
+ * restoration is not possible.
589
+ */
590
+ restore(filepath: string, silent?: boolean): Promise<boolean>;
591
+ /**
592
+ * Request a title to this chat, regarding the message history.
593
+ */
594
+ requestTitle(): Promise<string>;
595
+ /**
596
+ * Removes a queued message by its ID.
597
+ * @param messageId The ID of the queued message to remove
598
+ */
599
+ removeQueuedMessage(messageId: string): void;
600
+ /**
601
+ * The current message queue
602
+ */
603
+ messageQueue: any[];
604
+ /**
605
+ * Whether the chat is currently busy processing a message
606
+ */
607
+ isBusy: boolean;
608
+ }
536
609
  /**
537
610
  * The interface for the chat model handler.
538
611
  */
@@ -540,7 +613,7 @@ export interface IChatModelHandler {
540
613
  /**
541
614
  * The function to create a new model.
542
615
  */
543
- createModel(options: ICreateChatOptions): AIChatModel;
616
+ createModel(options: ICreateChatOptions): IAIChatModel;
544
617
  /**
545
618
  * The active cell manager (to copy code from chat to cell).
546
619
  */
@@ -482,6 +482,11 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
482
482
  }), color: "primary" }), label: React.createElement(Box, null,
483
483
  React.createElement(Typography, { variant: "body1" }, trans.__('Send with Shift+Enter')),
484
484
  React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Use Shift+Enter to send messages (Enter creates new line)'))) }),
485
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.autoTitle, onChange: e => handleConfigUpdate({
486
+ autoTitle: e.target.checked
487
+ }), color: "primary" }), label: React.createElement(Box, null,
488
+ React.createElement(Typography, { variant: "body1" }, trans.__('Auto Title')),
489
+ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Automatically generate a chat title from the model for every message until there are 5 messages'))) }),
485
490
  React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.showTokenUsage, onChange: e => handleConfigUpdate({
486
491
  showTokenUsage: e.target.checked
487
492
  }), color: "primary" }), label: React.createElement(Box, null,
@@ -2,8 +2,7 @@ import { ChatWidget } from '@jupyter/chat';
2
2
  import { MainAreaWidget } from '@jupyterlab/apputils';
3
3
  import type { TranslationBundle } from '@jupyterlab/translation';
4
4
  import { CommandRegistry } from '@lumino/commands';
5
- import { AIChatModel } from '../chat-model';
6
- import { type IAISettingsModel } from '../tokens';
5
+ import { IAIChatModel, type IAISettingsModel } from '../tokens';
7
6
  export declare namespace MainAreaChat {
8
7
  interface IOptions extends MainAreaWidget.IOptions<ChatWidget> {
9
8
  commands: CommandRegistry;
@@ -20,7 +19,7 @@ export declare class MainAreaChat extends MainAreaWidget<ChatWidget> {
20
19
  /**
21
20
  * Get the model of the chat.
22
21
  */
23
- get model(): AIChatModel;
22
+ get model(): IAIChatModel;
24
23
  /**
25
24
  * Get the area of the chat.
26
25
  */
@@ -43,14 +43,14 @@ export class MainAreaChat extends MainAreaWidget {
43
43
  this._outputAreaCompat = new RenderedMessageOutputAreaCompat({
44
44
  chatPanel: this.content
45
45
  });
46
- this.model.writersChanged.connect(this._writersChanged);
46
+ this.model.writersChanged?.connect(this._writersChanged);
47
47
  this.model.titleChanged.connect(this._titleChanged);
48
48
  }
49
49
  dispose() {
50
50
  super.dispose();
51
51
  // Dispose of the approval buttons widget when the chat is disposed.
52
52
  this._outputAreaCompat.dispose();
53
- this.model.writersChanged.disconnect(this._writersChanged);
53
+ this.model.writersChanged?.disconnect(this._writersChanged);
54
54
  this.model.titleChanged.disconnect(this._titleChanged);
55
55
  }
56
56
  /**
@@ -69,12 +69,10 @@ export class MainAreaChat extends MainAreaWidget {
69
69
  // Check if AI is currently writing (streaming)
70
70
  const aiWriting = writers.some(writer => writer.user.username === 'ai-assistant');
71
71
  if (aiWriting) {
72
- this.content.inputToolbarRegistry?.hide('send');
73
72
  this.content.inputToolbarRegistry?.show('stop');
74
73
  }
75
74
  else {
76
75
  this.content.inputToolbarRegistry?.hide('stop');
77
- this.content.inputToolbarRegistry?.show('send');
78
76
  }
79
77
  };
80
78
  _titleChanged = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/ai",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "AI code completions and chat for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -54,7 +54,7 @@
54
54
  "watch:labextension": "jupyter labextension watch .",
55
55
  "docs": "jupyter book start",
56
56
  "docs:build": "sed -e 's/\\[@/[/g' -e 's/@/\\&#64;/g' CHANGELOG.md > docs/_changelog_content.md && jupyter book build --html",
57
- "sync:model-context-windows": "node scripts/sync-model-context-windows.mjs && prettier --write src/providers/generated-context-windows.ts && eslint --fix src/providers/generated-context-windows.ts"
57
+ "sync:model-info": "node scripts/sync-model-info.mjs && prettier --write src/providers/generated-model-info.ts && eslint --fix src/providers/generated-model-info.ts"
58
58
  },
59
59
  "dependencies": {
60
60
  "@ai-sdk/anthropic": "^3.0.58",
@@ -89,7 +89,7 @@
89
89
  "@mui/icons-material": "^7",
90
90
  "@mui/material": "^7",
91
91
  "ai": "^6.0.116",
92
- "jupyter-chat-components": "^0.2.0",
92
+ "jupyter-chat-components": "^0.5.0",
93
93
  "jupyter-secrets-manager": "^0.5.0",
94
94
  "yaml": "^2.8.1",
95
95
  "zod": "^4.3.6"
@@ -210,6 +210,12 @@
210
210
  "type": "boolean",
211
211
  "default": false
212
212
  },
213
+ "autoTitle": {
214
+ "title": "Auto Title",
215
+ "description": "Automatically request a title from the model for every message until there are 5 messages",
216
+ "type": "boolean",
217
+ "default": false
218
+ },
213
219
  "showTokenUsage": {
214
220
  "title": "Show Token Usage",
215
221
  "description": "Display token usage information in the chat toolbar",
package/src/agent.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createMCPClient, type MCPClient } from '@ai-sdk/mcp';
2
- import type { IMessageContent } from '@jupyter/chat';
3
2
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
3
  import { PromiseDelegate } from '@lumino/coreutils';
5
4
  import { ISignal, Signal } from '@lumino/signaling';
@@ -490,32 +489,12 @@ export class AgentManager implements IAgentManager {
490
489
  }
491
490
 
492
491
  /**
493
- * Sets the history with a list of messages from the chat.
494
- * @param messages The chat messages to set as history
492
+ * Sets the history from already-processed model messages.
493
+ * @param messages Pre-built model messages (may include binary content)
495
494
  */
496
- setHistory(messages: IMessageContent[]): void {
497
- // Stop any ongoing streaming and reject awaiting approvals
498
- this.stopStreaming();
499
-
500
- for (const [approvalId, pending] of this._pendingApprovals) {
501
- pending.resolve(false, 'Chat history changed');
502
- this._agentEvent.emit({
503
- type: 'tool_approval_resolved',
504
- data: { approvalId, approved: false }
505
- });
506
- }
507
- this._pendingApprovals.clear();
508
-
509
- // Convert chat messages to model messages
510
- const modelMessages: ModelMessage[] = messages.map(msg => {
511
- const role =
512
- msg.sender.username === 'ai-assistant' ? 'assistant' : 'user';
513
- return {
514
- role,
515
- content: msg.body
516
- };
517
- });
518
- this._history = Private.sanitizeModelMessages(modelMessages);
495
+ setHistory(messages: ModelMessage[]): void {
496
+ this.stopStreaming('Chat history changed');
497
+ this._history = Private.sanitizeModelMessages(messages);
519
498
  }
520
499
 
521
500
  /**
@@ -526,11 +505,11 @@ export class AgentManager implements IAgentManager {
526
505
  this._controller?.abort();
527
506
 
528
507
  // Reject any pending approvals
529
- for (const [approvalId, pending] of this._pendingApprovals) {
508
+ for (const [toolCallId, pending] of this._pendingApprovals) {
530
509
  pending.resolve(false, reason ?? 'Stream ended by user');
531
510
  this._agentEvent.emit({
532
511
  type: 'tool_approval_resolved',
533
- data: { approvalId, approved: false }
512
+ data: { toolCallId, approved: false }
534
513
  });
535
514
  }
536
515
  this._pendingApprovals.clear();
@@ -538,34 +517,34 @@ export class AgentManager implements IAgentManager {
538
517
 
539
518
  /**
540
519
  * Approves a pending tool call.
541
- * @param approvalId The approval ID to approve
520
+ * @param toolCallId The tool call ID to approve
542
521
  * @param reason Optional reason for approval
543
522
  */
544
- approveToolCall(approvalId: string, reason?: string): void {
545
- const pending = this._pendingApprovals.get(approvalId);
523
+ approveToolCall(toolCallId: string, reason?: string): void {
524
+ const pending = this._pendingApprovals.get(toolCallId);
546
525
  if (pending) {
547
526
  pending.resolve(true, reason);
548
- this._pendingApprovals.delete(approvalId);
527
+ this._pendingApprovals.delete(toolCallId);
549
528
  this._agentEvent.emit({
550
529
  type: 'tool_approval_resolved',
551
- data: { approvalId, approved: true }
530
+ data: { toolCallId, approved: true }
552
531
  });
553
532
  }
554
533
  }
555
534
 
556
535
  /**
557
536
  * Rejects a pending tool call.
558
- * @param approvalId The approval ID to reject
537
+ * @param toolCallId The tool call ID to reject
559
538
  * @param reason Optional reason for rejection
560
539
  */
561
- rejectToolCall(approvalId: string, reason?: string): void {
562
- const pending = this._pendingApprovals.get(approvalId);
540
+ rejectToolCall(toolCallId: string, reason?: string): void {
541
+ const pending = this._pendingApprovals.get(toolCallId);
563
542
  if (pending) {
564
543
  pending.resolve(false, reason);
565
- this._pendingApprovals.delete(approvalId);
544
+ this._pendingApprovals.delete(toolCallId);
566
545
  this._agentEvent.emit({
567
546
  type: 'tool_approval_resolved',
568
- data: { approvalId, approved: false }
547
+ data: { toolCallId, approved: false }
569
548
  });
570
549
  }
571
550
  }
@@ -663,19 +642,10 @@ export class AgentManager implements IAgentManager {
663
642
  error.statusCode === 415 ||
664
643
  error.statusCode === 422)
665
644
  ) {
666
- for (const msg of [...this._history, ...responseHistory]) {
667
- if (msg.role === 'user' && Array.isArray(msg.content)) {
668
- const hasMedia = msg.content.some(p => p.type !== 'text');
669
- if (hasMedia) {
670
- const textContent = msg.content
671
- .filter(p => p.type === 'text')
672
- .map(p => (p as { text: string }).text)
673
- .join('\n');
674
- msg.content =
675
- textContent || '_Attachment removed due to error_';
676
- }
677
- }
678
- }
645
+ this._stripAttachments(
646
+ [...this._history, ...responseHistory],
647
+ '_Attachment removed due to error_'
648
+ );
679
649
  helpMessage +=
680
650
  '\n\nAttachments have been removed from history. Please send your prompt again.';
681
651
  }
@@ -735,6 +705,27 @@ export class AgentManager implements IAgentManager {
735
705
  this._tokenUsageChanged.emit(this._tokenUsage);
736
706
  }
737
707
 
708
+ /**
709
+ * Removes image and file parts from all user messages in the given list.
710
+ */
711
+ private _stripAttachments(
712
+ messages: ModelMessage[],
713
+ placeholder: string
714
+ ): void {
715
+ for (const msg of messages) {
716
+ if (msg.role === 'user' && Array.isArray(msg.content)) {
717
+ const hasMedia = msg.content.some(p => p.type !== 'text');
718
+ if (hasMedia) {
719
+ const textContent = msg.content
720
+ .filter(p => p.type === 'text')
721
+ .map(p => (p as { text: string }).text)
722
+ .join('\n');
723
+ msg.content = textContent || placeholder;
724
+ }
725
+ }
726
+ }
727
+ }
728
+
738
729
  /**
739
730
  * Gets the configured context window for the active provider.
740
731
  */
@@ -1092,14 +1083,13 @@ ${richOutputWorkflowInstruction}`;
1092
1083
  this._agentEvent.emit({
1093
1084
  type: 'tool_approval_request',
1094
1085
  data: {
1095
- approvalId,
1096
1086
  toolCallId: toolCall.toolCallId,
1097
1087
  toolName: toolCall.toolName,
1098
1088
  args: toolCall.input
1099
1089
  }
1100
1090
  });
1101
1091
 
1102
- const approved = await this._waitForApproval(approvalId);
1092
+ const approved = await this._waitForApproval(toolCall.toolCallId);
1103
1093
 
1104
1094
  result.approvalProcessed = true;
1105
1095
  result.approvalResponse = {
@@ -1116,12 +1106,12 @@ ${richOutputWorkflowInstruction}`;
1116
1106
 
1117
1107
  /**
1118
1108
  * Waits for user approval of a tool call.
1119
- * @param approvalId The approval ID to wait for
1109
+ * @param toolCallId The tool call ID to wait for approval
1120
1110
  * @returns Promise that resolves to true if approved, false if rejected
1121
1111
  */
1122
- private _waitForApproval(approvalId: string): Promise<boolean> {
1112
+ private _waitForApproval(toolCallId: string): Promise<boolean> {
1123
1113
  return new Promise(resolve => {
1124
- this._pendingApprovals.set(approvalId, {
1114
+ this._pendingApprovals.set(toolCallId, {
1125
1115
  resolve: (approved: boolean) => {
1126
1116
  resolve(approved);
1127
1117
  }
@@ -6,6 +6,7 @@ import { Contents } from '@jupyterlab/services';
6
6
  import { AIChatModel } from './chat-model';
7
7
  import type {
8
8
  IAgentManagerFactory,
9
+ IAIChatModel,
9
10
  IAISettingsModel,
10
11
  IChatModelHandler,
11
12
  ICreateChatOptions,
@@ -28,7 +29,7 @@ export class ChatModelHandler implements IChatModelHandler {
28
29
  this._contentsManager = options.contentsManager;
29
30
  }
30
31
 
31
- createModel(options: ICreateChatOptions): AIChatModel {
32
+ createModel(options: ICreateChatOptions): IAIChatModel {
32
33
  const { name, activeProvider, tokenUsage, messages, autosave, title } =
33
34
  options;
34
35
 
@@ -49,7 +50,8 @@ export class ChatModelHandler implements IChatModelHandler {
49
50
  agentManager,
50
51
  activeCellManager: this._activeCellManager,
51
52
  documentManager: this._docManager,
52
- contentsManager: this._contentsManager
53
+ contentsManager: this._contentsManager,
54
+ providerRegistry: this._providerRegistry
53
55
  });
54
56
 
55
57
  messages?.forEach(message => {