Skip to content

Design proposal: Chat Completions API (rev. 1.1) #144

@dlqqq

Description

@dlqqq

Description

This issue proposes a design for a new Chat Completions API. This API will allow consumer extensions to provide completions for the user's current input from the UI. In this context, a consumer extension is any frontend + server extension that intends to provide completions for substrings in the chat input.

Motivation

Suppose a user types / in the chat with Jupyter AI installed. Today, Jupyter Chat responds by showing a menu of chat completions:

Screenshot 2025-01-02 at 5 08 27 PM

The opening of this completions menu is triggered simply by typing /. However, the current implementation only allows a single "trigger character" (/). This means that @ commands in Jupyter AI cannot be autocompleted. Furthermore, the completer makes a network call every time a user types /.

This design aims to:

  1. Allow multiple completers to provide completions,
  2. Allow triggering patterns to be strictly defined, and
  3. Generate completions in a way which minimizes network calls.

To help explain the proposed design, this document will start from the perspective of a consumer extension, then work backwards towards the necessary changes in Jupyter Chat.

Step 1: Define a new IChatCompleter interface

To register completions for partial inputs, a consumer extension must provide a set of chat completers. A chat completer is a JavaScript/TypeScript class which provides:

  • id (property): Defines a unique ID for this chat completer. We will see why this is useful later.

  • regex (property): Defines a regex which matches any incomplete input.

    • Each regex should end with $ to ensure this regex only matches partial inputs just typed by the user. Without $, the completer may generate completions for commands which were already typed.
  • async initialize(): void: called and awaited by Jupyter Chat.

  • async getCompletions(match: str): ChatCompletion[]: Defines a method which accepts a substring matched by its regex, and returns a list of potential completions for that input. This list may be empty.

    • It would be helpful to think of this method as just returning a list of completions for the user's input. We return a list of objects to allow each completion to have metadata, such as the description & icon.

It's important to note that a consumer extension may provide more than 1 completer. This allows extensions to provide completions for different commands which aren't easily captured by a single regex. For example, Jupyter AI can have a completer for / commands and another completer for @ commands.

Jupyter Chat will define a new IChatCompleter interface which chat completers must implement, shown below.

import { LabIcon } from "@jupyterlab/ui-components";

type ChatCompletion = {
    // e.g. "/ask" if the input was `/`
    value: string;
    
    // if set, use this as the label. otherwise use `value`.
    label?: string;
    
    // if set, show this as a subtitle.
    description?: string;
    
    // identifies which icon should be used, if any.
    // Jupyter Chat should choose a default if one is not provided.
    icon?: LabIcon;
}

interface IChatCompleter {
    id: string;
    regex: string;
    async function initialize(): void;
    async function getCompletions(match: str): ChatCompletion[]
}

The consumer extension will construct/instantiate the class itself before providing it to Jupyter Chat. Jupyter Chat will call await initialize() on each completer on init. The details of this will be discussed later.

To define a chat completer, a consumer extension should implement the IChatCompleter interface. Here is an example of how Jupyter AI may implement a chat completer to provide completions for its slash commands:

import { IChatCompleter } from "@jupyter/chat"
import { AiService } from "@jupyter-ai/core"

class SlashCommandCompleter implements IChatCompleter {
    public id: string = "jai-slash-commands";
    
    /**
     * matches when:
       - any partial slash command appears at start of input
       - the partial slash command is immediately followed by end of input
       
       Examples:
       - "/" => matched
       - "/le" => matched
       - "/learn" => matched
       - "/learn " (note the space) => not matched
       - "what does /help do?" => not matched
     */
    public regex: string = "/^\/\w*$/";
    
    // used to cache list of slash commands
    private _slash_commands?: ChatCompletion[];
    
    async function initialize(): void {
        commands: any[] = await AiService.listSlashCommands()
        // process list of slash commands into list of potential completions
        // cache this under this._slash_commands
        this._slash_commands = ...
    }
    
    async function getCompletions(match: str) {
        // return completions by filtering this list
        // (no network call needed!)
        return this._slash_commands.filter(
            cmd => cmd.value.startsWith(match)
        )
    }
    
}

Step 2: Create a new completers registry

For Jupyter Chat to have awareness of completers in other extensions, the consumer extension must register each of its chat complters to a ChatCompletersRegistry object on init. This registry is a simple class which will provide the following methods:

  • add_completer(completer: IChatCompleter): void: adds a completer to its memory. A completer is said to be registered after this method is called on it.
  • get_completers(): IChatCompleter[]: returns a list of all registered completers.
  • init_completers(): void: calls await initialize() on all registered completers.

To provide access to this ChatCompletersRegistry object, Jupyter Chat will define a plugin which provides a IChatCompletersRegistry token. When consumer extensions require this token in their frontend plugins, they receive a reference to the ChatCompletersRegistry singleton initialized by Jupyter Chat, allowing them to register their completers. This system of providing & consuming tokens to build modular applications is common to all of JupyterLab's frontend.

Jupyter Chat already defines a IAutocompletionRegistry using a similar approach, used by Jupyter AI to provide completion for / commands. Because an implementation reference is already available, we will not go into detail here. It is sufficient to know that at this point, we have a way of allowing consumer extensions to define multiple completers and provide them to Jupyter Chat for use.

Step 3: Integrate new chat completions API

From the example SlashCommandCompleter implementation in Step 1, we can piece together how the application should behave:

  1. On init, each consumer extension instantiates its completers and adds them to the ChatCompletersRegistry singleton, provided by Jupyter Chat.

  2. Jupyter Chat should call ChatCompletersRegistry.init_completers() in the background.

  3. Perform the following on input changes:

    • Take the substring ending in the user's cursor, and store this as a local variable, e.g. partial_input.

    • For each completer, test partial_input against the completer's regex. If a match m is found, call getCompletions(m). Store a reference to this Promise.

    • Add a callback to the Promise to append the new completions to the existing list of completions.

    • If a completion is accepted, replace the substring of the input matched by the completer's regex with the completion.

    • If a user ignores completions and continues typing, cancel all Promises and return to 3).

The frontend implementation may debounce how frequently it tests the input against each regex, as testing an input against multiple regexes may be expensive. However, I think it is important we test the performance as-is first before making an optimization, since debouncing any callback adds a fixed amount of latency (the debounce delay).

Conclusion

The IChatCompleter interface defined in Step 1 and the ChatCompletersRegistry defined in Step 2 give consumer extensions a way of defining and providing chat completers. This interface and registry together define the Chat Completions API. Step 3 of this document provides guidance on how to use the new chat completions API to provide better completions in Jupyter Chat.

Benefits & applications

  • Because completers live in the frontend, they may not need to make a network call when triggered by the input. Some completers may allow completions to be statically defined (e.g. emoji names) and others may only need to make a network call at init (e.g. slash commands).

  • Because completers live in the frontend, it can choose to use any API to communicate with the server. If a Python-only API is required, a custom server handler can be defined to provide the same capabilities to the completer.

  • Completers are uniquely identified by their id, so two completers can use the same regex but yield two different sets of completions.

    • Application: Another extension could use the same / command regex to provide completions for its own custom / commands.

    • Application: @ can trigger multiple completers; one may provide usernames of other users in the chat, and another may provide the @ commands available in Jupyter AI (e.g. @file).

  • A completion doesn't need to share a prefix with the substring that triggered completions.

    • Application: Define a completer that matches $ and returns the completion \\$. Pressing "Enter" to accept the completion allows a user to easily type a literal dollar sign instead of opening math mode. If typing math was the user's intention, typing any character other than "Enter" hides the \\$ completion and allows math to be written.
  • Regex allows the triggering of completions to be strictly controlled. This means that "complete-able" suffixes don't need some unique identifier like / or @.

    • Application: Define a completer that matches ./ following whitespace and returns filenames for the current directory. For example, this could trigger the completions ./README.md, ./pyproject.toml, etc.

    • Application: Define a completer that matches : following whitespace and returns a list of emojis.

Shortcomings & risks

  • This design doesn't provide a clear way for a completer to open a custom UI instead of adding another completion entry.

    • Risk: If we don't address this shortcoming and this design makes it into Jupyter Chat v1, then we would likely need a major release to implement this in the future.

    • From @mlucool in Design proposal: Chat Completions API (rev. 0) #143: "I think a file completer would want a different experience than an variable one. As an example, for the @var completer, we envisioned users could click on the variable and interact with it. For example, maybe it lets the user have a preview of what will be sent or maybe it lets the user specify some parameters (e.g. you want the verbose mode of a specific variable). While these are only half-formed ideas, it's good to not restrict."

    • I agree that this could bring a lot of user benefit. At the same time, I have to be mindful of the engineering effort to implement this, as some stakeholders would like Jupyter AI v3.0.0 released by March. @mlucool Let's briefly discuss whether this is something we should do once you're back on Monday.

If a major revision of this design is needed, I will close this issue, revise the design, and open a new issue with a bumped revision number.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions