Skip to content

[v3.0.0bx]: Define chat command provider for slash commands #1278

Closed
@dlqqq

Description

@dlqqq

Problem

jupyterlab-chat v0.8.0 has now been released. Notably, this release replaces the old autocomplete framework with the new chat commands framework introduced by jupyterlab/jupyter-chat#161.

However, the main (3.x) branch needs to be updated to use the new chat commands framework.

Proposed Solution

Update jupyterlab-chat to 0.8.1:

  1. Update the version range of jupyterlab-chat to jupyterlab-chat>=0.8.1,<0.9.0 in packages/jupyter-ai/pyproject.toml.
  2. Update the version range of @jupyter/chat to "@jupyter/chat": "^0.8.1" in packages/jupyter-ai/package.json.
  3. Remove packages/jupyter-ai/src/slash-autocompletion.tsx and delete the chat_autocompletion plugin in packages/jupyter-ai/src/index.ts. You can also remove the import { IAutocompletionRegistry } from '@jupyter/chat' statement since that no longer is available in 0.8.0.

Define a new plugin that registers a chat command provider

  1. Create a new folder: packages/jupyter-ai/src/chat-commands/.
  2. Create a new file titled slash-commands.ts. For now, you can use the reference implementation below. You may have to update it to conform to the new command provider interface defined here: Add new InputModel class for managing input state jupyter-chat#171
Reference code (click to expand)
/*
 * Copyright (c) Jupyter Development Team.
 * Distributed under the terms of the Modified BSD License.
 */

import { JupyterFrontEndPlugin } from '@jupyterlab/application';
import {
  IChatCommandProvider,
  IChatCommandRegistry,
  IInputModel,
  ChatCommand
} from '@jupyter/chat';

export class SlashCommandProvider implements IChatCommandProvider {
  public id: string = '@jupyter-ai/core:slash-command-provider';
  private _slash_commands: ChatCommand[] = [
    { name: '/ask', providerId: this.id, replaceWith: '/ask ' },
    { name: '/learn', providerId: this.id, replaceWith: '/learn ' },
    { name: '/help', providerId: this.id, replaceWith: '/help ' }
  ];

  // regex used to test the current word
  private _regex: RegExp = /^\/\w*/;

  async getChatCommands(inputModel: IInputModel) {
    const match = inputModel.currentWord.match(this._regex)?.[0];
    if (!match) {
      return [];
    }

    const commands = this._slash_commands.filter(cmd =>
      cmd.name.startsWith(match)
    );
    return commands;
  }

  async handleChatCommand(
    command: ChatCommand,
    inputModel: IInputModel
  ): Promise<void> {
    // no handling needed because `replaceWith` is set in each command.
    return;
  }
}

export const slashCommandPlugin: JupyterFrontEndPlugin<void> = {
  id: '@jupyter-ai/core:slash-command-plugin',
  description:
    'Adds Jupyter AI slash commands to the chat commands menu.',
  autoStart: true,
  requires: [IChatCommandRegistry],
  activate: (app, registry: IChatCommandRegistry) => {
    registry.addProvider(new SlashCommandProvider());
  }

};

  1. Provide this plugin by adding it to the list of default exports in packages/jupyter-ai/src/index.ts. The list of exports should look like this:
 export default [
   plugin,
   statusItemPlugin,
   completionPlugin,
-  chat_autocompletion
+  slashCommandPlugin
 ];
  1. Re-build the lab extension frontend via jlpm build and refresh your browser. You can test the functionality by typing the / character and see if the menu pops up.

Updating the slash command provider to use the API

  1. Update async getChatCommands() to call AiService.listSlashCommands(). This should be self-explanatory.
  2. Update the returned list of commands to include icons for each slash command. The default list of icons can be found here:
    const DEFAULT_SLASH_COMMAND_ICONS: Record<string, JSX.Element> = {
    ask: <FindInPage />,
    clear: <HideSource />,
    export: <Download />,
    fix: <AutoFixNormal />,
    generate: <MenuBook />,
    help: <Help />,
    learn: <School />,
    unknown: <MoreHoriz />
    };
  3. (optional challenge) Inside SlashCommandProvider.constructor(), create a Promise that fetches the list of slash commands and processes it into a list of the type ChatCommand[]. This should be saved as a private attribute like this._slashCommands: Promise<ChatCommand[]> = .... Then, getChatCommands() can just call return await this._chatCommands. This ensures that a network request is only made once, and all calls to this command provider just return a cached value, improving performance substantially.

Feel free to open a PR anyways if you get stuck on step 3). I'll share more guidance in my review if necessary.

Additional context

@keerthi-swarna This will be a more challenging assignment, since it requires an understanding of how JupyterLab plugins work. This doc will be useful: https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#application-plugins

Essentially, every lab extension is a set of "plugins". Each plugin can do two things simultaneously:

  1. It can provide a token that other plugins can require/depend upon. This allows a plugin to provide a service to other plugins.
  2. It can require tokens provided by another plugins. This allows a plugin to use services provided by other plugins.

In JupyterLab, the entire frontend is defined using this plugin architecture. This means that an extensions' plugins can override default plugins provided by jupyterlab, and use other plugins provided by jupyterlab.

JupyterLab automatically initializes each of the plugins in the correct order, presumably by performing a topological sort on a directed acyclic graph (DAG). Understanding what a topological sort does may help you understand how plugins are initialized in JupyterLab.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestscope:chat-commandsIssues concerning chat commands in general

Type

No type

Projects

Status

Done

Relationships

None yet

Development

No branches or pull requests

Issue actions