@jupyterlite/ai 0.15.0 → 0.17.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 (45) hide show
  1. package/lib/agent.d.ts +12 -2
  2. package/lib/agent.js +112 -17
  3. package/lib/chat-commands/clear.js +1 -1
  4. package/lib/chat-model-handler.js +4 -1
  5. package/lib/chat-model.d.ts +25 -24
  6. package/lib/chat-model.js +262 -132
  7. package/lib/components/clear-button.d.ts +1 -1
  8. package/lib/components/clear-button.js +1 -1
  9. package/lib/components/index.d.ts +1 -1
  10. package/lib/components/index.js +1 -1
  11. package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +11 -11
  12. package/lib/components/usage-display.js +109 -0
  13. package/lib/index.js +205 -20
  14. package/lib/models/settings-model.js +1 -0
  15. package/lib/providers/built-in-providers.js +5 -0
  16. package/lib/providers/generated-context-windows.d.ts +8 -0
  17. package/lib/providers/generated-context-windows.js +96 -0
  18. package/lib/providers/model-info.d.ts +3 -0
  19. package/lib/providers/model-info.js +58 -0
  20. package/lib/tokens.d.ts +34 -3
  21. package/lib/tokens.js +8 -7
  22. package/lib/widgets/ai-settings.js +9 -0
  23. package/lib/widgets/main-area-chat.d.ts +1 -0
  24. package/lib/widgets/main-area-chat.js +10 -4
  25. package/lib/widgets/provider-config-dialog.js +18 -5
  26. package/package.json +3 -2
  27. package/schema/settings-model.json +11 -0
  28. package/src/agent.ts +151 -21
  29. package/src/chat-commands/clear.ts +1 -1
  30. package/src/chat-model-handler.ts +6 -1
  31. package/src/chat-model.ts +350 -175
  32. package/src/components/clear-button.tsx +3 -3
  33. package/src/components/index.ts +1 -1
  34. package/src/components/usage-display.tsx +208 -0
  35. package/src/index.ts +250 -26
  36. package/src/models/settings-model.ts +1 -0
  37. package/src/providers/built-in-providers.ts +5 -0
  38. package/src/providers/generated-context-windows.ts +102 -0
  39. package/src/providers/model-info.ts +88 -0
  40. package/src/tokens.ts +46 -10
  41. package/src/widgets/ai-settings.tsx +42 -0
  42. package/src/widgets/main-area-chat.ts +12 -4
  43. package/src/widgets/provider-config-dialog.tsx +45 -5
  44. package/lib/components/token-usage-display.js +0 -72
  45. package/src/components/token-usage-display.tsx +0 -137
package/lib/agent.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { IMessageContent } from '@jupyter/chat';
2
2
  import { ISignal } from '@lumino/signaling';
3
+ import { type ModelMessage, type UserContent } from 'ai';
3
4
  import { ISecretsManager } from 'jupyter-secrets-manager';
4
5
  import { type IAgentManager, type IAgentManagerFactory, type IAISettingsModel, type ISkillRegistry, type ITokenUsage, type ToolMap } from './tokens';
5
6
  /**
@@ -158,11 +159,20 @@ export declare class AgentManager implements IAgentManager {
158
159
  * Handles the complete execution cycle including tool calls.
159
160
  * @param message The user message to respond to (may include processed attachment content)
160
161
  */
161
- generateResponse(message: string): Promise<void>;
162
+ generateResponse(message: UserContent): Promise<void>;
162
163
  /**
163
- * Updates token usage statistics.
164
+ * Create a transient language model to request a text response which won't be added to history.
165
+ * @param messages - the messages sequence to send to the model.
166
+ */
167
+ textResponse(messages: ModelMessage[]): Promise<string>;
168
+ /**
169
+ * Updates cumulative token usage statistics from a completed model step.
164
170
  */
165
171
  private _updateTokenUsage;
172
+ /**
173
+ * Gets the configured context window for the active provider.
174
+ */
175
+ private _getActiveContextWindow;
166
176
  /**
167
177
  * Initializes the AI agent with current settings and tools.
168
178
  * Sets up the agent with model configuration, tools, and MCP tools.
package/lib/agent.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createMCPClient } from '@ai-sdk/mcp';
2
2
  import { PromiseDelegate } from '@lumino/coreutils';
3
3
  import { Signal } from '@lumino/signaling';
4
- import { ToolLoopAgent, stepCountIs } from 'ai';
4
+ import { generateText, ToolLoopAgent, stepCountIs, APICallError } from 'ai';
5
5
  import { createModel } from './providers/models';
6
+ import { getEffectiveContextWindow } from './providers/model-info';
6
7
  import { createProviderTools } from './providers/provider-tools';
7
8
  import { SECRETS_NAMESPACE } from './tokens';
8
9
  /**
@@ -256,7 +257,14 @@ export class AgentManager {
256
257
  return this._activeProvider;
257
258
  }
258
259
  set activeProvider(value) {
260
+ const previousProvider = this._activeProvider;
259
261
  this._activeProvider = value;
262
+ // Reset request-level context estimate only when switching between providers.
263
+ if (previousProvider && previousProvider !== value) {
264
+ this._tokenUsage.lastRequestInputTokens = undefined;
265
+ }
266
+ this._tokenUsage.contextWindow = this._getActiveContextWindow();
267
+ this._tokenUsageChanged.emit(this._tokenUsage);
260
268
  this.initializeAgent();
261
269
  this._activeProviderChanged.emit(this._activeProvider);
262
270
  }
@@ -315,7 +323,11 @@ export class AgentManager {
315
323
  await this._streaming.promise;
316
324
  // Clear history and token usage
317
325
  this._history = [];
318
- this._tokenUsage = { inputTokens: 0, outputTokens: 0 };
326
+ this._tokenUsage = {
327
+ inputTokens: 0,
328
+ outputTokens: 0,
329
+ contextWindow: this._getActiveContextWindow()
330
+ };
319
331
  this._tokenUsageChanged.emit(this._tokenUsage);
320
332
  }
321
333
  /**
@@ -335,9 +347,9 @@ export class AgentManager {
335
347
  this._pendingApprovals.clear();
336
348
  // Convert chat messages to model messages
337
349
  const modelMessages = messages.map(msg => {
338
- const isAIMessage = msg.sender.username === 'ai-assistant';
350
+ const role = msg.sender.username === 'ai-assistant' ? 'assistant' : 'user';
339
351
  return {
340
- role: isAIMessage ? 'assistant' : 'user',
352
+ role,
341
353
  content: msg.body
342
354
  };
343
355
  });
@@ -400,6 +412,11 @@ export class AgentManager {
400
412
  this._streaming = new PromiseDelegate();
401
413
  this._controller = new AbortController();
402
414
  const responseHistory = [];
415
+ // Add user message to history
416
+ responseHistory.push({
417
+ role: 'user',
418
+ content: message
419
+ });
403
420
  try {
404
421
  // Ensure we have an agent
405
422
  if (!this._agent) {
@@ -408,11 +425,6 @@ export class AgentManager {
408
425
  if (!this._agent) {
409
426
  throw new Error('Failed to initialize agent');
410
427
  }
411
- // Add user message to history
412
- responseHistory.push({
413
- role: 'user',
414
- content: message
415
- });
416
428
  let continueLoop = true;
417
429
  while (continueLoop) {
418
430
  const result = await this._agent.stream({
@@ -420,9 +432,20 @@ export class AgentManager {
420
432
  abortSignal: this._controller.signal
421
433
  });
422
434
  const streamResult = await this._processStreamResult(result);
423
- // Get response messages and update token usage
435
+ if (streamResult.aborted) {
436
+ try {
437
+ const responseMessages = await result.response;
438
+ if (responseMessages.messages?.length) {
439
+ this._history.push(...Private.sanitizeModelMessages(responseMessages.messages));
440
+ }
441
+ }
442
+ catch {
443
+ // Aborting before a step finishes leaves no completed response to persist.
444
+ }
445
+ break;
446
+ }
447
+ // Get response messages for completed steps.
424
448
  const responseMessages = await result.response;
425
- this._updateTokenUsage(await result.usage);
426
449
  // Add response messages to history
427
450
  if (responseMessages.messages?.length) {
428
451
  responseHistory.push(...responseMessages.messages);
@@ -450,9 +473,38 @@ export class AgentManager {
450
473
  }
451
474
  catch (error) {
452
475
  if (error.name !== 'AbortError') {
476
+ let helpMessage = `${error.message}`;
477
+ // Remove attachments from history on payload rejection errors
478
+ if (APICallError.isInstance(error) &&
479
+ (error.statusCode === 400 ||
480
+ error.statusCode === 404 ||
481
+ error.statusCode === 413 ||
482
+ error.statusCode === 415 ||
483
+ error.statusCode === 422)) {
484
+ for (const msg of [...this._history, ...responseHistory]) {
485
+ if (msg.role === 'user' && Array.isArray(msg.content)) {
486
+ const hasMedia = msg.content.some(p => p.type !== 'text');
487
+ if (hasMedia) {
488
+ const textContent = msg.content
489
+ .filter(p => p.type === 'text')
490
+ .map(p => p.text)
491
+ .join('\n');
492
+ msg.content =
493
+ textContent || '_Attachment removed due to error_';
494
+ }
495
+ }
496
+ }
497
+ helpMessage +=
498
+ '\n\nAttachments have been removed from history. Please send your prompt again.';
499
+ }
453
500
  this._agentEvent.emit({
454
501
  type: 'error',
455
- data: { error: error }
502
+ data: { error: new Error(helpMessage) }
503
+ });
504
+ this._history.push(...Private.sanitizeModelMessages(responseHistory));
505
+ this._history.push({
506
+ role: 'assistant',
507
+ content: helpMessage
456
508
  });
457
509
  }
458
510
  }
@@ -462,14 +514,43 @@ export class AgentManager {
462
514
  }
463
515
  }
464
516
  /**
465
- * Updates token usage statistics.
517
+ * Create a transient language model to request a text response which won't be added to history.
518
+ * @param messages - the messages sequence to send to the model.
466
519
  */
467
- _updateTokenUsage(usage) {
520
+ async textResponse(messages) {
521
+ try {
522
+ const model = await this._createModel();
523
+ const result = await generateText({
524
+ model,
525
+ messages
526
+ });
527
+ this._updateTokenUsage(result.totalUsage, result.totalUsage.inputTokens);
528
+ return result.text;
529
+ }
530
+ catch (e) {
531
+ throw `Error while getting the topic of the chat\n${e}`;
532
+ }
533
+ }
534
+ /**
535
+ * Updates cumulative token usage statistics from a completed model step.
536
+ */
537
+ _updateTokenUsage(usage, lastRequestInputTokens) {
538
+ const contextWindow = this._getActiveContextWindow();
539
+ const estimatedRequestInputTokens = lastRequestInputTokens ?? usage?.inputTokens;
468
540
  if (usage) {
469
541
  this._tokenUsage.inputTokens += usage.inputTokens ?? 0;
470
542
  this._tokenUsage.outputTokens += usage.outputTokens ?? 0;
471
- this._tokenUsageChanged.emit(this._tokenUsage);
472
543
  }
544
+ this._tokenUsage.lastRequestInputTokens = estimatedRequestInputTokens;
545
+ this._tokenUsage.contextWindow = contextWindow;
546
+ this._tokenUsageChanged.emit(this._tokenUsage);
547
+ }
548
+ /**
549
+ * Gets the configured context window for the active provider.
550
+ */
551
+ _getActiveContextWindow() {
552
+ const activeProviderConfig = this._settingsModel.getProvider(this._activeProvider);
553
+ return getEffectiveContextWindow(activeProviderConfig, this._providerRegistry);
473
554
  }
474
555
  /**
475
556
  * Initializes the AI agent with current settings and tools.
@@ -521,6 +602,9 @@ export class AgentManager {
521
602
  const activeProviderInfo = activeProviderConfig && this._providerRegistry
522
603
  ? this._providerRegistry.getProviderInfo(activeProviderConfig.provider)
523
604
  : null;
605
+ const contextWindow = getEffectiveContextWindow(activeProviderConfig, this._providerRegistry);
606
+ this._tokenUsage.contextWindow = contextWindow;
607
+ this._tokenUsageChanged.emit(this._tokenUsage);
524
608
  const temperature = activeProviderConfig?.parameters?.temperature ?? DEFAULT_TEMPERATURE;
525
609
  const maxTokens = activeProviderConfig?.parameters?.maxOutputTokens;
526
610
  const maxTurns = activeProviderConfig?.parameters?.maxTurns ?? DEFAULT_MAX_TURNS;
@@ -599,7 +683,10 @@ ${richOutputWorkflowInstruction}`;
599
683
  async _processStreamResult(result) {
600
684
  let fullResponse = '';
601
685
  let currentMessageId = null;
602
- const processResult = { approvalProcessed: false };
686
+ const processResult = {
687
+ approvalProcessed: false,
688
+ aborted: false
689
+ };
603
690
  for await (const part of result.fullStream) {
604
691
  switch (part.type) {
605
692
  case 'text-delta':
@@ -654,7 +741,15 @@ ${richOutputWorkflowInstruction}`;
654
741
  }
655
742
  await this._handleApprovalRequest(part, processResult);
656
743
  break;
657
- // Ignore: text-start, text-end, finish, error, and others
744
+ case 'error':
745
+ throw part.error;
746
+ case 'finish-step':
747
+ this._updateTokenUsage(part.usage, part.usage.inputTokens);
748
+ break;
749
+ case 'abort':
750
+ processResult.aborted = true;
751
+ break;
752
+ // Ignore: text-start, text-end, finish, and others
658
753
  default:
659
754
  break;
660
755
  }
@@ -16,7 +16,7 @@ export class ClearCommandProvider {
16
16
  return;
17
17
  }
18
18
  const context = inputModel.chatContext;
19
- context?.clearMessages?.();
19
+ await context?.clearMessages?.();
20
20
  inputModel.value = '';
21
21
  inputModel.clearAttachments();
22
22
  inputModel.clearMentions();
@@ -14,7 +14,7 @@ export class ChatModelHandler {
14
14
  this._contentsManager = options.contentsManager;
15
15
  }
16
16
  createModel(options) {
17
- const { name, activeProvider, tokenUsage, messages, autosave } = options;
17
+ const { name, activeProvider, tokenUsage, messages, autosave, title } = options;
18
18
  // Create Agent Manager first so it can be shared
19
19
  const agentManager = this._agentManagerFactory.createAgent({
20
20
  settingsModel: this._settingsModel,
@@ -38,6 +38,9 @@ export class ChatModelHandler {
38
38
  });
39
39
  model.autosave = autosave ?? false;
40
40
  model.name = name;
41
+ if (title) {
42
+ model.title = title;
43
+ }
41
44
  return model;
42
45
  }
43
46
  /**
@@ -18,6 +18,19 @@ export declare class AIChatModel extends AbstractChatModel {
18
18
  */
19
19
  get name(): string;
20
20
  set name(value: string);
21
+ /**
22
+ * A signal emitting when the chat name has changed.
23
+ */
24
+ get nameChanged(): ISignal<AIChatModel, string>;
25
+ /**
26
+ * The title of the chat.
27
+ */
28
+ get title(): string | null;
29
+ set title(value: string | null);
30
+ /**
31
+ * A signal emitting when the chat title has changed.
32
+ */
33
+ get titleChanged(): ISignal<AIChatModel, string | null>;
21
34
  /**
22
35
  * Whether to save the chat automatically.
23
36
  */
@@ -27,10 +40,6 @@ export declare class AIChatModel extends AbstractChatModel {
27
40
  * A signal emitting when the autosave flag changed.
28
41
  */
29
42
  get autosaveChanged(): ISignal<AIChatModel, boolean>;
30
- /**
31
- * A signal emitting when the chat name has changed.
32
- */
33
- get nameChanged(): ISignal<AIChatModel, string>;
34
43
  /**
35
44
  * Gets the current user information.
36
45
  */
@@ -62,7 +71,7 @@ export declare class AIChatModel extends AbstractChatModel {
62
71
  /**
63
72
  * Clears all messages from the chat and resets conversation state.
64
73
  */
65
- clearMessages: () => void;
74
+ clearMessages: () => Promise<void>;
66
75
  /**
67
76
  * Adds a non-user message to the chat (used by chat commands).
68
77
  */
@@ -83,6 +92,10 @@ export declare class AIChatModel extends AbstractChatModel {
83
92
  * restoration is not possible.
84
93
  */
85
94
  restore: (filepath: string, silent?: boolean) => Promise<boolean>;
95
+ /**
96
+ * Request a title to this chat, regarding the message history.
97
+ */
98
+ requestTitle(): Promise<string>;
86
99
  /**
87
100
  * Serialize the model for backup
88
101
  */
@@ -155,24 +168,6 @@ export declare class AIChatModel extends AbstractChatModel {
155
168
  * Updates a tool call's UI with new status and optional output.
156
169
  */
157
170
  private _updateToolCallUI;
158
- /**
159
- * Processes file attachments and returns their content as formatted strings.
160
- * @param attachments Array of file attachments to process
161
- * @returns Array of formatted attachment contents
162
- */
163
- private _processAttachments;
164
- /**
165
- * Reads the content of a notebook cell.
166
- * @param attachment The notebook attachment to read
167
- * @returns Cell content as string or null if unable to read
168
- */
169
- private _readNotebookCells;
170
- /**
171
- * Reads the content of a file attachment.
172
- * @param attachment The file attachment to read
173
- * @returns File content as string or null if unable to read
174
- */
175
- private _readFileAttachment;
176
171
  private _settingsModel;
177
172
  private _user;
178
173
  private _toolContexts;
@@ -183,6 +178,8 @@ export declare class AIChatModel extends AbstractChatModel {
183
178
  private _autosave;
184
179
  private _autosaveChanged;
185
180
  private _autosaveDebouncer;
181
+ private _title;
182
+ private _titleChanged;
186
183
  }
187
184
  /**
188
185
  * Namespace containing types and interfaces for AIChatModel.
@@ -232,7 +229,7 @@ export declare namespace AIChatModel {
232
229
  /**
233
230
  * The clear messages callback.
234
231
  */
235
- clearMessages: () => void;
232
+ clearMessages: () => Promise<void>;
236
233
  /**
237
234
  * Adds an assistant/system message to the chat.
238
235
  */
@@ -274,6 +271,10 @@ export declare namespace AIChatModel {
274
271
  * Whether the chat is automatically saved.
275
272
  */
276
273
  autosave?: boolean;
274
+ /**
275
+ * An optional title of the chat.
276
+ */
277
+ title?: string;
277
278
  };
278
279
  };
279
280
  }