<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Jupyter Blog - AI</title><link href="https://jasongrout.github.io/medium-archive/pelican/" rel="alternate"/><link href="https://jasongrout.github.io/medium-archive/pelican/feeds/tag-ai.atom.xml" rel="self"/><id>https://jasongrout.github.io/medium-archive/pelican/</id><updated>2026-05-28T18:07:00+00:00</updated><subtitle>The Project Jupyter blog: news, releases, and community stories, archived from blog.jupyter.org.</subtitle><entry><title>A User’s Journey with Plugin Playground: From First Idea to Installable JupyterLab Extension.</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2026/a-users-journey-with-plugin-playground-from-first-idea/" rel="alternate"/><published>2026-05-28T17:45:00+00:00</published><updated>2026-05-28T18:07:00+00:00</updated><author><name>Anuj Singh</name></author><id>tag:jasongrout.github.io,2026-05-28:/medium-archive/pelican/posts/2026/a-users-journey-with-plugin-playground-from-first-idea/</id><summary type="html">&lt;p&gt;We are excited to announce the 1.0 release of JupyterLab Plugin Playground, allowing you to seamlessly experiment with the creation of…&lt;/p&gt;
</summary><content type="html">&lt;p&gt;We are excited to announce the 1.0 release of JupyterLab Plugin Playground, allowing you to seamlessly experiment with the creation of Jupyter Notebook and JupyterLab plugins to add any functionality you may desire. To install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pip install jupyterlab-plugin-playground
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;or use it from Binder or JupyterLite without installing anything by clicking one of these links: &lt;a href="https://mybinder.org/v2/gh/jupyterlab/plugin-playground/main?urlpath=lab"&gt;Binder (Lab)&lt;/a&gt;, &lt;a href="https://mybinder.org/v2/gh/jupyterlab/plugin-playground/main?urlpath=tree"&gt;Binder (Notebook v7)&lt;/a&gt;, &lt;a href="https://jupyterlab-plugin-playground.readthedocs.io/en/latest/lite/lab/"&gt;JupyterLite (Lab)&lt;/a&gt;, &lt;a href="https://jupyterlab-plugin-playground.readthedocs.io/en/latest/lite/tree/"&gt;JupyterLite (Notebook v7)&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="motivation"&gt;&lt;strong&gt;Motivation&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Building a JupyterLab extension has traditionally required setting up a complex local environment: installing dependencies from disjoint ecosystems (Node.js and Python), configuring tooling, and hunting down the right documentation. The goal of the Playground is to eliminate that setup overhead and streamline extension development. By bringing the runtime, documentation, and the context required for extension development into a single browser tab, you can go from idea to working extension without any local setup required.&lt;/p&gt;
&lt;h2 id="journey-at-a-glance"&gt;&lt;strong&gt;Journey At a Glance&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Start with a tiny extension idea that is easy to verify visually.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Choose a build path: &lt;strong&gt;Manual&lt;/strong&gt; or &lt;strong&gt;AI-assisted&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Load&lt;/strong&gt; and iterate inside JupyterLab until behavior is stable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Build a second extension to prove the workflow is repeatable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Share plugin files/packages for product and engineering review.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Export as a &lt;strong&gt;wheel(.whl)&lt;/strong&gt; and validate installation in a clean Binder runtime.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Export as a &lt;strong&gt;zip&lt;/strong&gt; and move the scaffold into a normal GitHub repository.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="10-pick-your-starting-path"&gt;&lt;strong&gt;1.0: Pick Your Starting Path&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;At the beginning, choose the path that matches your confidence level and speed needs. If this is your first time with Plugin Playground, you can optionally run &lt;code&gt;Take the Tour&lt;/code&gt; from the Launcher or Command Palette for quick orientation before starting.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Start from File&lt;/code&gt; If you already know the APIs you need and want direct control.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Build with AI&lt;/code&gt; If you know the intended behavior but want a first draft quickly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both paths converge to the same shipping workflow later.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2026/a-users-journey-with-plugin-playground-from-first-idea/images/001-1_ejDftHO8TiktbPMim-EVqA.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="20-build-extension-1-manually"&gt;&lt;strong&gt;2.0: Build Extension #1 Manually&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;For the first pass, we will create something small and observable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our Goal&lt;/strong&gt;: add a command named &lt;code&gt;Toggle Right Sidebar&lt;/code&gt; to the command palette.&lt;/p&gt;
&lt;h3 id="21-create-the-plugin-file"&gt;&lt;strong&gt;2.1: Create the plugin file&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Launch &lt;strong&gt;Start from File&lt;/strong&gt;. This comes with a dummy template.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;JupyterFrontEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;JupyterFrontEndPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;@jupyterlab/application&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;JupyterFrontEndPlugin&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="ow"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;hello-world:plugin&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;autoStart&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;activate&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;JupyterFrontEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="22-fill-the-missing-pieces-using-the-right-sidebar"&gt;&lt;strong&gt;2.2: Fill the missing pieces using the right sidebar&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Now build the plugin in order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In &lt;code&gt;Tokens&lt;/code&gt;section in the right sidebar, search &lt;code&gt;ICommandPalette&lt;/code&gt; and click insert ( + icon ), so Plugin Playground adds token import and dependency wiring.&lt;/li&gt;
&lt;li&gt;Add a simple command skeleton inside the &lt;strong&gt;activate&lt;/strong&gt; function:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;commandID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;my-first-playground-plugin:toggle-sidebar&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commandID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Toggle Right Sidebar&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;// place cursor here&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;In &lt;code&gt;Commands&lt;/code&gt;, search &lt;code&gt;application:toggle-right-area&lt;/code&gt;, place your cursor inside &lt;code&gt;execute&lt;/code&gt; of the added command ( see above ) and use &lt;code&gt;Insert in selection&lt;/code&gt; This adds at the cursor position:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;application:toggle-right-area&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Register the command in the palette:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;commandPalette&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;commandID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;AAA&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;After those edits, your file should look like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ICommandPalette&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;@jupyterlab/apputils&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;JupyterFrontEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;JupyterFrontEndPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;@jupyterlab/application&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;JupyterFrontEndPlugin&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="ow"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;hello-world:plugin&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;autoStart&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;requires&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ICommandPalette&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;activate&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;JupyterFrontEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;commandPalette&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;ICommandPalette&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;commandID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;my-first-playground-plugin:toggle-sidebar&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commandID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Toggle Right Sidebar&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;application:toggle-right-area&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;commandPalette&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;commandID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;AAA&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Uses the typed plugin template (&lt;code&gt;JupyterFrontEndPlugin&amp;lt;void&amp;gt;&lt;/code&gt;) and typed &lt;code&gt;activate&lt;/code&gt; parameters. Imports &lt;code&gt;ICommandPalette&lt;/code&gt; so the plugin can add entries to the Command Palette.&lt;/li&gt;
&lt;li&gt;Registers a new command (&lt;code&gt;my-first-playground-plugin:toggle-sidebar&lt;/code&gt;) and executes &lt;code&gt;application:toggle-right-area&lt;/code&gt; when your command runs.&lt;/li&gt;
&lt;li&gt;Adds your command to the palette under the category &lt;code&gt;AAA&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="23-load-it-and-verify-behavior"&gt;&lt;strong&gt;2.3: Load it and verify behavior&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Click &lt;strong&gt;Load Current File As Extension or click the run button in the toolbar&lt;/strong&gt;, and open Command Palette.&lt;/p&gt;
&lt;p&gt;When you run &lt;strong&gt;Load Current File As Extension&lt;/strong&gt;, Plugin Playground compiles the active file and registers the plugin object into the live JupyterLab session. During iteration, if a plugin with the same &lt;strong&gt;id&lt;/strong&gt; already exists, Plugin Playground attempts to deactivate and replace it so reload loops stay fast.&lt;/p&gt;
&lt;h3 id="expected-result"&gt;&lt;strong&gt;Expected result&lt;/strong&gt;:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Toggle Right Sidebar&lt;/strong&gt; appears in the command palette ( open command palette by going through &lt;strong&gt;view -&amp;gt; Activate command palette&lt;/strong&gt; or Press &lt;strong&gt;Accel&lt;/strong&gt; + &lt;strong&gt;shift&lt;/strong&gt; + &lt;strong&gt;C&lt;/strong&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2026/a-users-journey-with-plugin-playground-from-first-idea/images/002-1_RXMnfrUXeu1q7AkukZEF_A.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Running it hides/shows the JupyterLab right sidebar.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="30-build-with-ai"&gt;&lt;strong&gt;3.0: Build with AI&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;If you want a faster draft, use AI assistance for the same extension workflow.&lt;/p&gt;
&lt;p&gt;Goal: add the &lt;strong&gt;Show Active Notebook Cell Count&lt;/strong&gt; command with a friendly no-notebook fallback.&lt;/p&gt;
&lt;h3 id="31-give-ai-a-precise-request"&gt;&lt;strong&gt;3.1: Give AI a precise request&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Use &lt;strong&gt;Build with AI&lt;/strong&gt; and give a prompt:&lt;/p&gt;
&lt;p&gt;for example, something like:-&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Create a JupyterLab plugin named &amp;quot;active-notebook-cell-counter&amp;quot;.
Add a command &amp;quot;Show Active Notebook Cell Count&amp;quot;.
If no notebook is active, show a friendly dialog message.
If a notebook is active, show the current cell count in a dialog.
Register the command in the command palette under category &amp;quot;Playground Demo&amp;quot;.
Export default plugin object.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="what-this-block-does"&gt;What this block does:&lt;/h3&gt;
&lt;p&gt;This gives AI a concrete plugin goal, command name, and fallback behavior requirements.&lt;/p&gt;
&lt;h3 id="32-use-ai-deterministic-inserts-together"&gt;&lt;strong&gt;3.2: Use AI + deterministic inserts together&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;In the &lt;strong&gt;Commands&lt;/strong&gt; tab:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Insert in selection&lt;/strong&gt; for predictable boilerplate insertion.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Prompt AI to insert&lt;/strong&gt; when placement context is tricky.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;{n}&lt;/strong&gt; when you need argument shape clarity ( Inspect the command signature before insertion: it shows expected argument names/types and return details).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Use &lt;strong&gt;Insert in selection&lt;/strong&gt; for predictable snippets, and switch to AI when insertion location or code adaptation depends on the surrounding context.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2026/a-users-journey-with-plugin-playground-from-first-idea/images/003-1_HAs9F-B9l77KolI3ZZTH3g.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;h3 id="40-iterate-faster-before-handoff"&gt;&lt;strong&gt;4.0: Iterate Faster Before Handoff&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Once the extensions run, enable &lt;strong&gt;Run on save&lt;/strong&gt;(from the toolbar or from the settings) while polishing labels, command categories, and small behaviour details.&lt;/p&gt;
&lt;p&gt;With &lt;strong&gt;Run on save&lt;/strong&gt;, each file save triggers a reload loop for that plugin file, which makes label tweaks, command grouping changes, and message-copy iteration much faster than manual reload cycles. And with enabling the global setting (&lt;strong&gt;Load as extension on save&lt;/strong&gt;), this behaviour is present for all files by default.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2026/a-users-journey-with-plugin-playground-from-first-idea/images/004-1_wfxS4HhzRfU5-bbKYfPonA.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;h3 id="50-share-for-review"&gt;&lt;strong&gt;5.0: Share for Review&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Before packaging, you can ask a teammate to review behaviour or just share links for the extension for a quick look using:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Share a single file&lt;/strong&gt; when sharing is about one plugin file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Share a package&lt;/strong&gt; when your logic spans several files.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This keeps review lightweight: collaborators can inspect and discuss behaviour before you commit to repository structure, CI, and release wiring.&lt;/p&gt;
&lt;h3 id="60-export-as-wheel-and-validate-in-binder"&gt;&lt;strong&gt;6.0: Export as Wheel and Validate in Binder&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Now test as an installable artifact in a separate environment.&lt;/p&gt;
&lt;h3 id="61-export-whl"&gt;&lt;strong&gt;6.1: Export &lt;code&gt;.whl&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;From the export dropdown ( in toolbar ), choose &lt;strong&gt;Export as Python package (.whl)&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="62-install-in-binder"&gt;&lt;strong&gt;6.2: Install in Binder&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open JupyterLab on Binder.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Upload the downloaded wheel.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open a terminal and run:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;force-reinstall&lt;span class="w"&gt; &lt;/span&gt;./your_exported_package.whl
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="what-this-block-does-1"&gt;What this block does:&lt;/h3&gt;
&lt;p&gt;Installs the wheel you just exported from Plugin Playground into Binder. (Uses &lt;strong&gt;— force-reinstall&lt;/strong&gt; so Binder replaces any previously installed copy with your newest build). Then refresh JupyterLab and verify commands.&lt;/p&gt;
&lt;h3 id="70-export-as-zip-and-create-a-github-repository"&gt;&lt;strong&gt;7.0: Export as Zip and Create a GitHub Repository&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;After runtime validation, move to normal engineering workflows.&lt;/p&gt;
&lt;h3 id="71-export-zip"&gt;&lt;strong&gt;7.1: Export &lt;code&gt;.zip&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Choose &lt;code&gt;**Export as archive (.zip)**&lt;/code&gt; from the toolbar export dropdown.&lt;/p&gt;
&lt;h3 id="72-bootstrap-repository"&gt;&lt;strong&gt;7.2: Bootstrap repository&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Unzip locally, create an empty GitHub repo, then run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;init&lt;/span&gt;
&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Initial extension scaffold from Plugin Playground&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;
&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//github.com/&amp;lt;your-org-or-user&amp;gt;/my-jlab-extension.git&lt;/span&gt;
&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="what-this-block-does-2"&gt;What this block does:&lt;/h3&gt;
&lt;p&gt;Initializes a new Git repository from the exported scaffold and creates an initial commit that captures the generated extension baseline.&lt;/p&gt;
&lt;p&gt;Now your prototype is in a standard repo flow with PRs, CI, and release automation.&lt;/p&gt;
&lt;h3 id="whats-next"&gt;What’s Next&lt;/h3&gt;
&lt;p&gt;Plugin Playground is still evolving. We’re actively working on the &lt;a href="https://github.com/jupyterlab/plugin-playground/pull/225"&gt;LSP integration&lt;/a&gt; for JupyterLite to bring code intelligence autocomplete and diagnostics directly into the editor, an &lt;a href="https://github.com/jupyterlab/plugin-playground/pull/231"&gt;“Ask AI” button on log errors&lt;/a&gt; that lets you send error context directly into the AI chat for instant debugging help. In the future, we would like to explore &lt;a href="https://github.com/jupyterlab/plugin-playground/issues/139"&gt;git integration&lt;/a&gt; to snapshots every successful load, paired with a &lt;a href="https://github.com/jupyterlab/plugin-playground/issues/140"&gt;built-in diff viewer&lt;/a&gt; to compare any snapshot against your current file. If you have any suggestions or encounter any problems, please let us know by opening &lt;a href="https://github.com/jupyterlab/plugin-playground/issues"&gt;an issue on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="acknowledgements"&gt;&lt;strong&gt;Acknowledgements&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;We are grateful to the &lt;a href="https://jupyterfoundation.org/join/"&gt;Jupyter Foundation&lt;/a&gt; and its &lt;a href="https://jupyterfoundation.org/members/"&gt;members&lt;/a&gt; for sponsoring the development of Plugin Playground as part of &lt;a href="https://jupyterfoundation.org/community-funding-proposals/"&gt;Community Funding Proposals 2025&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We thank Jeremy Tuloup, Nick Bollweg, and Nicolas Brichet for providing inspiration, review, and advice across our work on the plugin and its dependencies. Finally, this work would not be possible without the &lt;a href="https://github.com/jupyterlab/plugin-playground/graphs/contributors?from=01%2F01%2F2020&amp;amp;to=18%2F12%2F2024"&gt;authors of earlier&lt;/a&gt; iterations of Plugin Playground.&lt;/p&gt;
&lt;h3 id="about-the-developers"&gt;&lt;strong&gt;About the&lt;/strong&gt; Developers&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/anuj-singh-2b2ab6206/"&gt;&lt;strong&gt;Anuj Singh&lt;/strong&gt;&lt;/a&gt; is an OSS intern at OpenTeams. Anuj implemented Plugin Playground v1.0.0 and contributed to its dependencies during his internship at &lt;a href="https://openteams.com/"&gt;OpenTeams&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/smera-goel/"&gt;&lt;strong&gt;Smera Goel&lt;/strong&gt;&lt;/a&gt; is a UI/UX designer at &lt;a href="https://quansight.com/"&gt;Quansight&lt;/a&gt;. Smera contributed to the user experience and design direction of Plugin Playground.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/michal-krassowski/"&gt;&lt;strong&gt;Michał Krassowski&lt;/strong&gt;&lt;/a&gt; is a Senior Software Engineer at &lt;a href="https://openteams.com/"&gt;OpenTeams&lt;/a&gt;. Mike provided guidance and direction for the development of the Plugin Playground.&lt;/p&gt;
&lt;h3 id="further-reading"&gt;Further reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Blog post on &lt;a href="https://openteams.com/plugin-playground-ai-integration/"&gt;how and why we integrated AI functions in the playground&lt;/a&gt; extension&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jupyterlab/plugin-playground/blob/main/CHANGELOG.md"&gt;Changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jupyterlab/plugin-playground/blob/main/README.md"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content><category term="AI"/><category term="extensions"/><category term="JupyterLab"/><category term="JupyterLite"/></entry><entry><title>nb-cli: A Command-Line Interface for AI Agents and Notebook Automation</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2026/nb-cli-a-command-line-interface-for-ai-agents-and/" rel="alternate"/><published>2026-05-11T19:04:00+00:00</published><updated>2026-05-11T19:04:00+00:00</updated><author><name>Piyush Jain</name></author><id>tag:jasongrout.github.io,2026-05-11:/medium-archive/pelican/posts/2026/nb-cli-a-command-line-interface-for-ai-agents-and/</id><summary type="html">&lt;p&gt;The rise of AI coding agents has transformed how we think about developer tools. Large language models like Claude, GPT, and others are…&lt;/p&gt;
</summary><content type="html">&lt;p&gt;The rise of AI coding agents has transformed how we think about developer tools. Large language models like Claude, GPT, and others are remarkably effective at using command-line interfaces — they’ve been trained on billions of lines of CLI usage from documentation, Stack Overflow, and GitHub. But when it comes to working with Jupyter notebooks programmatically, there’s been a gap: existing tools focus on running agents within notebooks, but what about agents that need to work with notebooks as artifacts?&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/jupyter-ai-contrib/nb-cli"&gt;&lt;strong&gt;nb-cli&lt;/strong&gt;&lt;/a&gt;, an experimental open-source command-line interface designed specifically for AI agents, automation scripts, and developers who need programmatic access to Jupyter notebooks. Built with Rust for performance and reliability, nb-cli provides a fast, composable way to read, write, execute, and manipulate notebooks through a clean command line interface that follows the &lt;a href="https://nbformat.readthedocs.io/en/latest/"&gt;nbformat&lt;/a&gt; specification.&lt;/p&gt;
&lt;h2 id="the-problem-notebooks-as-black-boxes"&gt;&lt;strong&gt;The Problem: Notebooks as Black Boxes&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;While Jupyter notebooks are indispensable for interactive exploration, their underlying &lt;code&gt;.ipynb&lt;/code&gt; JSON structure has long been a friction point for programmatic interaction, especially for shell scripts and Large Language Models (LLMs).&lt;/p&gt;
&lt;p&gt;Traditional workflows often break down when automation or AI-driven analysis is required. Consider the following scenarios where standard notebook interfaces prove insufficient:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Autonomous Analysis&lt;/strong&gt;: An AI agent tasked with auditing a data science workflow must programmatically inspect individual cells to map out the analysis pipeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated Validation&lt;/strong&gt;: CI/CD systems require a reliable method to execute notebooks, validate outputs, and catch errors before deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation at Scale&lt;/strong&gt;: Developers need tools to automatically transform notebook content into clean, accessible documentation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production Debugging&lt;/strong&gt;: Teams need a way to troubleshoot notebook execution failures in headless production environments without manual intervention.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notebooks as Data&lt;/strong&gt;: Analysts may want to treat a notebook as a structured database to programmatically generate business reports, research summaries, or custom visualizations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Historically, solving these problems required labor-intensive workarounds like manually navigating the JupyterLab UI, writing brittle Python scripts to parse complex JSON files, or using execution tools that lack real-time integration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;nb-cli&lt;/strong&gt; bridges this gap by offering a CLI-first interface designed for the modern era of automation. By leveraging command-line patterns and Unix composability, it provides the structured output and predictable interface that AI agents and developers need to treat notebooks as first-class citizens in any software stack.&lt;/p&gt;
&lt;h3 id="key-features"&gt;Key Features&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Works With or Without a Jupyter Server&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;nb-cli doesn’t require a running Jupyter server. By default, it reads and writes &lt;code&gt;.ipynb&lt;/code&gt; files directly and communicates with kernels over ZeroMQ for execution. This makes it well-suited for scripting, CI pipelines, and any workflow where launching a server is unnecessary overhead.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a notebook — no server needed&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb

&lt;span class="c1"&gt;# Add cells&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;import pandas as pd&amp;quot;&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;# Data Analysis&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--type&lt;span class="w"&gt; &lt;/span&gt;markdown

&lt;span class="c1"&gt;# Execute&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb

&lt;span class="c1"&gt;# Read back with outputs&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Connecting to a server becomes valuable if multiple users and/or agents are editing the same notebook simultaneously within a JupyterLab session. Once connected, nb-cli uses Y.js, the same CRDT protocol JupyterLab uses internally for conflict-free real-time synchronization.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Auto-detect and connect to local Jupyter server&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;connect

&lt;span class="c1"&gt;# Or, connect to a specific server&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;connect&lt;span class="w"&gt; &lt;/span&gt;--server&lt;span class="w"&gt; &lt;/span&gt;http://localhost:9999&lt;span class="w"&gt; &lt;/span&gt;--token&lt;span class="w"&gt; &lt;/span&gt;abc

&lt;span class="c1"&gt;# Add a cell - it appears instantly in JupyterLab&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;experiment.ipynb&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;df.head()&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Execute via the remote kernel&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;experiment.ipynb&lt;span class="w"&gt; &lt;/span&gt;--cell&lt;span class="w"&gt; &lt;/span&gt;fe456

&lt;span class="c1"&gt;# Restart the kernel before execution for reproducibility checks&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;experiment.ipynb&lt;span class="w"&gt; &lt;/span&gt;--restart-kernel

&lt;span class="c1"&gt;# Disconnect&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;disconnect
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When connected to a Jupyter server, &lt;strong&gt;nb-cli&lt;/strong&gt; detects whether a notebook is open in JupyterLab and uses server APIs for conflict-free collaborative editing. If the notebook isn’t open, it seamlessly falls back to file-based operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI-Optimized Markdown Format&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Language models don’t parse JSON, they predict tokens. This distinction matters more than you’d think when you’re building a tool that feeds notebook content into an LLM’s context window. Jupyter’s native notebook format is deeply nested JSON. Source code is stored as arrays of strings. Outputs carry base64-encoded blobs. Metadata nests several levels deep. This is fine for a JSON parser, but for a language model working within a fixed context window, 30–40% of those tokens are structural characters — braces, brackets, escaped newlines that carry no semantic value. Another common option is plain Markdown which is token-efficient and human-readable, but it’s ambiguous. A # could be a markdown heading or a Python comment. A fenced code block could be a notebook cell or an example inside a markdown cell’s documentation. When an LLM is asked to “fix the error in cell 7,” it needs to reliably locate that cell in the text and plain markdown gives it no structural markers to count on, just a sequence of code fences that all look the same.&lt;/p&gt;
&lt;p&gt;So we designed a line-oriented sentinel format:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;@@&lt;/span&gt;&lt;span class="kc"&gt;n&lt;/span&gt;&lt;span class="err"&gt;o&lt;/span&gt;&lt;span class="kc"&gt;te&lt;/span&gt;&lt;span class="err"&gt;book&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;format&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ai-notebook&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;metadata&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;kernelspec&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;python3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;

&lt;span class="err"&gt;@@cell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;index&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;f68t57&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;cell_type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;code&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;execution_count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;```py&lt;/span&gt;&lt;span class="kc"&gt;t&lt;/span&gt;&lt;span class="err"&gt;ho&lt;/span&gt;&lt;span class="kc"&gt;n&lt;/span&gt;
&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="kc"&gt;f&lt;/span&gt;&lt;span class="err"&gt;.head()&lt;/span&gt;
&lt;span class="err"&gt;```&lt;/span&gt;
&lt;span class="err"&gt;@@ou&lt;/span&gt;&lt;span class="kc"&gt;t&lt;/span&gt;&lt;span class="err"&gt;pu&lt;/span&gt;&lt;span class="kc"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;output_type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;execute_result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;```&lt;/span&gt;&lt;span class="kc"&gt;te&lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="kc"&gt;t&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;col_a&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;col_b&lt;/span&gt;
&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;
&lt;span class="err"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The format makes a few deliberate tradeoffs. &lt;strong&gt;@@cell&lt;/strong&gt; and &lt;strong&gt;@@output&lt;/strong&gt; sentinels give the model unambiguous structural boundaries without counting braces or tracking nesting. Inline JSON metadata on each sentinel line places cell type, index, and execution count in the tokens immediately before the content — matching how attention mechanisms locate information. Code in fenced blocks with language hints activates the model’s syntax-level training. And because each cell block is self-contained, truncation degrades gracefully — unlike JSON, where a cut anywhere breaks the entire structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Designed for Composability&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;nb-cli follows Unix conventions — plain text output, stdin support, predictable exit codes — so it composes naturally with other CLI tools. For AI agents, this matters because a single shell command can replace what would otherwise be multiple tool calls with intermediate parsing.&lt;/p&gt;
&lt;p&gt;Consider an agent asked to “add a summary section to the notebook and run it.” Without nb-cli, this requires separate API calls to read the notebook, parse the structure, insert a cell, write the file, find a kernel, execute, read back the output — each consuming tokens for the request and response. With nb-cli:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;                                                                         &lt;/span&gt;
&lt;span class="s"&gt;@@markdown                                                                                                                 &lt;/span&gt;
&lt;span class="s"&gt;# Summary                                                                                                                  &lt;/span&gt;
&lt;span class="s"&gt;                                                                                                                            &lt;/span&gt;
&lt;span class="s"&gt;@@code                                                                                                                     &lt;/span&gt;
&lt;span class="s"&gt;print(f&amp;quot;Rows: {len(df)}, Columns: {len(df.columns)}&amp;quot;)                                                                      &lt;/span&gt;
&lt;span class="s"&gt;df.describe()                                                                                                              &lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;&lt;span class="w"&gt;                                                                                                                        &lt;/span&gt;
&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;-2&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;-1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;-1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Three operations — add cells, execute them, read the result — in a single shell invocation. The agent gets back only the output it needs without re-reading the entire notebook.&lt;/p&gt;
&lt;p&gt;The same principle applies to debugging. An agent investigating a failed notebook doesn’t need to read every cell to find the problem.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Find cells with errors — returns only the relevant cells                                                                 &lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--with-errors
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One call, targeted output, no wasted tokens on cells that ran successfully.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stable Cell Referencing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;nb-cli supports two ways to reference cells.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Index-based&lt;/strong&gt;:&lt;code&gt;--cell-index 0&lt;/code&gt; (supports negative indexing: -1 = last cell)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ID-based&lt;/strong&gt;: &lt;code&gt;--cell f68t57&lt;/code&gt; (doesn’t change when cells move)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Reference by position&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--cell-index&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;x = 42&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Reference by stable ID — safe even after cells are reordered&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--cell&lt;span class="w"&gt; &lt;/span&gt;ce456&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;print(&amp;#39;Done&amp;#39;)&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Execute the last cell&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--cell-index&lt;span class="w"&gt; &lt;/span&gt;-1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Powerful Search Capabilities&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;nb-cli includes built-in search to quickly locate cells by content, type, or execution errors. By default, search matches against cell source code, but a scope filter extends it to execution outputs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Search for cells containing a pattern  &lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;import pandas&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                      &lt;/span&gt;
&lt;span class="c1"&gt;# Find all cells with execution errors&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--with-errors&lt;span class="w"&gt;                                                                                                     &lt;/span&gt;

&lt;span class="c1"&gt;# Search within outputs instead of source                                                                                       &lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;KeyError&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--scope&lt;span class="w"&gt; &lt;/span&gt;output

&lt;span class="c1"&gt;# Filter by cell type&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;TODO&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--cell-type&lt;span class="w"&gt; &lt;/span&gt;mardown
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For AI agents, — with-errors is particularly useful — instead of reading an entire notebook to find what failed, the agent gets back only the cells that need attention. Combined with — scope output, it can search error tracebacks directly without parsing every cell’s results. The same capabilities are useful for humans auditing notebooks for deprecated APIs, locating specific functions across large notebooks, or extracting patterns before a refactor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-Cell Operations&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One of the most common patterns when working with notebooks programmatically is adding a sequence of cells — a markdown header, then setup code, then analysis. Doing this one cell at a time means multiple round-trips and index bookkeeping. Instead, nb-cli accepts multiple cells in a single call using the sentinels format we saw earlier.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add a markdown header followed by a code cell in one command&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;report.ipynb&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;&lt;/span&gt;
&lt;span class="s"&gt;@@markdown&lt;/span&gt;
&lt;span class="s"&gt;# Results&lt;/span&gt;

&lt;span class="s"&gt;@@code&lt;/span&gt;
&lt;span class="s"&gt;import pandas as pd&lt;/span&gt;
&lt;span class="s"&gt;df = pd.read_csv(&amp;#39;results.csv&amp;#39;)&lt;/span&gt;
&lt;span class="s"&gt;df.head()&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For more control, sentinels also accept the full @@cell {“cell_type”: “…”} JSON format.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;report.ipynb&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;                                     &lt;/span&gt;
&lt;span class="s"&gt;@@cell {&amp;quot;cell_type&amp;quot;: &amp;quot;markdown&amp;quot;}                                                     &lt;/span&gt;
&lt;span class="s"&gt;# Analysis Header                                                                     &lt;/span&gt;

&lt;span class="s"&gt;@@cell {&amp;quot;cell_type&amp;quot;: &amp;quot;code&amp;quot;}                                                         &lt;/span&gt;
&lt;span class="s"&gt;print(&amp;quot;hello&amp;quot;)&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;&lt;span class="w"&gt;                                                                &lt;/span&gt;
&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Both formats work with stdin, making it easy to compose cells from scripts or pipelines.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;@@markdown\n## Summary\n\n@@code\ndf.describe()\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;report.ipynb&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The same batching philosophy extends to execution and deletion:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Execute cells 2 through 5&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--start&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--end&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="c1"&gt;# Delete specific cells&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="c1"&gt;# Delete a range of cells&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--range&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Environment-Aware Execution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;— uv&lt;/strong&gt; and &lt;strong&gt;— pixi&lt;/strong&gt; flags are supported on &lt;strong&gt;nb connect&lt;/strong&gt;, &lt;strong&gt;nb execute&lt;/strong&gt;, and &lt;strong&gt;nb create&lt;/strong&gt;, telling nb to discover Jupyter servers and kernels through the appropriate environment manager. &lt;strong&gt;nb status — python&lt;/strong&gt; returns the command prefix needed to run Python in the same environment as the connected kernel — useful when agent-generated shell commands need to match the active notebook environment.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Connect using a uv-managed environment&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;connect&lt;span class="w"&gt; &lt;/span&gt;--uv

&lt;span class="c1"&gt;# Execute in a pixi-managed environment&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--pixi

&lt;span class="c1"&gt;# Get the Python prefix for agent-generated shell commands&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;--python
&lt;span class="c1"&gt;# Returns: &amp;quot;uv run&amp;quot;, &amp;quot;pixi run&amp;quot;, or empty for system Python&lt;/span&gt;

&lt;span class="c1"&gt;# Use in a pipeline&lt;/span&gt;
&lt;span class="k"&gt;$(&lt;/span&gt;nb&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;--python&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;import pandas; print(pandas.__version__)&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="real-world-use-cases"&gt;Real World Use Cases&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;AI Agent Workflows&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI coding agents can now manipulate notebooks as a part of their analysis workflow.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Surface all failing cells&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;data_analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--with-errors

&lt;span class="c1"&gt;# Apply the fix&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;data_analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--cell-index&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;df = pd.read_csv(&amp;#39;data.csv&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;)&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Re-execute to verify&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;data_analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;--cell-index&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;CI/CD Integration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Automated testing and validation of notebooks in continuous integration pipelines.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Executing notebook...&amp;quot;&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;pipeline.ipynb&lt;span class="w"&gt; &lt;/span&gt;--allow-errors

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Checking for errors...&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;pipeline.ipynb&lt;span class="w"&gt; &lt;/span&gt;--with-errors&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Notebook execution failed&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Clearing outputs before commit...&amp;quot;&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;output&lt;span class="w"&gt; &lt;/span&gt;clear&lt;span class="w"&gt; &lt;/span&gt;pipeline.ipynb

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;✓ All cells executed successfully&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Programmatic Notebook Generation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate documentation, reports, and analysis automatically.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a report notebook&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;report.ipynb

&lt;span class="c1"&gt;# Add title, introduction, and analysis in one multi-cell command&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;cell&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;report.ipynb&lt;span class="w"&gt; &lt;/span&gt;--source&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;&lt;/span&gt;
&lt;span class="s"&gt;@@markdown&lt;/span&gt;
&lt;span class="s"&gt;# Monthly Sales Report&lt;/span&gt;

&lt;span class="s"&gt;@@markdown&lt;/span&gt;
&lt;span class="s"&gt;Generated on $(date)&lt;/span&gt;

&lt;span class="s"&gt;@@code&lt;/span&gt;
&lt;span class="s"&gt;import pandas as pd&lt;/span&gt;
&lt;span class="s"&gt;df = pd.read_csv(&amp;#39;sales_data.csv&amp;#39;)&lt;/span&gt;
&lt;span class="s"&gt;df.describe()&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Execute to populate outputs&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;report.ipynb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Debugging Production Notebooks&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Quickly inspect and diagnose issues in deployed notebooks.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Find all cells with errors&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;failing_notebook.ipynb&lt;span class="w"&gt; &lt;/span&gt;--with-errors

&lt;span class="c1"&gt;# Search for cells with deprecated API usage&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;analysis.ipynb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pandas.np&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Find cells with potential security issues&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;notebook.ipynb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;eval(&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Examine specific failing cell with full output&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;failing_notebook.ipynb&lt;span class="w"&gt; &lt;/span&gt;--cell-index&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="c1"&gt;# Restart kernel and re-run for clean reproducibility check&lt;/span&gt;
nb&lt;span class="w"&gt; &lt;/span&gt;execute&lt;span class="w"&gt; &lt;/span&gt;failing_notebook.ipynb&lt;span class="w"&gt; &lt;/span&gt;--restart-kernel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="nb-cli-in-action"&gt;nb-cli in Action&lt;/h3&gt;
&lt;p&gt;To illustrate how AI agents use nb-cli naturally, here are some examples of agent interactions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 1&lt;/strong&gt;: Claude creating a RL for LLMs notebook&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;User Prompt&lt;/strong&gt;&lt;/em&gt;: Help me learn about reinforcement learning for LLMs by creating a notebook and explaining at each cell how it all works. Cover the key concepts: policy model, reward model, KL divergence penalty, PPO, and GRPO. Use a tiny toy model (small vocab, GRU-based) so everything runs on CPU without any API keys.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2026/nb-cli-a-command-line-interface-for-ai-agents-and/images/001-1_TGHiiVA5yiE4HBdvSAZ6vg.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 2&lt;/strong&gt;: Codex fixing multiple bugs in a notebook&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;User Prompt&lt;/strong&gt;&lt;/em&gt;: The file churn_analysis.ipynb is a broken research notebook that was last updated in 2023. Fix it so it runs cleanly end-to-end. Identify every cell that fails, fix each issue and verify the notebook executes successfully. After fixing, add a brief markdown note above each cell you changed explaining what was broken and why.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2026/nb-cli-a-command-line-interface-for-ai-agents-and/images/002-1_VrpFHv9vbJCo-SByBhAwmQ.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Codex fixed four bugs in churn_analysis.ipynb: a hardcoded file path, DataFrame.append() (removed in pandas 2.0), sklearn.cross_validation (removed in sklearn 0.20), and plot_confusion_matrix (removed in sklearn 1.2) and verified the notebook runs end-to-end after these chages.&lt;/p&gt;
&lt;h3 id="getting-started-with-nb-cli"&gt;Getting Started with nb-cli&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Installation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use the install script.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-fsSL&lt;span class="w"&gt; &lt;/span&gt;https://raw.githubusercontent.com/jupyter-ai-contrib/nb-cli/main/install.sh&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If your platform is not supported, and you get an error during install, use cargo to install.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;cargo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;nb-cli&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or build from source.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/jupyter-ai-contrib/nb-cli.git
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nb-cli
cargo&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;--release
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The binary will be available at &lt;code&gt;target/release/nb&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;To enable your AI agents to use nb for all notebook operations, install the skill.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;npx&lt;span class="w"&gt; &lt;/span&gt;skills&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;jupyter-ai-contrib/nb-cli
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="about-the-developers"&gt;About the developers&lt;/h3&gt;
&lt;figure&gt;
&lt;img alt="Andrii Ieroshenko is a Software Development Engineer at AWS. He is a long term contributor to project Jupyter working on JupyterLab, Jupyter AI and several other projects. He is also a member of the Jupyter Media Strategy Working Group." src="https://jasongrout.github.io/medium-archive/pelican/posts/2026/nb-cli-a-command-line-interface-for-ai-agents-and/images/003-0_i4frZE6ZuXt6owQy.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;&lt;a href="https://github.com/andrii-i/"&gt;Andrii Ieroshenko&lt;/a&gt; is a Software Development Engineer at AWS. He is a long term contributor to project Jupyter working on JupyterLab, Jupyter AI and several other projects. He is also a member of the Jupyter Media Strategy Working Group.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="Brian Granger is a Senior Principal Technologist at AWS. Brian is a cofounder of Project Jupyter, a board member of the Jupyter and PyTorch Foundations." src="https://jasongrout.github.io/medium-archive/pelican/posts/2026/nb-cli-a-command-line-interface-for-ai-agents-and/images/004-0_OkFJLojf_qblg8Uv.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;&lt;a href="https://github.com/ellisonbg"&gt;Brian Granger&lt;/a&gt; is a Senior Principal Technologist at AWS. Brian is a cofounder of Project Jupyter, a board member of the Jupyter and PyTorch Foundations.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="Piyush Jain is a Principal Engineer at AWS working on Jupyter and Agentic AI. He is a distinguished Jupyter contributor and a member of the Jupyter Server Council." src="https://jasongrout.github.io/medium-archive/pelican/posts/2026/nb-cli-a-command-line-interface-for-ai-agents-and/images/005-0__uwMJpQQ-GZTs1Fh.jpeg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;&lt;a href="https://github.com/3coins"&gt;Piyush Jain&lt;/a&gt; is a Principal Engineer at AWS working on Jupyter and Agentic AI. He is a distinguished Jupyter contributor and a member of the Jupyter Server Council.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id="how-can-you-help"&gt;How can you help?&lt;/h3&gt;
&lt;p&gt;We’re just getting started with &lt;strong&gt;nb-cli&lt;/strong&gt;. Please join us!.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Install and use the nb-cli.&lt;/strong&gt; If you find any bugs or have suggestions, please create issues on GitHub.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Join the discussion&lt;/strong&gt; about nb-cli, open issues or add to discussion in &lt;a href="https://github.com/orgs/jupyter-ai-contrib/discussions"&gt;jupyter-ai-contrib&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contribute:&lt;/strong&gt; Your bug reports, feature requests, and pull requests will help improve this project for everyone.&lt;/li&gt;
&lt;/ul&gt;
</content><category term="AI"/></entry><entry><title>JupyterLab 4.5 and Notebook 7.5 are available!</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-5-and-notebook-7-5-are-available/" rel="alternate"/><published>2025-11-24T09:32:00+00:00</published><updated>2025-11-24T09:32:00+00:00</updated><author><name>Jeremy Tuloup</name></author><id>tag:jasongrout.github.io,2025-11-24:/medium-archive/pelican/posts/2025/jupyterlab-4-5-and-notebook-7-5-are-available/</id><summary type="html">&lt;p&gt;JupyterLab 4.5 has been released! This new minor release of JupyterLab includes 51 new features and enhancements, 81 bug fixes, 44…&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;a href="https://github.com/jupyterlab/jupyterlab"&gt;JupyterLab&lt;/a&gt; 4.5 has been released! This new minor release of JupyterLab includes 51 new features and enhancements, 81 bug fixes, 44 maintenance tasks and 38 documentation improvements. There were 51 contributors to this release.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/jupyter/notebook"&gt;Jupyter Notebook&lt;/a&gt; 7.5 has also been released, including many of the fixes and enhancements in this JupyterLab 4.5 release. While the releases on GitHub provide additional detail on the updates made to both &lt;a href="https://github.com/jupyterlab/jupyterlab/releases/tag/v4.5.0"&gt;JupyterLab 4.5&lt;/a&gt; and &lt;a href="https://github.com/jupyter/notebook/releases/tag/v7.5.0"&gt;Notebook 7.5&lt;/a&gt;, this article highlights some particularly notable changes.&lt;/p&gt;
&lt;p&gt;Extension authors can refer to the &lt;a href="https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#jupyterlab-4-4-to-4-5"&gt;Extension Migration Guide&lt;/a&gt; for information on API updates and details regarding changes to core packages.&lt;/p&gt;
&lt;h2 id="performance-and-windowing"&gt;Performance and windowing&lt;/h2&gt;
&lt;p&gt;The default windowing mode is now &lt;code&gt;contentVisibility&lt;/code&gt; which improves cell rendering and alleviates previously reported issues. The minimap can be enabled for all windowing modes and all notebooks via the &lt;code&gt;Settings&lt;/code&gt; menu, or by toggling the minimap per notebook with the &lt;code&gt;View&lt;/code&gt; -&amp;gt; &lt;code&gt;Show Minimap&lt;/code&gt; option. Additional fixes improve layout containment and scrollbar behavior when working with long cells.&lt;/p&gt;
&lt;h2 id="notebook-improvements"&gt;Notebook improvements&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Saving large notebooks is now faster&lt;/strong&gt; and more responsive thanks to streamed uploads and users can now open notebooks without starting a kernel, via the &lt;code&gt;Open With&lt;/code&gt; context menu option.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Open a notebook without a kernel" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-5-and-notebook-7-5-are-available/images/001-0_zjjwutgF-mDuypaA.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Open a notebook without a kernel&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Markdown support has been expanded&lt;/strong&gt; for using grid and flex based layouts and the Table of Contents has been significantly improved to better handle markdown headings, fixing issues with HTML tags, comments, and horizontal lines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Copying and pasting cells across browser tabs is now supported&lt;/strong&gt; and the “Line Wrap” toggle in settings now correctly applies line wrapping to cells.&lt;/p&gt;
&lt;p&gt;Other improvements include: restored scrolling to in-notebook links, manual saving support in collaborative mode, and improved performance and memory usage when streaming text outputs.&lt;/p&gt;
&lt;h2 id="debugger-and-console-improvements"&gt;Debugger and Console improvements&lt;/h2&gt;
&lt;p&gt;The debugger’s evaluate dialog has been replaced with an interactive console with syntax highlighting and code completion, and the variables panel now preserves its contents when switching editors. Selected breakpoints are now visually highlighted and breakpoints correctly handle empty lines and duplicate clicks. A visual indicator is displayed when the debugger is paused.&lt;/p&gt;
&lt;p&gt;Debugger panels as well as console debugging panels now reference the cell execution count helping users relate breakpoints, call stacks and source code to the cells they originate from.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="JupyterLab 4.5 includes many improvements to the debugger" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-5-and-notebook-7-5-are-available/images/002-0_5p78_XdDJh5oSnci.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;JupyterLab 4.5 includes many improvements to the debugger&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;For the Log Console, users can now configure the default log level via settings, as well as customise the toolbar.&lt;/p&gt;
&lt;h2 id="new-search-functionality-inside-terminals"&gt;New search functionality inside terminals&lt;/h2&gt;
&lt;p&gt;The terminal now includes search functionality, with search highlights that adapt to theme changes.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Search is now compatible with terminals" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-5-and-notebook-7-5-are-available/images/003-0_cJt44woU3gYoMhHu.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Search is now compatible with terminals&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="media-and-content-support"&gt;Media and content support&lt;/h2&gt;
&lt;p&gt;JupyterLab now includes built-in audio and video viewers, allowing users to open audio and video files directly from within JupyterLab.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Open audio and video files in JupyterLab" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-5-and-notebook-7-5-are-available/images/004-0_Jd_3FHeg3saSiwRs.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Open audio and video files in JupyterLab&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="file-browser-enhancements"&gt;File browser enhancements&lt;/h2&gt;
&lt;p&gt;Uploaded files can auto-open, with controls for size limits and behavior, and a new &lt;code&gt;allowFileUploads&lt;/code&gt; setting provides control over upload permissions.&lt;/p&gt;
&lt;p&gt;There is now a menu option for creating new Python, R, or Julia files, and a “Select All” command has also been added, also accessible via the &lt;code&gt;Ctrl + A&lt;/code&gt; keyboard shortcut.&lt;/p&gt;
&lt;p&gt;Users can now configure file browser breadcrumbs through the settings editor options &lt;code&gt;breadcrumbsLeftItems&lt;/code&gt; and &lt;code&gt;breadcrumbsRightItems&lt;/code&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="JupyterLab 4.5 includes many improvements to the file browser" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-5-and-notebook-7-5-are-available/images/005-0_V05aZinAy9fytWaU.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;JupyterLab 4.5 includes many improvements to the file browser&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Updates to file dialogs and navigation have addressed column spacing, preferred-directory handling and double-click behavior.&lt;/p&gt;
&lt;h2 id="keyboard-shortcuts-and-commands"&gt;Keyboard shortcuts and commands&lt;/h2&gt;
&lt;p&gt;Code fold and unfold commands have been added and shortcuts for these can be set in the settings editor.&lt;/p&gt;
&lt;p&gt;The “Show Workspace Indicator” command has been added to the command palette and the “Send Code to Console” shortcut has been restored. The previous issue involving editing shortcuts on Safari loosing focus, has been resolved.&lt;/p&gt;
&lt;p&gt;With Jupyter Notebook 7.5.0, the &lt;code&gt;O&lt;/code&gt; key has been defined as the keyboard shortcut to toggle cell outputs, restoring this familiar functionality from the classic notebook.&lt;/p&gt;
&lt;h2 id="settings"&gt;Settings&lt;/h2&gt;
&lt;p&gt;The settings editor now supports a &lt;code&gt;toSkip&lt;/code&gt; configuration to hide specific plugins from view.&lt;/p&gt;
&lt;p&gt;A new setting, &lt;code&gt;addExtraLineOnCellMerge&lt;/code&gt;, allows to remove the extra line added when merging cells, which is expected behavior based on the classic notebook.&lt;/p&gt;
&lt;h2 id="user-interface-and-accessibility"&gt;User interface and accessibility&lt;/h2&gt;
&lt;p&gt;JupyterLab can now automatically switch between light and dark themes when “Synchronize with System Settings” is enabled.&lt;/p&gt;
&lt;p&gt;Dialog components display buttons and checkboxes on separate lines for better readability, and selections in the terminal under high-contrast themes have better visibility.&lt;/p&gt;
&lt;p&gt;Keyboard navigation and tab order in the status bar has also been improved.&lt;/p&gt;
&lt;h2 id="update-from-jupytercon"&gt;Update from JupyterCon&lt;/h2&gt;
&lt;p&gt;At JupyterCon 2025 in San Diego, the Jupyter Frontends team presented an update on features released since JupyterLab 4.0. This session covers the progress made since the previous conference in 2023. You can watch the full recording below.&lt;/p&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/xiPJDIbUU0o" title="Lightning Talk: What&amp;#x27;s New in Jupyter Frontends - Jeremy Tuloup, QuantStack &amp;amp; Rosio Reyes, Anaconda" width="560" height="315" style="aspect-ratio: 560 / 315" loading="lazy" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;
&lt;h2 id="acknowledgements"&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;Thank you to all the new contributors, the Jupyter Frontends team hopes to see you back!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JupyterLab: @Ankitaupadhaya, @CrafterKolyan, @EtiennePelletier, @Frank-Steiner, @Meriem-BenIsmail, @arjxn-py, @bnavigator, @bsundaram1, @dnlzrgz, @dotnetautor, @gjmooney&lt;/li&gt;
&lt;li&gt;Notebook: @honnix, @jchen1223, @sangampaudel530&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And thank you to returning contributors for their continued work!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JupyterLab: @Darshan808, @DenisaCG, @HaudinFlorence, @MUFFANUJ, @RRosio, @SylvainCorlay, @achhina, @afshin, @andrii-i, @bollwyvl, @brichet, @choldgraf, @cmarmo, @davidbrochart, @ianthomas23, @isabela-pf, @itsmevichu, @jasongrout, @jrdnbradford, @jtpio, @kathatherine, @kr-2003, @krassowski, @mahendrapaipuri, @martinRenou, @minrk, @nkn2022, @peytondmurray, @ritzdevp, @robertstrauss, @rschroll, @shreve, @skapin, @trungleduc, @yacchin1205, @yukiiii04, @zanieb&lt;/li&gt;
&lt;li&gt;Notebook: @choldgraf, @itsmevichu, @jasongrout, @jtpio, @krassowski, @minrk&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thank you to all the people who tested and provided feedback for the pre-releases! Lastly, thank you to the weekly triage group for triaging JupyterLab, Notebook and JupyterLab Desktop issues: @RRosio, @krassowski, @andrii-i, @danyeaw among others who attend.&lt;/p&gt;
</content><category term="AI"/><category term="Jupyter Notebook"/><category term="JupyterLab"/><category term="releases"/></entry><entry><title>JupyterLab 4.4 and Notebook 7.4 are available!</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-4-and-notebook-7-4-are-available/" rel="alternate"/><published>2025-05-21T09:54:00+00:00</published><updated>2025-05-21T09:54:00+00:00</updated><author><name>Jeremy Tuloup</name></author><id>tag:jasongrout.github.io,2025-05-21:/medium-archive/pelican/posts/2025/jupyterlab-4-4-and-notebook-7-4-are-available/</id><summary type="html">&lt;p&gt;JupyterLab 4.4 has been released! This new minor release of JupyterLab includes many new features and bug fixes.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;a href="https://github.com/jupyterlab/jupyterlab"&gt;JupyterLab&lt;/a&gt; 4.4 has been released! This new minor release of JupyterLab includes 28 new features and enhancements, 76 bug fixes, 63 maintenance tasks and 34 documentation improvements. There were 56 contributors to this release.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/jupyter/notebook"&gt;Jupyter Notebook&lt;/a&gt; 7.4 has also been released, including many of the fixes and enhancements in this JupyterLab 4.4 release. While the releases on GitHub provide additional detail on the updates made to both &lt;a href="https://github.com/jupyterlab/jupyterlab/releases/tag/v4.4.0"&gt;JupyterLab 4.4&lt;/a&gt; and &lt;a href="https://github.com/jupyter/notebook/releases/tag/v7.4.0"&gt;Notebook 7.4&lt;/a&gt;, this article highlights some particularly notable changes.&lt;/p&gt;
&lt;p&gt;Extension authors can refer to the &lt;a href="https://jupyterlab.readthedocs.io/en/stable/extension/extension_migration.html#jupyterlab-4-3-to-4-4"&gt;Extension Migration Guide&lt;/a&gt; for information on API updates and details regarding changes to core packages.&lt;/p&gt;
&lt;h2 id="code-console-improvements"&gt;Code console improvements&lt;/h2&gt;
&lt;p&gt;The code console prompt can now be positioned on the top, left, or right side of the console, in addition to the default bottom placement. The placement is persisted in the Prompt Cell Position setting. The console toolbar now includes buttons to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Execute code&lt;/li&gt;
&lt;li&gt;Restart the kernel&lt;/li&gt;
&lt;li&gt;Clear the cells&lt;/li&gt;
&lt;li&gt;Switch the kernel&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
&lt;img alt="Changing the position of the prompt cell in JupyterLab code consoles" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-4-and-notebook-7-4-are-available/images/001-0_xR1yXwkrzFar51Rh.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Changing the position of the prompt cell in JupyterLab code consoles&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Additional settings for console behavior have been added:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clear Code Content on Execute (enabled by default) — When disabled, the code submitted for execution remains in the code editor, allowing for further edits.&lt;/li&gt;
&lt;li&gt;Hide Code Input — When enabled, only the execution output is shown in the console.&lt;/li&gt;
&lt;li&gt;Clear Cells on Execute — When enabled, only the last cell is displayed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Toggling all the three new settings transforms the console into an interactive editor resembling an ephemeral notebook with a single cell.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="The code console configured as a single executable cell" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-4-and-notebook-7-4-are-available/images/002-0_yzStobKzh8gMimoc.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;The code console configured as a single executable cell&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="settings-import-and-export"&gt;Settings import and export&lt;/h2&gt;
&lt;p&gt;Settings can now be exported to &lt;code&gt;overrides.json&lt;/code&gt; from the Settings Editor, which can be used to pre-configure defaults in deployments or to restore settings.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Import and export settings from the Settings Editor" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-4-and-notebook-7-4-are-available/images/003-0_MaZubqod8W1g-JHz.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Import and export settings from the Settings Editor&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="workspace-indicator"&gt;Workspace indicator&lt;/h2&gt;
&lt;p&gt;An opt-in workspace indicator is displayed on the top bar (next to the menu bar). It can be enabled in the &lt;code&gt;Settings → View → Appearance → Show Workspace Indicator&lt;/code&gt;:&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="The workspace indicator" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-4-and-notebook-7-4-are-available/images/004-0_9RGYIRu0foogfJC-.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;The workspace indicator&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="support-for-collaboration-without-rtc-drive"&gt;Support for collaboration without &lt;code&gt;RTC:&lt;/code&gt; drive&lt;/h2&gt;
&lt;p&gt;Real time collaboration in previous versions of JupyterLab (which can be enabled by installing jupyter-collaboration package) was implemented by swapping the default JupyterLab file system drive to a collaborative drive with the &lt;code&gt;RTC:&lt;/code&gt; prefix. This approach was incompatible with multiple extensions. JupyterLab 4.4 introduces the Content Providers API, which allows the next version of &lt;code&gt;jupyter-collaboration&lt;/code&gt; to work without changing the drive prefix. This improves compatibility with extensions that do not support arbitrary drives.&lt;/p&gt;
&lt;h2 id="kernel-subshells-support"&gt;Kernel subshells support&lt;/h2&gt;
&lt;p&gt;Kernel subshells, introduced in &lt;a href="https://jupyter.org/enhancement-proposals/91-kernel-subshells/kernel-subshells.html"&gt;JEP 91&lt;/a&gt;, enable concurrent code execution in kernels that support them. When performing long-running computations (such as training a model), subshells enable users to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;ipywidgets&lt;/code&gt; with updates displayed immediately&lt;/li&gt;
&lt;li&gt;Monitor kernel resource usage (e.g. memory usage)&lt;/li&gt;
&lt;li&gt;Invoke arbitrary commands (e.g., refine code for a subsequent cell) while maintaining live access to previously defined variables&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Subshells are supported by &lt;code&gt;ipykernel&lt;/code&gt; 7 (currently in alpha).&lt;/p&gt;
&lt;p&gt;To create a subshell, open context menu (right-click) over a notebook using a kernel supporting subshells and select “New Subshell Console for Notebook”. A new console panel will open allowing access to variables from the main session.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Creating a new subshell console for a notebook in JupyterLab 4.4" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/jupyterlab-4-4-and-notebook-7-4-are-available/images/005-0_Ivi-4TMFnn-lcz4S.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Creating a new subshell console for a notebook in JupyterLab 4.4&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;JupyterLab can automatically create subshells for comm messages if kernels support this feature. This enables concurrent execution of &lt;code&gt;ipywidgets&lt;/code&gt; callbacks without requiring a user action. This behavior can be changed from the settings, with options to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;disable that feature to not use subshells for comms&lt;/li&gt;
&lt;li&gt;(default) automatically create one subshell per comm-target (e.g. one subshell for all &lt;code&gt;ipywidgets&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;automatically create one subshell per comm (this can lead to issues if creating many comms)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="context-menu-opt-out"&gt;Context menu opt-out&lt;/h2&gt;
&lt;p&gt;The context menu, available under right mouse click in browsers, can now be disabled globally by toggling the &lt;code&gt;Enable Context Menu&lt;/code&gt; option available in the Command Palette, or via the JSON Settings Editor in the Application Context Menu.&lt;/p&gt;
&lt;h2 id="slimmer-status-bar"&gt;Slimmer status bar&lt;/h2&gt;
&lt;p&gt;The terminals counter no longer appears in the status bar if no terminals are open. The kernels counter can be disabled by toggling the &lt;code&gt;Show the status bar item&lt;/code&gt; setting in &lt;code&gt;Running Terminals and Kernels&lt;/code&gt; (code name &lt;code&gt;showStatusBarItem&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="walkthrough-video"&gt;Walkthrough video&lt;/h2&gt;
&lt;p&gt;If you prefer a more interactive tour of the new features mentioned in the post, check out the video below:&lt;/p&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/YhYor8LWeJY" title="What&amp;#x27;s new in JupyterLab 4.4 and Notebook 7.4" width="560" height="315" style="aspect-ratio: 560 / 315" loading="lazy" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;
&lt;h2 id="acknowledgements"&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;Thank you to all the contributors (list below is based on GitHub usernames):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JupyterLab: @Adam-D-Lewis @afshin @ajbozarth @AmberArr @andrewfulton9 @andreytaboola @andrii-i @bollwyvl @brichet @Carreau @claytonparnell @cmarmo @Darshan808 @davidbrochart @deephbz @dlqqq @echarles @fcollonval @fleming79 @holzman @hxrshxz @ianthomas23 @iisakkirotko @itsmevichu @JasonWeill @jesuino @joaopalmeiro @jtpio @kellyrowland @krassowski @kuraga @maitreya2954 @martenrichter @martinRenou @mgeier @MUFFANUJ @nkn2022 @Nriver @pawel99k @pdarshane @peytondmurray @pre-commit-ci @Princekumarofficial @Rishab87 @rpwagner @RRosio @rsaditya01 @SamuelMarks @SatyajitRedekar @SylvainCorlay @trungleduc @Zsailer&lt;/li&gt;
&lt;li&gt;Notebook: @andrii-i @jdavid @jtpio @krassowski @RRosio&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Special thank you to all the new contributors, the Jupyter Frontends team hopes to see you back!&lt;/p&gt;
&lt;p&gt;Thank you to all the people who tested and provided feedback for the pre-releases! Lastly, thank you to the weekly triage group for triaging JupyterLab, Notebook and JupyterLab Desktop issues: @krassowski, @RRosio, @andrii-i, @danyeaw among others who attend.&lt;/p&gt;
</content><category term="AI"/><category term="Jupyter Notebook"/><category term="JupyterLab"/><category term="releases"/></entry><entry><title>Building AI Agents for JupyterLab using Notebook Intelligence</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2025/building-ai-agents-for-jupyterlab-using-notebook/" rel="alternate"/><published>2025-02-24T18:03:00+00:00</published><updated>2025-02-24T18:03:00+00:00</updated><author><name>Mehmet Bektas</name></author><id>tag:jasongrout.github.io,2025-02-24:/medium-archive/pelican/posts/2025/building-ai-agents-for-jupyterlab-using-notebook/</id><summary type="html">&lt;p&gt;It is now possible to build AI Agents for JupyterLab and access from Copilot Chat UI, using Notebook Intelligence!&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;em&gt;Please note that this is not an official Jupyter subproject but an independent open-source tool for JupyterLab users who want to use GitHub Copilot as an AI coding assistant.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/notebook-intelligence/notebook-intelligence"&gt;Notebook Intelligence&lt;/a&gt; (NBI) is an AI coding assistant and extensible AI framework for JupyterLab. (&lt;em&gt;For an introduction to NBI see&lt;/em&gt; &lt;a href="/posts/2025/introducing-notebook-intelligence/"&gt;&lt;em&gt;Introducing Notebook Intelligence&lt;/em&gt;&lt;/a&gt; &lt;em&gt;and for basics of extending NBI see&lt;/em&gt; &lt;a href="https://notebook-intelligence.github.io/notebook-intelligence/blog/2025/02/05/building-ai-extensions-for-jupyterlab.html"&gt;&lt;em&gt;Building AI Extensions for JupyterLab&lt;/em&gt;&lt;/a&gt; &lt;em&gt;blog posts.&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;GitHub Copilot and other AI coding assistants are great at generating code and answering coding related questions. But they can do a lot more than generating text and code thanks to LLM features such as tool calling and AI agents. NBI provides an extensible AI framework to integrate tool calling and AI agents into JupyterLab Copilot Chat.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="AI Agent extension example" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/building-ai-agents-for-jupyterlab-using-notebook/images/001-1_x9r8gIqX1wbubuE8sT6GFg.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;AI Agent extension example&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="what-is-tool-calling-and-an-ai-agent"&gt;What is tool calling and an AI Agent?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Tool calling&lt;/strong&gt; is a feature of LLMs. It lets you introduce your own functions to LLM so that they can be called in response to chat prompts. LLM can convert natural language prompts to function calls with arguments. Tool calls are executed on the client side (i.e. Jupyter server) by your extension and only the function schema is provided to the LLM. Tool calling lets LLM interact with real time data, proprietary or external apps and services.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI Agents&lt;/strong&gt; are collections of tools that can run tasks on behalf of the user. Given a natural language prompt, LLMs can reason, create an execution plan and call multiple tools in a chain. NBI provides a framework to build these type of AI Agent integrations and handles the orchestration between LLMs and your tools.&lt;/p&gt;
&lt;h2 id="ai-agent-extension-example"&gt;AI Agent Extension Example&lt;/h2&gt;
&lt;p&gt;Let’s build an AI Agent for JupyterLab using Notebook Intelligence extension APIs. (&lt;em&gt;The full source code for this extension is&lt;/em&gt; &lt;a href="https://github.com/notebook-intelligence/nbi-ai-agent-example"&gt;&lt;em&gt;available here&lt;/em&gt;&lt;/a&gt;.) This will be an AI agent for map creation and notebook sharing. It will have the following capabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Looking up geo-coordinates for an address&lt;/li&gt;
&lt;li&gt;Showing maps centered at an address in the Copilot Chat UI&lt;/li&gt;
&lt;li&gt;Creating notebooks that show maps centered at specified addresses&lt;/li&gt;
&lt;li&gt;Sharing notebooks publicly using &lt;a href="https://notebooksharing.space/"&gt;notebooksharing.space&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The tasks above will be run by the AI Agent in response to natural language prompts by the user.&lt;/p&gt;
&lt;p&gt;For this extension we will build four tools that will be integrated into JupyterLab Copilot Chat, for each of the tasks above. Tools are defined as classes derived from NBI &lt;code&gt;Tool&lt;/code&gt; abstract class. A tool needs to implement the methods and properties defined in this base class.&lt;/p&gt;
&lt;p&gt;Tool class provides the metadata information for the tool and implements the &lt;code&gt;pre_invoke&lt;/code&gt; and &lt;code&gt;handle_tool_call&lt;/code&gt; methods. &lt;code&gt;pre_invoke&lt;/code&gt; method is called right before &lt;code&gt;handle_tool_call&lt;/code&gt; with the tool arguments and it gives an opportunity for the tool to prompt for confirmation of the tool execution.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;schema&lt;/code&gt; property of the Tool is the function schema based on OpenAI’s function calling schema. It lets you describe your function and its parameters as an object. A Tool is expected to return an object as response from the &lt;code&gt;handle_tool_call&lt;/code&gt; method call.&lt;/p&gt;
&lt;h2 id="geo-coordinates-lookup-tool"&gt;Geo Coordinates Lookup Tool&lt;/h2&gt;
&lt;p&gt;This tool looks up geo-coordinates for an address using &lt;a href="https://github.com/geopy/geopy"&gt;Nominatim&lt;/a&gt; library. &lt;code&gt;pre_invoke&lt;/code&gt; method for this tool only shows a message in Chat UI before looking up for the geo-coordinates in &lt;code&gt;handle_tool_call&lt;/code&gt; method.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;GeoCoordinateLookupTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;geo_coordinate_lookup&amp;quot;&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Get geo-coordinates from an address&amp;quot;&lt;/span&gt;
    
    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;This is a tool that converts an address to a geo-coordinates&amp;quot;&lt;/span&gt;
    
    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;function&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;function&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;&amp;quot;parameters&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;object&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;&amp;quot;properties&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;string&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="s2"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Address to convert to geo-coordinates&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="s2"&gt;&amp;quot;required&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="s2"&gt;&amp;quot;additionalProperties&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pre_invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ToolPreInvokeResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;address&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ToolPreInvokeResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Getting coordinates for &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;address&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geolocator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geocode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;latitude&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;longitude&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figure&gt;
&lt;img alt="Get geo-coordinates tool" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/building-ai-agents-for-jupyterlab-using-notebook/images/002-1_5qScMVUaGlIWWF7_F36Fsg.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Get geo-coordinates tool&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="map-response-generator-tool"&gt;Map Response Generator Tool&lt;/h2&gt;
&lt;p&gt;This tool shows a map in Copilot Chat UI centered at geo-coordinates. In &lt;code&gt;pre_invoke&lt;/code&gt; method this method only shows a notification message in Chat UI. In &lt;code&gt;handle_tool_call&lt;/code&gt; method, this tool returns a &lt;code&gt;HTMLFrame&lt;/code&gt; response that uses HTML to show a map centered at the requested location using Google Maps.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MapResponseGeneratorTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pre_invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ToolPreInvokeResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;geo_coordinates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;geo_coordinates&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geo_coordinates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latitude&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geo_coordinates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;longitude&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ToolPreInvokeResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Showing a map centered at latitude: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and longitude: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;geo_coordinates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;geo_coordinates&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geo_coordinates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latitude&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geo_coordinates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;longitude&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLFrameData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&amp;lt;iframe width=&amp;quot;100%&amp;quot; height=&amp;quot;100%&amp;quot; frameborder=&amp;quot;0&amp;quot; scrolling=&amp;quot;no&amp;quot; marginheight=&amp;quot;0&amp;quot; marginwidth=&amp;quot;0&amp;quot; id=&amp;quot;gmap_canvas&amp;quot; src=&amp;quot;https://maps.google.com/maps?width=400&amp;amp;amp;height=400&amp;amp;amp;hl=en&amp;amp;amp;q=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;amp;t=&amp;amp;amp;z=11&amp;amp;amp;ie=UTF8&amp;amp;amp;iwloc=B&amp;amp;amp;output=embed&amp;quot;&amp;gt;&amp;lt;/iframe&amp;gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;I showed the map&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Below is an example prompt showing map centered at “Golden Gate Bridge, San Francisco”. Note that an address was provided to Copilot as the input but Map Response Generator Tool accepts only geo-coordinates as input. This is where LLM automatically decided that it needs to first call the Geo Coordinates Lookup Tool to get geo-coordinates for this address and then it called the Map Response Generator Tool with the geo-coordinates. LLM automatically chained multiple tools and NBI handled this chaining to get the correct response for the user’s prompt.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Map Response Generator Tool" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/building-ai-agents-for-jupyterlab-using-notebook/images/003-1_6o_XrO-44kKaGYmEefsXKQ.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Map Response Generator Tool&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="map-notebook-creator-tool"&gt;Map Notebook Creator Tool&lt;/h2&gt;
&lt;p&gt;This tool creates a notebook centered at the specified geo-coordinates. In &lt;code&gt;pre_invoke&lt;/code&gt; method this method only shows a notification message in Chat UI. In &lt;code&gt;handle_tool_call&lt;/code&gt; method, the tool creates a notebook using &lt;code&gt;nbformat&lt;/code&gt; library, saves it to disk and then opens the notebook in JupyterLab UI using the &lt;code&gt;response.run_ui_command&lt;/code&gt; NBI method.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MapNotebookCreatorTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pre_invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ToolPreInvokeResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;geo_coordinates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;geo_coordinates&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geo_coordinates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latitude&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geo_coordinates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;longitude&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ToolPreInvokeResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Creating a map notebook for latitude: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and longitude: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;geo_coordinates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;geo_coordinates&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geo_coordinates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latitude&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geo_coordinates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;longitude&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;map_file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;map_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;%Y%m&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s1"&gt;_%H%M%S&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.ipynb&amp;quot;&lt;/span&gt;

        &lt;span class="n"&gt;nb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nbf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_notebook&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;        ### This map notebook was created by an AI Agent using [Notebook Intelligence](https://github.com/notebook-intelligence)&lt;/span&gt;
&lt;span class="s2"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

        &lt;span class="n"&gt;install_code_cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%%&lt;/span&gt;&lt;span class="s2"&gt;capture&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;%pip install folium&amp;quot;&lt;/span&gt;

        &lt;span class="n"&gt;map_code_cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;        import folium&lt;/span&gt;

&lt;span class="s2"&gt;        map = folium.Map(location=[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;], zoom_start=13)&lt;/span&gt;
&lt;span class="s2"&gt;        map&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

        &lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cells&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;nbf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_markdown_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;nbf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_code_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;install_code_cell&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;nbf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_code_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map_code_cell&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;kernelspec&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;python3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;nbf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;map_file_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_ui_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;docmanager:open&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;map_file_name&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;I created and opened the map notebook&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figure&gt;
&lt;img alt="Map Notebook Creator Tool" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/building-ai-agents-for-jupyterlab-using-notebook/images/004-1_2BZGe7aGPnNTPY79ZNhUDQ.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Map Notebook Creator Tool&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="notebook-share-tool"&gt;Notebook Share Tool&lt;/h2&gt;
&lt;p&gt;This tool shares a notebook publicly by uploading it to &lt;a href="https://notebooksharing.space/"&gt;notebooksharing.space&lt;/a&gt; and displays the link to the shared notebook.&lt;/p&gt;
&lt;p&gt;In the &lt;code&gt;pre_invoke&lt;/code&gt; method implementation, this tool asks for confirmation first as this operation is an undoable share of the notebook publicly. Only after the user confirms, &lt;code&gt;handle_tool_call&lt;/code&gt; is executed. In &lt;code&gt;handle_tool_call&lt;/code&gt; method the tool uploads the notebook at the &lt;code&gt;notebook_file_path&lt;/code&gt; using &lt;a href="https://github.com/notebook-sharing-space/nbss-upload"&gt;nbss_upload&lt;/a&gt; library and then shows the link to the shared notebook on &lt;a href="https://notebooksharing.space/"&gt;notebooksharing.space&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;NotebookShareTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pre_invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ToolPreInvokeResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;notebook_file_path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ToolPreInvokeResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Sharing notebook &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;confirmationTitle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Confirm sharing&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;confirmationMessage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Are you sure you want to share the notebook at &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;? This will upload the notebook to public internet and cannot be undone.&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;notebook_file_path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;share_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nbss_upload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upload_notebook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://notebooksharing.space&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AnchorData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;share_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Click here to view the shared notebook &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Notebook &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39; has been shared at: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;share_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figure&gt;
&lt;img alt="Notebook Share Tool" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/building-ai-agents-for-jupyterlab-using-notebook/images/005-1_CYXJbXzmMGnuW_c_ktwkgA.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Notebook Share Tool&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="tool-call-schema-definitions"&gt;Tool call schema definitions&lt;/h2&gt;
&lt;p&gt;It is important to define schemas of the tools clearly and disambiguate the tools as much as possible so that the LLM can invoke the proper tool based on the user prompt. LLM parses the user prompt, decides which tools to call and generates the input parameters for the call.&lt;/p&gt;
&lt;p&gt;If current file or selection is made visible by the user, NBI can provide the file paths and content as context to the LLM. That way LLM can use those as additional context for a tool call. That is how Notebook Share Tool was able to access the current notebook file.&lt;/p&gt;
&lt;h2 id="tool-chaining"&gt;Tool chaining&lt;/h2&gt;
&lt;p&gt;After parsing the user prompt, LLM creates an execution plan and can call multiple tools in a chain. NBI handles this tool chaining for you. It is important to define your schemas with the chaining in mind. Consider defining matching tool outputs and inputs so that the output of a tool can be passed onto another one directly if needed.&lt;/p&gt;
&lt;p&gt;Notice that in this extension example MapResponseGeneratorTool and MapNotebookCreatorTool both take in geo_coordinates (latitude, longitude) as input and GeoCoordinateLookupTool outputs geo_coordinates. This lets LLM to directly pass the output of GeoCoordinateLookupTool to MapResponseGeneratorTool and MapNotebookCreatorTool. It also lets a user to use an address to trigger MapResponseGeneratorTool and MapNotebookCreatorTool, because LLM knows that there is another tool it can call to generate input (geo_coordinates) from address for these tools.&lt;/p&gt;
&lt;h2 id="chat-participant"&gt;Chat Participant&lt;/h2&gt;
&lt;p&gt;In NBI AI framework, AI Agents are defined as chat participants and tools are tied to specific chat participants. For our extension we create &lt;code&gt;AIAgentChatParticipant&lt;/code&gt; as our participant (for more details on NBI extensions and chat participants see &lt;a href="http://127.0.0.1:4000/notebook-intelligence/blog/2025/02/04/building-ai-extensions-for-jupyterlab.html"&gt;this blog&lt;/a&gt;). Our chat participant returns list of tools it defines from the &lt;code&gt;tools&lt;/code&gt; property.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;handle_chat_request&lt;/code&gt; method our chat participant passes the request to the base &lt;code&gt;ChatParticipant&lt;/code&gt; class to handle tool calling for us.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;AIAgentChatParticipant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ChatParticipant&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="vi"&gt;@property&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ai-agent&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="vi"&gt;@property&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Tool&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="no"&gt;GeoCoordinateLookupTool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;MapResponseGeneratorTool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;MapNotebookCreatorTool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;NotebookShareTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_chat_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;ChatResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_chat_request_with_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="nbi-extension"&gt;NBI Extension&lt;/h2&gt;
&lt;p&gt;Finally we create our NBI extension class &lt;code&gt;AIAgentExtension&lt;/code&gt;. This class basically registers our chat participant to NBI on extension activation.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;AIAgentExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotebookIntelligenceExtension&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;participant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AIAgentChatParticipant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register_chat_participant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;participant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AI Agent example extension activated&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That is all there is to create an AI Agent for JupyterLab using Notebook Intelligence. The &lt;a href="https://github.com/notebook-intelligence/nbi-ai-agent-example"&gt;full source code&lt;/a&gt; for this example is available along with installation instructions for you to use as a reference and/or build on top.&lt;/p&gt;
&lt;h2 id="try-it-out-and-share-your-feedback"&gt;Try it out and share your feedback!&lt;/h2&gt;
&lt;p&gt;I am looking forward to seeing the AI Agents built by the community. Please try the extension APIs and share your feedback using project’s &lt;a href="https://github.com/notebook-intelligence/notebook-intelligence/issues"&gt;GitHub issues&lt;/a&gt;! User feedback from the community will shape the project’s roadmap.&lt;/p&gt;
&lt;h2 id="about-the-author"&gt;About the Author&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/mehmet-bektas"&gt;Mehmet Bektas&lt;/a&gt; is a Senior Software Engineer at Netflix and a Jupyter Distinguished Contributor. He is the author of Notebook Intelligence, and contributes to JupyterLab, JupyterLab Desktop and several other projects in the Jupyter eco-system.&lt;/p&gt;
</content><category term="AI"/><category term="JupyterLab"/></entry><entry><title>Introducing Notebook Intelligence!</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/" rel="alternate"/><published>2025-01-13T16:28:00+00:00</published><updated>2025-01-13T16:28:00+00:00</updated><author><name>Mehmet Bektas</name></author><id>tag:jasongrout.github.io,2025-01-13:/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/</id><summary type="html">&lt;p&gt;I am thrilled to announce the release of Notebook Intelligence! NBI is an AI coding assistant for JupyterLab powered by GitHub Copilot…&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;em&gt;Please note that this is not an official Jupyter subproject but an independent open-source tool for JupyterLab users who want to use GitHub Copilot as an AI coding assistant.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I am thrilled to announce the release of &lt;a href="https://github.com/mbektas/notebook-intelligence"&gt;Notebook Intelligence&lt;/a&gt; (NBI)! NBI is an AI coding assistant and extensible AI framework for JupyterLab. It uses &lt;a href="https://github.com/features/copilot"&gt;GitHub Copilot&lt;/a&gt; under the hood and is inspired by its design principles. NBI greatly boosts the productivity of JupyterLab users with AI assistance powered by GitHub Copilot.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Generate code" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/001-1_X4bZbN5zydCMu6AJV3bP7A.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Generate code&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="generate-code-iterate-on-it"&gt;Generate code, iterate on it&lt;/h2&gt;
&lt;p&gt;NBI integrates tightly with the notebook document. Using cell toolbar item “Generate code” or keyboard shortcut “Cmd + G” / “Ctrl + G”, you can launch the inline coding assistant popover to generate code cells.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Generate code popover" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/002-1_WnT4YBV94oGF_qvT7rpMZg.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Generate code popover&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;If the inline coding assistant is launched for a cell with existing code, then the generated code is shown in a diff view for approval.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Generate code with diff viewer" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/003-1_4hM3aV5c3HycBY2HWQSaBQ.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Generate code with diff viewer&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;If you are not satisfied with the generated code, you can re-generate with an updated prompt. Diff viewer also lets you edit the generated code manually before accepting it.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Fix code in cell with NBI" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/004-1_8GgXLlEYjGtHNNw_m1VK4A.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Fix code in cell with NBI&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="explain-and-fix-code-troubleshoot-errors-reported"&gt;Explain and fix code, troubleshoot errors reported&lt;/h2&gt;
&lt;p&gt;NBI adds a new sub menu to notebook cell context menu. Copilot can explain code in a cell or suggest fixes for any issues in it. Clicking these menu items opens Copilot Chat and generates a suggestion.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Copilot context menu" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/005-1_ItUuGiP1VvhgxQ4BzBDb5w.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Copilot context menu&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;If the code cell has an output you can ask Copilot to explain it. If there are any errors reported, you can have Copilot to troubleshoot as well. These actions also take you to Copilot Chat interface.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Explain, fix, troubleshoot" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/006-1_X7c10sZzH5qbv5kpAUOMBA.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Explain, fix, troubleshoot&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="inline-completions"&gt;Inline Completions&lt;/h2&gt;
&lt;p&gt;Notebook Intelligence integrates with JupyterLab’s inline completion APIs and provides code suggestions as you type in a code cell or a Python file.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Inline completions" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/007-1_EG7lvC6mjTyHLi6mcAGEIQ.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Inline completions&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Code suggestions are generated using GitHub Copilot. They are blazing fast and relevant to document you are working on. In addition to the code cell you are working on, the code in the surrounding cells are also used as context when generating suggestions.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Inline completions example" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/008-1_BkIHAJsG4Fs7-8aD3k-4GA.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Inline completions example&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="copilot-chat"&gt;Copilot Chat&lt;/h2&gt;
&lt;p&gt;NBI provides a user friendly chat interface to chat with GitHub Copilot. You can ask questions related to coding.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Copilot Chat" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/009-1_ebEfHbxI6JnOWu9sSgP-nA.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Copilot Chat&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;If Copilot generates code snippets, they are rendered in a special format in a section with an action toolbar. Toolbar will have buttons to copy, insert, create new Python file and notebook from the snippet. Using these you can easily integrate the generated code into your project.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Copilot chat toolbar actions" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/010-1_aAOuJZrObHMhZ2aaxhWRuA.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Copilot chat toolbar actions&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id="chat-commands"&gt;Chat Commands&lt;/h3&gt;
&lt;p&gt;Chat interface also provides commands to generate new notebooks and Python code files based on your task described in the prompt. Commands start with “/” and a command auto-complete list is shown as you type. You can navigate between the commands using keyboard and choose a suggestion using “Enter” or “Tab” keys.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Chat command auto-complete" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/011-1_WBLVr1CtBxhVpMr2KYPVuQ.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Chat command auto-complete&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id="newnotebook-command"&gt;/newNotebook command&lt;/h3&gt;
&lt;p&gt;You can generate new notebooks from a prompt with the &lt;strong&gt;/newNotebook&lt;/strong&gt; command. Notebook generation is shown interactively, a new empty notebook is created and opened, then code and markdown cells are added onto the notebook as they are generated by NBI and GitHub Copilot.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Generate notebook example" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/012-1_Dkhv6nZt2vExa-UjMTkaRA.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Generate notebook example&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id="newpythonfile-command"&gt;/newPythonFile command&lt;/h3&gt;
&lt;p&gt;You can also create new Python files from a prompt using the &lt;strong&gt;/newPythonFile&lt;/strong&gt; command.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Generate Python file example" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/013-1_H6fNVs50mmQcsYlTAE17bg.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Generate Python file example&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="getting-started-with-notebook-intelligence"&gt;Getting Started with Notebook Intelligence&lt;/h2&gt;
&lt;p&gt;Notebook Intelligence is a JupyterLab extension published as a Python package. Simply install the package and restart JupyterLab. NBI will add a new sidebar item for Copilot Chat, a notebook context sub-menu, a cell toolbar item for “Generate code” and a status bar item for GitHub Copilot login status to JupyterLab UI. It will also be integrated with inline completion (AI suggestions for code completion).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;notebook-intelligence
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="authentication-with-github-copilot"&gt;Authentication with GitHub Copilot&lt;/h2&gt;
&lt;p&gt;Notebook Intelligence requires a &lt;a href="https://github.com/features/copilot"&gt;GitHub Copilot&lt;/a&gt; subscription. NBI provides a user friendly interface to sign into your GitHub Copilot account from JupyterLab UI to activate access to your subscription.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="GitHub Copilot authentication" src="https://jasongrout.github.io/medium-archive/pelican/posts/2025/introducing-notebook-intelligence/images/014-1_5qQDXByT8dHSHMs86geeiQ.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;GitHub Copilot authentication&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="extensible-framework"&gt;Extensible Framework&lt;/h2&gt;
&lt;p&gt;Notebook Intelligence provides APIs to let developers extend its capabilities. You can add custom agents / chat participants, define tools (function calling) and add RAG capabilities to provide your own context to LLM for code / chat response generation. Stay tuned for my next blog post where I will walk you though extensibility features.&lt;/p&gt;
&lt;h2 id="try-it-out-and-share-your-feedback"&gt;Try it out and share your feedback!&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/mbektas/notebook-intelligence"&gt;Notebook Intelligence&lt;/a&gt; is currently in beta and designed for Python (support for more languages coming soon). Please try it out and share your feedback and any feature requests using project’s &lt;a href="https://github.com/mbektas/notebook-intelligence/issues"&gt;GitHub issues&lt;/a&gt;! User feedback from the community will shape the project’s roadmap.&lt;/p&gt;
&lt;h2 id="about-the-author"&gt;About the Author&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/mehmet-bektas"&gt;Mehmet Bektas&lt;/a&gt; is a Senior Software Engineer at Netflix and a Jupyter Distinguished Contributor. He is the author of Notebook Intelligence, and contributes to JupyterLab, JupyterLab Desktop and several other projects in the Jupyter eco-system.&lt;/p&gt;
</content><category term="AI"/><category term="JupyterLab"/></entry><entry><title>Generative AI in Jupyter</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/" rel="alternate"/><published>2023-08-02T16:15:00+00:00</published><updated>2023-08-16T00:33:00+00:00</updated><author><name>Jason Weill</name></author><id>tag:jasongrout.github.io,2023-08-02:/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/</id><summary type="html">&lt;p&gt;Jupyter AI, a new open source project, brings generative artificial intelligence to notebooks with magic commands and a chat interface.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;a href="https://github.com/jupyterlab/jupyter-ai"&gt;Jupyter AI&lt;/a&gt; brings generative artificial intelligence to Jupyter notebooks, giving users the power to explain and generate code, fix errors, summarize content, ask questions about their local files, and generate entire notebooks from a natural language prompt. Using its powerful magic commands and chat interface, Jupyter AI connects Jupyter with large language models (LLM) from providers such as AI21, Anthropic, AWS, Cohere, and OpenAI. We use LangChain to support all popular LLMs and providers, giving you access to new models as they are released. LangChain will let Jupyter AI use local models as well. Jupyter AI version 1.0, for JupyterLab 3, and Jupyter AI 2.0, for JupyterLab 4, are now available as free and open source software.&lt;/p&gt;
&lt;p&gt;Jupyter AI is designed with responsible AI and data privacy in mind. You can pick which LLM and embedding model best suit your needs. The underlying prompts, chains, and other software are open source, so you can see exactly how your data is being used. Jupyter AI saves metadata about model-generated content in each AI-generated code cell, so you and your collaborators can track where AI-generated code enters your workflow. Finally, Jupyter AI only contacts an LLM when you ask it to, directly; it does not read your data or transmit it to models without your explicit consent.&lt;/p&gt;
&lt;p&gt;Jupyter AI is an official subproject of Project Jupyter and is available now as free, open source software. We designed it based on the same principles that underlie all of Project Jupyter: we made it simple, easy to use, modular, and extensible, and we prioritized ethical considerations and social responsibility. We’d love to hear from you about how we can improve it!&lt;/p&gt;
&lt;h2 id="getting-started-with-jupyter-ai"&gt;Getting started with Jupyter AI&lt;/h2&gt;
&lt;p&gt;Start using Jupyter AI by installing the appropriate version with &lt;code&gt;pip&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jupyter-ai&amp;gt;=1.0,&amp;lt;2.0&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# If you use JupyterLab 3&lt;/span&gt;
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;jupyter-ai&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="c1"&gt;# If you use JupyterLab 4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, launch JupyterLab. Jupyter AI provides two different interfaces to interact with LLMs. In JupyterLab, you can converse with a chat UI to assist you with your code. Also, in any supported notebook or IPython environment, including JupyterLab, Notebook, IPython, Colab, and Visual Studio Code, you can invoke LLMs using the &lt;code&gt;%%ai&lt;/code&gt; magic command. Jupyter AI can turn any Jupyter Notebook session into a generative AI playground with support for text and image models.&lt;/p&gt;
&lt;p&gt;Project Jupyter is vendor-neutral, so Jupyter AI supports LLMs from AI21, Anthropic, AWS, Cohere, HuggingFace Hub, and OpenAI. More model providers will be added in the future. Please review a provider’s privacy policy and pricing model before you use it. We’re also working on support for locally-deployed models, for maximum privacy. Once you have installed Jupyter AI, before you can use magic commands, you will need to authenticate to each model provider that you wish to use. For most providers, this involves setting an environment variable. The user documentation has detailed instructions for &lt;a href="https://jupyter-ai.readthedocs.io/en/latest/users/index.html#model-providers"&gt;configuring model providers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The chat interface has its own configuration panel for choosing a language model and an embedding model, and for authenticating to each model’s provider. A language model responds to users’ messages in the chat panel. When you ask the chat interface to learn about local files, it uses an embedding model to parse these files and to assist when you ask questions about them.&lt;/p&gt;
&lt;p&gt;You can find full details about how to configure and use Jupyter AI in the &lt;a href="https://jupyter-ai.readthedocs.io/en/latest/users/index.html"&gt;user documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-chat-interface-your-ai-assistant"&gt;The chat interface, your AI assistant&lt;/h2&gt;
&lt;p&gt;The chat interface puts you in conversation with &lt;strong&gt;Jupyternaut&lt;/strong&gt;, a conversational agent using a language model of your choice.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/001-0_LKAfjrw26xlrAJ-s.webp" alt="A screenshot of JupyterLab showing Jupyter AI in the left panel, with a help message shown to the user." loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Jupyternaut communicates primarily through text, and it can also interact with files in JupyterLab. It can answer questions as a general-purpose AI assistant, include selections from your notebooks with your questions, insert AI-generated output into your notebooks, learn from and ask questions about your local files, and generate notebooks from a prompt. Jupyternaut can only see the information you send it by sending chat commands; it only reads your data when you specifically ask it to.&lt;/p&gt;
&lt;p&gt;To get started, you can ask Jupyternaut a question:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/002-0_SBrRAW0NdUWIcj3B.webp" alt="Partial screenshot of Jupyter AI showing a conversation between jweill and Jupyternaut, in which jweill has asked “What’s the difference between a tuple and a list in Python?” and Jupyternaut has provided a detailed response with a code example." loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;You can also highlight part of your notebook and include it with your prompt.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/003-0_ppPDPJyDZ5vjprCP.webp" alt="Screenshot of JupyterLab with some source code highlighted. The user has typed the question “What does this code do?” and has checked a checkbox to include the selection with their question." loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Using prompts that include the selected code, you can ask Jupyternaut to explain your code in plain English (or in any other language it can speak), make modifications to it, and identify errors in it. If you want, Jupyternaut can even replace your selection with its response. Please review AI-generated code before you run it, as you would review code written by another person.&lt;/p&gt;
&lt;p&gt;For example, you can ask Jupyternaut to rewrite code by adding comments to it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/004-0_lWJIaPn8REBWHPHV.webp" alt="Partial screenshot of JupyterLab with some source code selected. The user has typed the prompt “Rewrite this code with comments added to it”, and has checked the boxes for “include selection” and “replace selection”" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Jupyternaut sends the code to your chosen language model, then replaces the selection with the language model’s response.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/005-0_1AQQ9XR-JQ2oqZ10.jpg" alt="Partial screenshot of JupyterLab with source code that has been rewritten with comments" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;h2 id="generating-a-notebook-from-a-text-prompt"&gt;Generating a notebook from a text prompt&lt;/h2&gt;
&lt;p&gt;Jupyter AI’s chat interface can generate an entire notebook from a text prompt. To do this, run the /generate command and provide a text description. Jupyternaut will use its AI language model to name the workbook and fill it with markdown and code cells. This may take a few minutes. While Jupyternaut is working, you can continue to use the chat UI, and Jupyternaut will continue generating your notebook.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/006-0_Ln2poeiOXHq2mlkw.webp" alt="Screenshot of a user executing a /generate command in Jupyter AI to generate a demonstration of how to use Matplotlib. Jupyternaut provides a response that it is starting to generate a notebook, then another response that it has generated a notebook" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Once Jupyternaut has finished generating your notebook, it will send you a message with its filename, so that you can open it. Please review any AI-driven code before you run it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/007-0_jdpMEQCErUeEtzO_.webp" alt="Screenshot of JupyterLab showing “Matplotlib: A Comprehensive Guide”, a Jupyter notebook generated by Jupyter AI" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;h2 id="learning-from-and-asking-about-local-files"&gt;Learning from and asking about local files&lt;/h2&gt;
&lt;p&gt;You can use the &lt;code&gt;/learn&lt;/code&gt; command to teach Jupyternaut about local files, so that you can use the &lt;code&gt;/ask&lt;/code&gt; command to ask questions about them. For example, using the &lt;code&gt;/learn&lt;/code&gt; command, you can teach Jupyternaut about Jupyter AI’s documentation:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/008-0_PxPcfM_mb_ZfsfRi.webp" alt="Screenshot of Jupyter AI having run a /learn command to learn the contents of the “docs” directory" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;When you learn local files, Jupyternaut uses an embedding model to convert data, then stores the output in a local vector database. Please review the privacy policy for each model, and be aware of any restrictions about sharing your local data with third-party model providers. Once the learning process is complete, you can ask a question with the &lt;code&gt;/ask&lt;/code&gt; command. Using retrieval-augmented generation (RAG), Jupyternaut will append relevant info to your question from its vector database, then it will use the AI language model you selected to answer your question.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/009-0_fXKXFT2dq2P4yK4B.webp" alt="Screenshot of Jupyter AI answering an “/ask” command based on the data it has previously learned from local files" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;h2 id="notebooks-as-generative-ai-playgrounds-with-magic-commands"&gt;Notebooks as generative AI playgrounds with magic commands&lt;/h2&gt;
&lt;p&gt;Jupyter AI also provides magic commands that you can run in notebook cells and in the IPython command-line interface. To get started, run &lt;code&gt;%load_ext jupyter_ai_magics&lt;/code&gt;, which will load the magics extension. You can then use Jupyter AI with the &lt;code&gt;%%ai&lt;/code&gt; magic command. You can run &lt;code&gt;%ai help&lt;/code&gt; to learn about all the options and commands you can run using the &lt;code&gt;%ai&lt;/code&gt; line magic and &lt;code&gt;%%ai&lt;/code&gt; cell magic commands.&lt;/p&gt;
&lt;p&gt;Each &lt;code&gt;%%ai&lt;/code&gt; command requires a model, typically specified as &lt;code&gt;provider‑id:model‑id&lt;/code&gt;. To use a particular provider, you’ll need to set its API key using an appropriate environment variable or Python module. See the &lt;a href="https://jupyter-ai.readthedocs.io/en/latest/users/index.html#model-providers"&gt;model providers section of the user documentation&lt;/a&gt; for specific information. Once you’ve provided the key to your model provider, you can run a magic command by specifying the model on the first line and specifying your prompt on subsequent lines.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/010-0_J1CWCz_eo5_vJWu0.webp" alt="Partial screenshot of JupyterLab running an %%ai magic command using the Anthropic Claude-v1.2 model" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;You can use the &lt;code&gt;-f&lt;/code&gt; or &lt;code&gt;--format&lt;/code&gt; parameter to customize the format of the output, including HTML, math, source code, and images.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/011-0_T8EepY5jfigS67Yn.webp" alt="Screenshot of JupyterLab running three %%ai magic commands to output content in HTML, math, and image formats" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;You can interpolate a variable name or expression in a prompt by enclosing it in braces (curly brackets).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/012-0_jPgHUMJegDgaQUNt.webp" alt="Screenshot showing JupyterLab running an %%ai magic command with variable interpolation" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Interpolation also works with the special &lt;code&gt;In&lt;/code&gt; and &lt;code&gt;Out&lt;/code&gt; variables, which contain the inputs and outputs of code cells. Note that a cell output can contain both text and markdown values.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/013-0_su7ZC4EpVmK2FRxg.webp" alt="Screenshot of JupyterLab running two %%ai magic commands: the first one generates output, and the second one uses interpolation to incorporate the first command’s output into its prompt" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Jupyter AI adds a special &lt;code&gt;Err&lt;/code&gt; variable, which stores the errors that occur while executing code. By interpolating this variable into a prompt, you can use an AI language model to explain and correct an error in your code.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/014-0_4fix_RU095C7PqZB.webp" alt="Screenshot of JupyterLab having run a cell with an error in it, then having run a cell that interpolates the error into a prompt" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;h2 id="about-the-developers"&gt;About the developers&lt;/h2&gt;
&lt;p&gt;Jupyter AI is an officially supported Jupyter subproject. The following Jupyter contributors built Jupyter AI.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Brian Granger is a Senior Principal Technologist at AWS. Brian is a cofounder of Project Jupyter, a member of the Jupyter Executive Council, and a board member of the PyTorch Foundation." src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/015-1_WC29kYZhf40VeGfM7muopg.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;&lt;a href="https://github.com/ellisonbg"&gt;Brian Granger&lt;/a&gt; is a Senior Principal Technologist at AWS. Brian is a cofounder of Project Jupyter, a member of the Jupyter Executive Council, and a board member of the PyTorch Foundation.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="Andrii Ieroshenko is a Software Development Engineer at AWS. Andrii is a Project Jupyter contributor and JupyterLab Council member." src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/016-0_zMXhUHT0SrteMJDo.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;&lt;a href="https://github.com/andrii-i/"&gt;Andrii Ieroshenko&lt;/a&gt; is a Software Development Engineer at AWS. Andrii is a Project Jupyter contributor and JupyterLab Council member.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="Piyush Jain is a Software Development Engineer at AWS working on open source contributions to Project Jupyter. He is also a member of the Jupyter Server team, actively participating in maintenance and development of the Jupyter Server project." src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/017-0_wDtguQqxfEHtzgzR.jpg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;&lt;a href="https://github.com/3coins"&gt;Piyush Jain&lt;/a&gt; is a Software Development Engineer at AWS working on open source contributions to Project Jupyter. He is also a member of the Jupyter Server team, actively participating in maintenance and development of the Jupyter Server project.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="David Qiu is a Software Development Engineer at AWS working on Project Jupyter. Previously, David was researching physical chemistry at the University of Illinois at Urbana-Champaign." src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/018-0_hfX4LQ_08sjeDoKE.jpg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;&lt;a href="https://github.com/dlqqq/"&gt;David Qiu&lt;/a&gt; is a Software Development Engineer at AWS working on Project Jupyter. Previously, David was researching physical chemistry at the University of Illinois at Urbana-Champaign.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="Jason Weill is a Senior Front-End Engineer at AWS; a contributor to JupyterLab, Jupyter Governance, and Jupyter Scheduler; and a member of the JupyterLab Council, the Jupyter Security Working Group, and the Jupyter Diversity, Equity, and Inclusion Standing Committee." src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/generative-ai-in-jupyter/images/019-0_65ivwhUugkG0UUR8.jpg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;&lt;a href="https://github.com/JasonWeill"&gt;Jason Weill&lt;/a&gt; is a Senior Front-End Engineer at AWS; a contributor to JupyterLab, Jupyter Governance, and Jupyter Scheduler; and a member of the JupyterLab Council, the Jupyter Security Working Group, and the Jupyter Diversity, Equity, and Inclusion Standing Committee.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="how-can-you-help"&gt;How can you help?&lt;/h2&gt;
&lt;p&gt;We’re just getting started with generative AI in Jupyter. Please join us! We have &lt;a href="https://github.com/jupyterlab/jupyter-ai/issues"&gt;a list of issues&lt;/a&gt; that could use your help, such as &lt;a href="https://github.com/jupyterlab/jupyter-ai/issues/190"&gt;adding support for locally hosted LLMs&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Install and use the&lt;/strong&gt; &lt;a href="https://github.com/jupyterlab/jupyter-ai"&gt;&lt;strong&gt;Jupyter AI&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;extension.&lt;/strong&gt; If you find any bugs or have suggestions, please create issues on GitHub.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Join the discussion&lt;/strong&gt; about generative AI in Jupyter in the “Generative AI in Jupyter” topic on Discourse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contribute:&lt;/strong&gt; Your bug reports, feature requests, and pull requests will help improve this project for everyone.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Note: An earlier version of this story said that users can choose which vector database to use with Jupyter AI. As of Jupyter AI 2.1.0, only FAISS is available; users cannot choose another vector database. Sorry for the error.&lt;/em&gt;&lt;/p&gt;
</content><category term="AI"/></entry></feed>