Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamped Jupyter AI #1

Closed
wants to merge 1 commit into from
Closed

Conversation

govinda18
Copy link

@govinda18 govinda18 commented Dec 17, 2024

Revamped Jupyter AI

Hi team,

We at D.E. Shaw have forked Jupyter AI and made a significant number of changes to greatly enhance the user experience. Our goal was to align Jupyter AI with the capabilities offered by leading AI-based IDEs such as Cursor, Copilot etc. This pull request includes the majority of the changes we implemented.

This proposal is intended to outline these changes and may require some deliberation. We anticipate that this PR will need to be broken down into several smaller PRs, with some design changes to ensure smooth integration. Additionally, we are planning further enhancements, including per notebook chat and inline code generation, details of which are included in this proposal.

The demo showcases the powerful capabilities of Jupyter AI by incorporating notebook context, leveraging the kernel, and enhancing keyboard accessibility (I did not use my mouse at all while creating this).

jai_demo.mp4

Point of contact: @govinda18 @mlucool

Philosophy

Unlike in JupyterAI, IDEs like Cursor or Copilot have gone towards anything in the IDE is fair to be used. We believe this is the correct user experience for sharing context with the LLM. For Jupyter AI to be effective, it should always provide context automatically, further allowing quick code iterations through insertions and replacements.

We also want to leverage the one major difference that sets notebooks and JupyterLab apart from any other IDE - a runtime kernel. This opens up possibilities where you can include any context of any variable declared in your notebook and further inspect methods and objects to understand their usage. Checkout jupyter/enhancement-proposals#128 for our pre-proposal on streamlining the same.

For autocompletion to be helpful, it needs to be aware of the context in previous cells to provide a Copilot-like experience. For both chat and inline completion, we automatically let it send the entire context of the notebook. We further optimize for context window of the LLM. Checkout the current limitations section for more details.

Lastly, we felt that the chat interface feels a little distant and difficult to access. Most power users of JupyterLab would not use a mouse while working with a notebook, but it's currently not easy to use Jupyter AI without mouse intervention.

Overall, we want to bring the chat and the notebook close enough such that the LLM intelligently understands what the user is talking about without the need to manually decide what context needs to be added. This should further integrate into the natural workflow of the user by providing inline code generation and keyboard accessibility.

What has changed?

This proposal aims to make Jupyter AI much more powerful and accessible, bringing a host of new features designed to enhance user productivity and streamline their data science workflows.

Major Features

  • Inline Code Completion: The Jupyter AI inline completer is now context-aware, utilizing code from both preceding and succeeding cells to provide more accurate suggestions.

    Currently, changes are sent from the frontend (reference), but we believe that jupyter_ydoc should handle this more effectively. We encountered some issues related to jupyter-collaboration issue #202 and did not want to wait for it.

  • Notebook Aware Chat: The chat model is now aware of your active notebook, current cell, and text selection. You can directly ask it to perform actions like "Refactor this code" and it will refactor your notebook or active cell accordingly, saving you valuable time. This was also proposed in Add all notebook to context,  jupyterlab/jupyter-ai#1037.

    Again, while currently this is being sent from the frontend, we believe that jupyter_ydoc should handle this more effectively. Refer this prompt to understand more.

    Further, we have added the ability to modify prompts directly from the Jupyter AI settings, enabling quicker iterations. Users can also view the exact prompt sent to the LLM by using the View Prompt call-to-action (CTA) in the chat interface.

  • Keyboard Accessibility: Navigate Jupyter AI with ease using keyboard shortcuts. Use Ctrl + Shift + K to enter chat mode (this is inline with the eventual Ctrl + K for inline code generation) and Escape to exit(ref). You can easily add generated code to your notebook by navigating with Shift + Tab.

    For enhanced user experience, we have

    • Changed the order of the cell toolbar to be Copy, Replace, Add before, Add after as we anticipate most people would want to add the code after the active cell, so it's just a Shift Tab away. (ref)
    • Goes back to the notebook directly after any action which would be the typical next step. (ref)
  • Variable Insertion: Make the Large Language Model (LLM) aware of your variables effortlessly by inserting them into the chat using the @ symbol. This helps the model better understands your context and provides more accurate suggestions by asking the kernel for runtime information. We felt this should work directly without using a prefix like @var:var_name as it is one of the most powerful benefits of having a live kernel.

    This functionality is driven by a variable description registry that knows how to describe any object to an LLM. Additionally, we are working on drafting a proposal to IPython for a generic __llm__ method that can be used by any Python class to describe itself to an LLM. More details in Pre-proposal: standardize object representations for ai and a protocol to retrieve them jupyter/enhancement-proposals#128.

    This also enhances user experience by providing an autocomplete of the variables declared in the currently active notebook. (ref)

    The current implementation is also pluggable as documented here.

Dropped Features

We suppressed some of the existing features as well. There are different reasons though but we feel each of these features needs to be given a bit more thought before we can allow them generically.

Note that these features still work but are not shown in the autocomplete (ref) or help message (ref).

  • Support for @file: We believe that adding an entire file in a context needs to be given a bit more thought. Most users would want to add the current notebook in the context which we now do by default. For adding different types of files, there should be some pre-processing. For example, adding a python file would be siginficiantly different that how a csv should be added.
  • Support for /learn and /ask: There are some fundamental issues with the current implementation:
    • The embedding generation occurs in the main thread (last we checked, not sure if it has changed now) thereby blocking the kernel. We often found users shooting themselves in the foot while trying to make the LLM learn a big folder.
    • Embedding generation of notebooks would not work great as semantic search on code fails more often than not. A better approach may be to use an LLM based text description of code in case of notebooks.
    • /learn and /ask don't scale to a large number of files as accuracy greatly decreases as you add more embeddings. This is just a problem with semantic search in general. Things like hybrid search, reranking and metadata filtering should be researched and added. We found for our internal data that it was too easy to add too much for it to be accurate (adding smaller amount of data was useful but not very practical).
  • /fix should be replaced with an eventual Fix with AI button that we plan to have whenever an error occurs in the notebook.

Enhancements

  • Support for resetting the jupyter AI config from the UI (ref)
  • Support for settings the default completion model from the config (ref)
  • Change the delete icon for human message to be a menu similar to AI message for scalability with support to copy the prompt sent by the user (ref)
  • Updated the default prompts for chat input (ref)

Current Limitations

  • To avoid exceeding the context window when including the entire notebook, we have introduced an abstraction called process_notebook_for_context. Individual providers can implement a method like the one below to optimize the context window:

    View Code
      def process_notebook_for_context(model_id: str, code_cells: list[str], active_cell: int | None) -> str:
      """
      Processes the notebook to prepare context-aware code for LLM-based completion.
      
      This method respects the token limit for the notebook context by strategically selecting 
      code from surrounding cells (both prefix and suffix) to ensure the current active cell 
      has the most relevant context.
      
      Steps:
      1. The current active cell is taken entirely as the initial context.
      2. Tokens are allocated for suffix cells and added until the token limit is reached.
      3. Remaining tokens are allocated to prefix cells and added similarly.
      4. Any remaining tokens are used to extend the suffix further if the prefix is fully utilized.
      5. Comments are added to indicate the number of cells hidden above and below the context.
      
      Parameters:
      - model_id (str): The identifier for the LLM model being used.
      - code_cells (list[str]): The list of code cells in the notebook.
      - active_cell (int | None): The index of the currently active cell in the notebook. 
                                  Defaults to 0 if not provided.
      
      Returns:
      - str: The context-aware code string, including the active cell, prefix, and suffix 
             code cells, along with comments indicating hidden cells.
      """
      active_cell = active_cell or 0
      code_context = code_cells[active_cell]
      prefix_idx = active_cell - 1
      suffix_idx = active_cell + 1
    
      total_tokens = get_max_token(model_id) * MAX_NOTEBOOK_TOKENS_PCT
    
      model_for_counting = model_id
      rem_tokens = total_tokens - get_token_count_by_model(
          code_context, model_for_counting
      )
    
      max_suffix_tokens = int(total_tokens * PREFIX_SUFFIX_RATIO)
      suffix_code: list[str] = []
      while suffix_idx < len(code_cells):
          token_to_be_used = get_token_count_by_model(
              code_cells[suffix_idx], model_for_counting
          )
          if max_suffix_tokens - token_to_be_used < 0:
              break
    
          max_suffix_tokens -= token_to_be_used
          suffix_code.append(code_cells[suffix_idx])
          suffix_idx += 1
    
      rem_tokens += max_suffix_tokens
    
      prefix_code: list[str] = []
      while prefix_idx > 0:
          token_to_be_used = get_token_count_by_model(
              code_cells[prefix_idx], model_for_counting
          )
          if rem_tokens - token_to_be_used < 0:
              break
    
          rem_tokens -= token_to_be_used
          prefix_code.append(code_cells[prefix_idx])
          prefix_idx -= 1
    
      # If any tokens are remaining, add them to the suffix
      while rem_tokens > 0 and suffix_idx < len(code_cells):
          token_to_be_used = get_token_count_by_model(
              code_cells[suffix_idx], model_for_counting
          )
          if rem_tokens - token_to_be_used < 0:
              break
    
          rem_tokens -= token_to_be_used
          suffix_code.append(code_cells[suffix_idx])
          suffix_idx += 1
    
      if prefix_idx != -1:
          prefix_code.append(
              f"# Hiding {prefix_idx + 1} more cells above the context provided."
          )
    
      if suffix_idx != len(code_cells):
          suffix_code.append(
              f"# Hiding {len(code_cells) - suffix_idx} more cells below the context provided."
          )
    
      return "\n\n".join(prefix_code[::-1] + [code_context] + suffix_code)
    
  • This PR has only been tested for the models that stream but it should be easy to extend support for those who do not as well.

  • Variable Context is limited to basic data types and pandas only. While we default to __str__ for any object, we further aim to expand support for these.

Other ideas we are working on

  • Inline Code Generation: Inspired by Cursor, we are developing an inline code generation feature in Jupyter AI. Although still in development, here is a GIF demonstrating our vision for this feature (note that some major UI changes are still pending):
    jai-2

  • Per Notebook Chat: Currently, the chat is shared across notebooks, which creates a poor user experience as chat history becomes irrelevant when switching notebooks. We are working on adding support for maintaining a per-notebook chat to address this issue.

  • Fix with AI Button: Each error in JupyterLab will have a "Fix with AI" button, which essentially implements an inline version of the /fix command, making it easier for users to resolve errors directly within their workflow.

@echarles
Copy link

This looks awesome. and can be aligned with the work we are doing in https://github.com/datalayer/jupyter-ai-agent which is an external AI agent which can be requested on demand, or listens to the nbmodel (via RTC). The Agent then can mutate the notebook directly. We are working on Inputters, Outputters and Controllers to ensure the user is correctly informed on what has be done, and that the AI remain a "responsible" agent.

@govinda18
Copy link
Author

Thanks @echarles

This was meant for internal review, please state any feedback on jupyterlab#1157

@govinda18 govinda18 closed this Dec 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants