<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://plmbr.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://plmbr.dev/" rel="alternate" type="text/html" /><updated>2026-05-23T02:32:18+00:00</updated><id>https://plmbr.dev/feed.xml</id><title type="html">Notebook Intelligence</title><subtitle>A JupyterLab extension for Claude Code, GitHub Copilot, Ollama, and OpenAI-compatible models — with MCP, Skills, Plugins, and admin policy.</subtitle><author><name>Mehmet Bektaş</name></author><entry><title type="html">NBI 4.8.0 — Smarter workspace picker, atomic config saves, GitHub-import gate</title><link href="https://plmbr.dev/blog/v4-8-0/" rel="alternate" type="text/html" title="NBI 4.8.0 — Smarter workspace picker, atomic config saves, GitHub-import gate" /><published>2026-05-12T13:00:00+00:00</published><updated>2026-05-12T13:00:00+00:00</updated><id>https://plmbr.dev/blog/v4-8-0</id><content type="html" xml:base="https://plmbr.dev/blog/v4-8-0/"><![CDATA[<p>NBI 4.8.0 focuses on the small surfaces that users hit most: the chat sidebar’s <code class="language-plaintext highlighter-rouge">@</code>-mention workspace picker, the on-disk config file, and the Skills import path.</p>

<h2 id="workspace-picker--faster-quieter-gitignore-aware">Workspace picker — faster, quieter, gitignore-aware</h2>

<p>When you type <code class="language-plaintext highlighter-rouge">@</code> in the chat sidebar, NBI now scans your workspace in parallel. On a 1,000-file project the scan is roughly an order of magnitude faster than the 4.7.x serial walk, with a single concurrent dispatch instead of nested awaits per directory.</p>

<p>Three other improvements land alongside the perf work:</p>

<ul>
  <li><strong>Skip dot-prefixed files.</strong> <code class="language-plaintext highlighter-rouge">.git/</code>, <code class="language-plaintext highlighter-rouge">.venv/</code>, <code class="language-plaintext highlighter-rouge">.idea/</code>, <code class="language-plaintext highlighter-rouge">.DS_Store</code> and similar entries no longer clutter the picker by default.</li>
  <li><strong>Honor <code class="language-plaintext highlighter-rouge">.gitignore</code>.</strong> Workspace files matched by the project’s <code class="language-plaintext highlighter-rouge">.gitignore</code> are excluded. This is the right default for most repos and stops the picker from returning multi-megabyte build artifacts.</li>
  <li><strong>Extensible filter system.</strong> Filters are pluggable internally, so future filters (a per-user allowlist, a max-file-size cap, a JupyterLab settings-driven exclude list) slot in without churn.</li>
</ul>

<p>If you want a folder back that’s currently ignored, set <code class="language-plaintext highlighter-rouge">additional_skipped_workspace_directories</code> in <code class="language-plaintext highlighter-rouge">~/.jupyter/nbi/config.json</code> or override at the deployment level via <code class="language-plaintext highlighter-rouge">NBI_ADDITIONAL_SKIPPED_WORKSPACE_DIRECTORIES</code>.</p>

<h2 id="atomic-config-saves">Atomic config saves</h2>

<p><code class="language-plaintext highlighter-rouge">NBIConfig.save()</code> is now atomic. Previously, a crash mid-write (or a hard kill of the JupyterLab process) could leave <code class="language-plaintext highlighter-rouge">~/.jupyter/nbi/config.json</code> truncated or partially-written, and the next launch would either error out or fall back to defaults with no warning.</p>

<p>The new path:</p>

<ol>
  <li>Write the new contents to a sibling temp file in the same directory.</li>
  <li><code class="language-plaintext highlighter-rouge">fsync()</code> the temp file.</li>
  <li><code class="language-plaintext highlighter-rouge">rename()</code> the temp file over the target — atomic on POSIX, single-syscall-ish on Windows.</li>
  <li>Preserve the original file’s mode and symlinks.</li>
</ol>

<p>Net effect: the config file is either the full old contents or the full new contents at all times. No more silent corruption.</p>

<h2 id="allow_github_skill_import-admin-policy"><code class="language-plaintext highlighter-rouge">allow_github_skill_import</code> admin policy</h2>

<p>A new traitlet and environment variable gate the <strong>Import from GitHub</strong> Skills dialog:</p>

<ul>
  <li>Traitlet: <code class="language-plaintext highlighter-rouge">NotebookIntelligence.allow_github_skill_import</code> (Bool, default <code class="language-plaintext highlighter-rouge">True</code>).</li>
  <li>Env override: <code class="language-plaintext highlighter-rouge">NBI_ALLOW_GITHUB_SKILL_IMPORT=true|false</code>.</li>
</ul>

<p>When set to <code class="language-plaintext highlighter-rouge">false</code>, the dialog is removed from the Skills UI and the server rejects GitHub-import API calls. Org-managed manifest installs still work — this gate is for the user-driven path.</p>

<p>The env-var parser is fail-loud: a typo like <code class="language-plaintext highlighter-rouge">NBI_ALLOW_GITHUB_SKILL_IMPORT=ture</code> raises at startup rather than silently falling through to the default.</p>

<h2 id="redirect-hardening-on-skill-fetches">Redirect hardening on Skill fetches</h2>

<p>The Skills GitHub-import path used to follow HTTP redirects without restriction. 4.8.0 narrows the policy:</p>

<ul>
  <li><strong>No HTTPS-to-HTTP downgrades.</strong> If a <code class="language-plaintext highlighter-rouge">https://...</code> redirect points at <code class="language-plaintext highlighter-rouge">http://...</code>, the request fails.</li>
  <li><strong>Scope-checked redirects.</strong> Redirects to hosts outside the original request’s scope (e.g. <code class="language-plaintext highlighter-rouge">github.com</code> → <code class="language-plaintext highlighter-rouge">evil.example.com</code>) are blocked.</li>
</ul>

<p>A malicious or hijacked Skill bundle source can no longer redirect NBI through an attacker-controlled host.</p>

<h2 id="cell-toolbar-position-adjustment">Cell toolbar position adjustment</h2>

<p>The NBI cell toolbar’s position has been nudged to coexist better with JupyterLab’s native cell toolbar — fewer overlaps on narrow notebooks. The NBI notebook toolbar button (sparkle icon) is also now correctly gated on whether Claude mode is active.</p>

<h2 id="skipping-dot-files-in--mentions">Skipping dot-files in <code class="language-plaintext highlighter-rouge">@</code>-mentions</h2>

<p>Beyond the workspace scan, the <code class="language-plaintext highlighter-rouge">@</code>-mention context picker also now skips dot-prefixed files at the top level. Previously they could appear in the picker if you typed <code class="language-plaintext highlighter-rouge">@.&lt;character&gt;</code> — useful in rare cases but mostly noise.</p>

<h2 id="install">Install</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">--upgrade</span> notebook-intelligence
</code></pre></div></div>

<p>Then restart JupyterLab. Full set of changes in the <a href="https://github.com/notebook-intelligence/notebook-intelligence/releases/tag/v4.8.0">v4.8.0 release notes</a>.</p>]]></content><author><name>Mehmet Bektaş</name></author><summary type="html"><![CDATA[The chat-sidebar workspace picker is now .gitignore-aware, dot-file-aware, and parallel. Config saves are atomic. A new admin policy gates GitHub-based Skill imports.]]></summary></entry><entry><title type="html">NBI 4.7.0 — Cell output actions, agent toolbar, Claude launcher tile</title><link href="https://plmbr.dev/blog/v4-7-0/" rel="alternate" type="text/html" title="NBI 4.7.0 — Cell output actions, agent toolbar, Claude launcher tile" /><published>2026-05-07T19:00:00+00:00</published><updated>2026-05-07T19:00:00+00:00</updated><id>https://plmbr.dev/blog/v4-7-0</id><content type="html" xml:base="https://plmbr.dev/blog/v4-7-0/"><![CDATA[<p>NBI 4.7.0 is a feature-dense release. The headline is <strong>cell output actions</strong>: right-click any output to send it to the chat as structured context. Image attachments, streaming inline chat, and a Claude Code launcher tile all land alongside.</p>

<h2 id="cell-output-actions">Cell output actions</h2>

<p>Right-click a cell output — or hover for the new toolbar — to open the chat sidebar with the output already attached as context. Three actions ship:</p>

<ul>
  <li><strong>Explain</strong> — “what is this output telling me?”</li>
  <li><strong>Ask</strong> — open a chat with the output attached, ready for whatever follow-up you want.</li>
  <li><strong>Troubleshoot</strong> — for stack traces, error messages, or unexpected results.</li>
</ul>

<p>Outputs forward as structured MIME bundles, including images for vision-capable models. The attachment is token-bounded so a large output doesn’t overflow the context window.</p>

<p>Per-user toggles live in <code class="language-plaintext highlighter-rouge">config.json</code> (<code class="language-plaintext highlighter-rouge">enable_explain_error</code>, <code class="language-plaintext highlighter-rouge">enable_output_followup</code>, <code class="language-plaintext highlighter-rouge">enable_output_toolbar</code>, all default on). Admins can lock the feature via <code class="language-plaintext highlighter-rouge">NBI_EXPLAIN_ERROR_POLICY</code>, <code class="language-plaintext highlighter-rouge">NBI_OUTPUT_FOLLOWUP_POLICY</code>, and <code class="language-plaintext highlighter-rouge">NBI_OUTPUT_TOOLBAR_POLICY</code>.</p>

<h2 id="image-attachments-in-chat">Image attachments in chat</h2>

<p>Paste or attach images alongside a prompt. When the active model is vision-capable (Claude with vision enabled, GPT-4o, Gemini, etc.), the image is forwarded as model input — useful for screenshots, plots, and architecture diagrams.</p>

<h2 id="streaming-inline-chat">Streaming inline chat</h2>

<p>The inline chat popover (<kbd>Cmd</kbd>+<kbd>I</kbd> / <kbd>Ctrl</kbd>+<kbd>I</kbd>) now streams tokens as they arrive instead of waiting for the full response. Long answers feel responsive instead of stalled.</p>

<h2 id="notebook-scoped-generation">Notebook-scoped generation</h2>

<p>A sparkle icon on the active notebook’s toolbar opens a popover that scopes the generation to that notebook specifically. Useful when you have multiple notebooks open and want the agent to focus on the one in front.</p>

<h2 id="claude-code-launcher-tile">Claude Code launcher tile</h2>

<p>A Claude Code tile in the JupyterLab launcher opens a session picker: resume an existing transcript or start a new one in the file browser’s active subdirectory. Session IDs are copyable from the picker for sharing or for use with the <code class="language-plaintext highlighter-rouge">claude</code> CLI directly.</p>

<h2 id="repo-level-agentsmd">Repo-level <code class="language-plaintext highlighter-rouge">AGENTS.md</code></h2>

<p>When a project root contains an <code class="language-plaintext highlighter-rouge">AGENTS.md</code> file, NBI appends its contents under the system prompt’s “Additional Guidelines” section, alongside the existing ruleset injection. Useful for monorepos that already keep agent instructions there.</p>

<h2 id="claude-websocket-heartbeat">Claude WebSocket heartbeat</h2>

<p>Long-running Claude agent requests now stay alive through upstream proxy and load-balancer idle timeouts (JupyterHub’s nginx defaults to 60s) by sending a status heartbeat every 20s while a request is in flight. Fixes Bedrock-style request failures where processing exceeded the proxy idle window.</p>

<h2 id="extended-admin-policy-coverage">Extended admin policy coverage</h2>

<p>Every Settings panel toggle is now lockable via an environment variable. New boolean policies:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">NBI_CLAUDE_MODE_POLICY</code></li>
  <li><code class="language-plaintext highlighter-rouge">NBI_CLAUDE_CONTINUE_CONVERSATION_POLICY</code></li>
  <li><code class="language-plaintext highlighter-rouge">NBI_CLAUDE_CODE_TOOLS_POLICY</code></li>
  <li><code class="language-plaintext highlighter-rouge">NBI_CLAUDE_JUPYTER_UI_TOOLS_POLICY</code></li>
  <li><code class="language-plaintext highlighter-rouge">NBI_CLAUDE_SETTING_SOURCE_USER_POLICY</code></li>
  <li><code class="language-plaintext highlighter-rouge">NBI_CLAUDE_SETTING_SOURCE_PROJECT_POLICY</code></li>
  <li><code class="language-plaintext highlighter-rouge">NBI_STORE_GITHUB_ACCESS_TOKEN_POLICY</code></li>
</ul>

<p>New value-presence locks (set the env var to a value to pin it; the UI control becomes read-only):</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">NBI_CHAT_MODEL_PROVIDER</code>, <code class="language-plaintext highlighter-rouge">NBI_CHAT_MODEL_ID</code></li>
  <li><code class="language-plaintext highlighter-rouge">NBI_INLINE_COMPLETION_MODEL_PROVIDER</code>, <code class="language-plaintext highlighter-rouge">NBI_INLINE_COMPLETION_MODEL_ID</code></li>
  <li><code class="language-plaintext highlighter-rouge">NBI_CLAUDE_CHAT_MODEL</code>, <code class="language-plaintext highlighter-rouge">NBI_CLAUDE_INLINE_COMPLETION_MODEL</code></li>
  <li><code class="language-plaintext highlighter-rouge">ANTHROPIC_API_KEY</code>, <code class="language-plaintext highlighter-rouge">ANTHROPIC_BASE_URL</code></li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">/claude-sessions</code> HTTP route also accepts <code class="language-plaintext highlighter-rouge">?scope=cwd</code> to filter to sessions whose recorded <code class="language-plaintext highlighter-rouge">cwd</code> matches the lab’s working directory.</p>

<p>Full env-var reference: <a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/docs/admin-guide.md">docs/admin-guide.md</a>.</p>

<h2 id="internal">Internal</h2>

<ul>
  <li>Claude agent connection now happens in the background so JupyterLab finishes loading without waiting on the SDK handshake.</li>
  <li>CI now runs <code class="language-plaintext highlighter-rouge">pytest tests/</code> and <code class="language-plaintext highlighter-rouge">jlpm test</code> on every PR. Both build jobs declare <code class="language-plaintext highlighter-rouge">permissions: { contents: read }</code> so a compromised step can’t push.</li>
</ul>

<h2 id="notable-fixes">Notable fixes</h2>

<ul>
  <li><strong>API hygiene</strong> in <code class="language-plaintext highlighter-rouge">notebook_intelligence.api</code>: <code class="language-plaintext highlighter-rouge">raise NotImplemented</code> (which raised <code class="language-plaintext highlighter-rouge">TypeError</code>) is now <code class="language-plaintext highlighter-rouge">raise NotImplementedError</code>; five shared-default-argument cases were corrected; <code class="language-plaintext highlighter-rouge">Signal.disconnect</code> tolerates double-disconnect with a debug-level log; registrar methods raise a new <code class="language-plaintext highlighter-rouge">RegistrationError</code> instead of silently logging.</li>
  <li><strong>Claude headers</strong> (model + version) now sent on inline completion calls, matching the chat path.</li>
  <li><strong>OpenAI-compatible provider</strong> drops the unsupported <code class="language-plaintext highlighter-rouge">tool.strict</code> flag when targeting vLLM (#108).</li>
  <li><strong>Symlink-aware session lookup</strong> — resolves symlinks when locating Claude session transcripts, so <code class="language-plaintext highlighter-rouge">~/.claude/projects/</code> symlinked off another volume keeps working.</li>
  <li>Claude worker thread no longer crashes on cancellation.</li>
  <li>“Generating…” row no longer reflows the chat sidebar on narrow widths.</li>
  <li>Skills popup in the chat sidebar dismisses on click-outside or when the input is cleared.</li>
  <li>Spurious “Skills reloaded” notification at Claude-session launch is gone — the watcher now keys off a structural signature of bundle dirs plus <code class="language-plaintext highlighter-rouge">SKILL.md</code> mtimes.</li>
</ul>

<h2 id="install">Install</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">--upgrade</span> notebook-intelligence
</code></pre></div></div>

<p>Then restart JupyterLab. Full changes in the <a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/CHANGELOG.md#470--2026-05-07">CHANGELOG</a>.</p>]]></content><author><name>Mehmet Bektaş</name></author><summary type="html"><![CDATA[Right-click any cell output for Explain / Ask / Troubleshoot. Image attachments in chat. Claude Code launcher tile. Streaming inline-chat responses. Extended admin policy coverage.]]></summary></entry><entry><title type="html">NBI 4.6.0 — Skills management, restructured docs, Windows reliability</title><link href="https://plmbr.dev/blog/v4-6-0/" rel="alternate" type="text/html" title="NBI 4.6.0 — Skills management, restructured docs, Windows reliability" /><published>2026-04-29T19:00:00+00:00</published><updated>2026-04-29T19:00:00+00:00</updated><id>https://plmbr.dev/blog/v4-6-0</id><content type="html" xml:base="https://plmbr.dev/blog/v4-6-0/"><![CDATA[<p>NBI 4.6.0 ships a Claude Skills management panel, a documentation restructure that splits reference content out of the README, and a stack of Windows Claude-mode reliability fixes.</p>

<h2 id="claude-skills-management">Claude Skills management</h2>

<p>Settings now exposes a <strong>Skills</strong> tab for managing the bundles Claude can invoke (the <code class="language-plaintext highlighter-rouge">SKILL.md</code> frontmatter, helper files, and allowed tools). Skills resolve from the same two locations the Claude CLI reads:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">~/.claude/skills/</code> — user scope.</li>
  <li><code class="language-plaintext highlighter-rouge">&lt;project&gt;/.claude/skills/</code> — project scope.</li>
</ul>

<p>You can author Skills inline, duplicate and rename them, and delete with undo. The <strong>Import from GitHub</strong> dialog accepts a <code class="language-plaintext highlighter-rouge">github.com/owner/repo/tree/&lt;ref&gt;/&lt;path&gt;</code> URL pointing at a Skill bundle and installs it via the public tarball API.</p>

<p>For organization deployments, NBI can install and maintain a curated set from a YAML manifest:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">skills</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">https://github.com/your-org/data-skills/tree/main/skills/eda</span>
    <span class="na">scope</span><span class="pi">:</span> <span class="s">user</span>
  <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">https://github.com/your-org/data-skills/tree/main/skills/ml-recipes</span>
    <span class="na">scope</span><span class="pi">:</span> <span class="s">user</span>
</code></pre></div></div>

<p>Point <code class="language-plaintext highlighter-rouge">NBI_SKILLS_MANIFEST</code> at the manifest’s URL or filesystem path. NBI’s reconciler installs every Skill at startup and re-syncs every 24h (<code class="language-plaintext highlighter-rouge">NBI_SKILLS_MANIFEST_INTERVAL</code> overrides). Managed Skills are read-only in the UI — users can’t edit, rename, or delete them, and the reconciler restores any that disappear from disk.</p>

<p>Full reference: <a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/docs/skills.md">docs/skills.md</a>.</p>

<h2 id="restructured-documentation">Restructured documentation</h2>

<p>The README has been rewritten with a table of contents and a concept glossary. Reference content moved out of the README and into focused docs:</p>

<ul>
  <li><a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/SECURITY.md"><code class="language-plaintext highlighter-rouge">SECURITY.md</code></a> — private disclosure, threat model, supply-chain posture.</li>
  <li><a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/PRIVACY.md"><code class="language-plaintext highlighter-rouge">PRIVACY.md</code></a> — what NBI sends where.</li>
  <li><a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/docs/admin-guide.md"><code class="language-plaintext highlighter-rouge">docs/admin-guide.md</code></a> — every <code class="language-plaintext highlighter-rouge">NBI_*</code> env var and policy.</li>
  <li><a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/docs/rulesets.md"><code class="language-plaintext highlighter-rouge">docs/rulesets.md</code></a> — markdown-based prompt injection.</li>
  <li><a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/docs/skills.md"><code class="language-plaintext highlighter-rouge">docs/skills.md</code></a> — Skills authoring, importing, and the manifest reconciler.</li>
  <li><a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/docs/troubleshooting.md"><code class="language-plaintext highlighter-rouge">docs/troubleshooting.md</code></a> — common issues with copy-pasteable fixes.</li>
</ul>

<p>If you’ve been linking deep into the README from internal wikis, the new home for each section is documented in the <a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/README.md">migration table at the top of the README</a>.</p>

<h2 id="windows-claude-mode-reliability">Windows Claude mode reliability</h2>

<p>A cluster of fixes for Claude mode on Windows:</p>

<ul>
  <li>The Claude agent thread now uses the Proactor event loop on Windows, which fixes subprocess spawn failures and intermittent “Claude agent not connected” races at startup.</li>
  <li>The Claude SDK retry path reconnects when the worker thread has died instead of waiting out the full response timeout.</li>
  <li>Anthropic credentials are normalized (whitespace + scheme handling) before being passed to the SDK, so a stray newline in a <code class="language-plaintext highlighter-rouge">.env</code> file no longer breaks auth.</li>
</ul>

<h2 id="security-hardening">Security hardening</h2>

<p>Skill imports from GitHub reject tarball entries containing absolute paths or <code class="language-plaintext highlighter-rouge">../</code> traversal — a malicious or buggy bundle can no longer write outside its install directory.</p>

<h2 id="other-fixes">Other fixes</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">_send_claude_agent_request</code> guards against a disconnect race that left chat handlers waiting on a closed queue.</li>
  <li>WebSocket message handlers are disconnected when the originating request finishes; previously they accumulated for the lifetime of the WebSocket.</li>
  <li><code class="language-plaintext highlighter-rouge">configChanged</code> handlers are disconnected when components unmount, fixing a slow leak when the chat sidebar was opened and closed repeatedly.</li>
  <li>The Claude session picker scrolls correctly when the transcript count exceeds the visible area.</li>
</ul>

<h2 id="install">Install</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">--upgrade</span> notebook-intelligence
</code></pre></div></div>

<p>Then restart JupyterLab. Full changes in the <a href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/CHANGELOG.md#460--2026-04-29">CHANGELOG</a>.</p>]]></content><author><name>Mehmet Bektaş</name></author><summary type="html"><![CDATA[Claude Skills management panel, a documentation restructure, and a stack of Windows Claude-mode reliability fixes.]]></summary></entry><entry><title type="html">Notebook Intelligence now supports any LLM Provider and AI Model</title><link href="https://plmbr.dev/blog/archive/support-for-any-llm-provider/" rel="alternate" type="text/html" title="Notebook Intelligence now supports any LLM Provider and AI Model" /><published>2025-03-05T09:00:00+00:00</published><updated>2025-03-05T09:00:00+00:00</updated><id>https://plmbr.dev/blog/archive/support-for-any-llm-provider</id><content type="html" xml:base="https://plmbr.dev/blog/archive/support-for-any-llm-provider/"><![CDATA[<p><a href="https://github.com/notebook-intelligence/notebook-intelligence">Notebook Intelligence</a> (NBI) is an AI coding assistant and extensible AI framework for JupyterLab. (<em>For an introduction to NBI see <a href="/blog/archive/introducing-notebook-intelligence/">Introducing Notebook Intelligence</a> and for basics of extending NBI see <a href="/blog/archive/building-ai-extensions-for-jupyterlab/">Building AI Extensions for JupyterLab</a> blog posts.</em>)</p>

<p>Notebook Intelligence now supports any LLM Provider and compatible model for chat and auto-complete. Chat model is used for Copilot Chat in the sidebar and inline chat popups that are accessible from notebook and file editors. Auto-complete model is used for providing completion suggestions as you type in a notebook or file editor (as ghost text). Your chat model and auto-complete model don’t have to be from the same provider for use with NBI.</p>

<p>GitHub Copilot is still the recommended and the default model provider. Now, you can choose which model to use from the options provided by GitHub Copilot service.</p>

<p><img src="/notebook-intelligence/assets/images/llm-providers/llm-provider-model.gif" alt="which AI model prompt" width="600" /></p>

<h2 id="notebook-intelligence-settings-dialog">Notebook Intelligence Settings Dialog</h2>

<p>You can configure the model provider and model options using the Notebook Intelligence Settings dialog. You can access this dialog from JupyterLab Settings menu -&gt; <code class="language-plaintext highlighter-rouge">Notebook Intelligence Settings</code>, using <code class="language-plaintext highlighter-rouge">/settings</code> command in Copilot Chat or by using the command palette.</p>

<p><img src="/notebook-intelligence/assets/images/llm-providers/provider-list.png" alt="LLM provider list" width="600" /></p>

<p>Your settings are stored as a file on the disk at location <code class="language-plaintext highlighter-rouge">~/.jupyter/nbi-config.json</code>. Saved data includes your API keys for OpenAI and LiteLLM compatible providers you choose. Removing this file would reset the LLM provider to GitHub Copilot.</p>

<h2 id="github-copilot-model-options">GitHub Copilot Model Options</h2>

<p>GitHub Copilot provides multiple model options both for chat and auto-complete. You can now specify the models to use. The default chat model has been <code class="language-plaintext highlighter-rouge">GPT-4o</code> and auto-complete model <code class="language-plaintext highlighter-rouge">copilot-codex</code>. Chat model options available are: <code class="language-plaintext highlighter-rouge">GPT-4o</code>, <code class="language-plaintext highlighter-rouge">o3-mini</code>, <code class="language-plaintext highlighter-rouge">Claude 3.5 Sonnet</code>, <code class="language-plaintext highlighter-rouge">Claude 3.7 Sonnet</code>. Auto-complete model options are: <code class="language-plaintext highlighter-rouge">copilot-codex</code> and <code class="language-plaintext highlighter-rouge">gpt-4o-copilot</code>.</p>

<p><img src="/notebook-intelligence/assets/images/llm-providers/github-copilot-models.png" alt="GitHub Copilot provider" width="600" /></p>

<h1 id="openai-compatible-model-provider">OpenAI Compatible Model Provider</h1>

<p>If you have an OpenAI subscription or any other OpenAI compatible LLM provider such as OpenRouter then you can use the <code class="language-plaintext highlighter-rouge">OpenAI Compatible</code> provider option. Enter your API key, model ID (e.g. gpt-4o) and service base URL in the settings dialog. You can leave Base URL blank if you are using OpenAI as the service provider.</p>

<p>Not all models support auto-complete (insertion). If you have an OpenAI subscription, you can use the model <code class="language-plaintext highlighter-rouge">gpt-3.5-turbo-instruct</code> for auto-complete.</p>

<p><img src="/notebook-intelligence/assets/images/llm-providers/openai-compatible-provider.png" alt="OpenAI compatible provider" width="600" /></p>

<h1 id="litellm-compatible-model-provider">LiteLLM Compatible Model Provider</h1>

<p>You can choose <code class="language-plaintext highlighter-rouge">LiteLLM Compatible</code> provider option for any provider that is not compatible with OpenAI APIs. LiteLLM supports a wide range of providers. Please check <a href="https://docs.litellm.ai/docs/providers" target="_blank">LiteLLM documentation</a> for the list of providers and models. Enter your API key, model ID (e.g. anthropic/claude-3.5) and service base URL in the settings dialog for the provider and model you would like to use.</p>

<p>Use a model that supports auto-complete (insertion).</p>

<p><img src="/notebook-intelligence/assets/images/llm-providers/litellm-compatible-provider.png" alt="OpenAI compatible provider" width="600" /></p>

<h2 id="anthropic">Anthropic</h2>

<p>If you have an Anthropic subscription then you can use <code class="language-plaintext highlighter-rouge">LiteLLM Compatible</code> provider.</p>

<h2 id="use-local-models-with-ollama">Use local models with Ollama</h2>

<p>NBI supports Ollama as provider for local chat and auto-complete models. Any Ollama chat model can be used with NBI and they will be automatically listed in the settings dialog. For auto-complete, NBI supports a selected list of models which were tested to work well to support completions. Auto-complete models supported are: <code class="language-plaintext highlighter-rouge">deepseek-coder-v2</code>, <code class="language-plaintext highlighter-rouge">qwen2.5-coder</code>, <code class="language-plaintext highlighter-rouge">codestral</code>, <code class="language-plaintext highlighter-rouge">starcoder2</code>, <code class="language-plaintext highlighter-rouge">codellama:7b-code</code>. Make sure you have these models installed before trying to use with NBI.</p>

<p><img src="/notebook-intelligence/assets/images/llm-providers/ollama-provider.png" alt="Ollama provider" width="600" /></p>

<h2 id="add-your-custom-llm-provider-by-creating-an-nbi-extension">Add your custom LLM Provider by creating an NBI Extension</h2>

<p>If you work with LLM providers which don’t fit in the NBI supported provider types then you can build an extension to add support for those. You can introduce new LLM providers, chat models and auto-complete models using the NBI extension API. Refer to NBI <code class="language-plaintext highlighter-rouge">OllamaLLMProvider</code> class for an example.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">LLMProviderExtension</span><span class="p">(</span><span class="n">NotebookIntelligenceExtension</span><span class="p">):</span>
    <span class="p">...</span>

    <span class="k">def</span> <span class="nf">activate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">host</span><span class="p">:</span> <span class="n">Host</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">llm_provider</span> <span class="o">=</span> <span class="n">CustomLLMProvider</span><span class="p">()</span>
        <span class="n">host</span><span class="p">.</span><span class="n">register_llm_provider</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">llm_provider</span><span class="p">)</span>
        <span class="n">log</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">"Custom LLM Provider extension activated"</span><span class="p">)</span>
</code></pre></div></div>

<p>For building NBI extensions see <a href="/blog/archive/building-ai-extensions-for-jupyterlab/">Building AI Extensions for JupyterLab</a> blog post.</p>

<h2 id="try-it-out-and-share-your-feedback">Try it out and share your feedback!</h2>

<p>Please try the LLM provider and model options and share your feedback using project’s <a href="https://github.com/notebook-intelligence/notebook-intelligence/issues">GitHub issues</a>! User feedback from the community will shape the project’s roadmap.</p>

<h2 id="about-theauthor">About the Author</h2>

<p><a href="https://www.linkedin.com/in/mehmet-bektas">Mehmet Bektas</a> 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.</p>]]></content><author><name>Mehmet Bektaş</name></author><summary type="html"><![CDATA[Notebook Intelligence (NBI) is an AI coding assistant and extensible AI framework for JupyterLab. (For an introduction to NBI see Introducing Notebook Intelligence and for basics of extending NBI see Building AI Extensions for JupyterLab blog posts.)]]></summary></entry><entry><title type="html">Building AI Agents for JupyterLab</title><link href="https://plmbr.dev/blog/archive/building-ai-agents-for-jupyterlab/" rel="alternate" type="text/html" title="Building AI Agents for JupyterLab" /><published>2025-02-09T09:00:00+00:00</published><updated>2025-02-09T09:00:00+00:00</updated><id>https://plmbr.dev/blog/archive/building-ai-agents-for-jupyterlab</id><content type="html" xml:base="https://plmbr.dev/blog/archive/building-ai-agents-for-jupyterlab/"><![CDATA[<p><a href="https://github.com/notebook-intelligence/notebook-intelligence">Notebook Intelligence</a> (NBI) is an AI coding assistant and extensible AI framework for JupyterLab. (<em>For an introduction to NBI see <a href="/blog/archive/introducing-notebook-intelligence/">Introducing Notebook Intelligence</a> and for basics of extending NBI see <a href="/blog/archive/building-ai-extensions-for-jupyterlab/">Building AI Extensions for JupyterLab</a> blog posts.</em>)</p>

<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/ai-agent-blog/nb-ai-agent-demo.gif" alt="AI Agent demo" /></p>

<h2 id="what-is-tool-calling-and-an-ai-agent">What is tool calling and an AI Agent?</h2>

<p><strong>Tool calling</strong> 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.</p>

<p><strong>AI Agents</strong> 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.</p>

<h2 id="ai-agent-extension-example">AI Agent Extension Example</h2>

<p>Let’s build an AI Agent for JupyterLab using Notebook Intelligence extension APIs. (<em>The full source code for this extension is <a href="https://github.com/notebook-intelligence/nbi-ai-agent-example" target="_blank">available here</a>.</em>) This will be an AI agent for map creation and notebook sharing. It will have the following capabilities:</p>
<ul>
  <li>Looking up geo-coordinates for an address</li>
  <li>Showing maps centered at an address in the Copilot Chat UI</li>
  <li>Creating notebooks that show maps centered at specified addresses</li>
  <li>Sharing notebooks publicly using <a href="https://notebooksharing.space" target="_blank">notebooksharing.space</a></li>
</ul>

<p>The tasks above will be run by the AI Agent in response to natural language prompts by the user.</p>

<p>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 <code class="language-plaintext highlighter-rouge">Tool</code> abstract class. A tool needs to implement the methods and properties defined in this base class.</p>

<p>Tool class provides the metadata information for the tool and implements the <code class="language-plaintext highlighter-rouge">pre_invoke</code> and <code class="language-plaintext highlighter-rouge">handle_tool_call</code> methods. <code class="language-plaintext highlighter-rouge">pre_invoke</code> method is called right before <code class="language-plaintext highlighter-rouge">handle_tool_call</code> with the tool arguments and it gives an opportunity for the tool to prompt for confirmation of the tool execution.</p>

<p><code class="language-plaintext highlighter-rouge">schema</code> 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 <code class="language-plaintext highlighter-rouge">handle_tool_call</code> method call.</p>

<h3 id="geo-coordinates-lookup-tool">Geo Coordinates Lookup Tool</h3>

<p>This tool looks up geo-coordinates for an address using <a href="https://github.com/geopy/geopy" target="_blank">Nominatim</a> library. <code class="language-plaintext highlighter-rouge">pre_invoke</code> method for this tool only shows a message in Chat UI before looking up for the geo-coordinates in <code class="language-plaintext highlighter-rouge">handle_tool_call</code> method.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">GeoCoordinateLookupTool</span><span class="p">(</span><span class="n">Tool</span><span class="p">):</span>
    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">name</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"geo_coordinate_lookup"</span>

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

    <span class="k">def</span> <span class="nf">pre_invoke</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">tool_args</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Union</span><span class="p">[</span><span class="n">ToolPreInvokeResponse</span><span class="p">,</span> <span class="bp">None</span><span class="p">]:</span>
        <span class="n">address</span> <span class="o">=</span> <span class="n">tool_args</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'address'</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">ToolPreInvokeResponse</span><span class="p">(</span>
            <span class="n">message</span><span class="o">=</span><span class="sa">f</span><span class="s">"Getting coordinates for '</span><span class="si">{</span><span class="n">address</span><span class="si">}</span><span class="s">'"</span>
        <span class="p">)</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">handle_tool_call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">response</span><span class="p">:</span> <span class="n">ChatResponse</span><span class="p">,</span> <span class="n">tool_context</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">tool_args</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
        <span class="n">address</span> <span class="o">=</span> <span class="n">tool_args</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'address'</span><span class="p">)</span>
        <span class="n">location</span> <span class="o">=</span> <span class="n">geolocator</span><span class="p">.</span><span class="n">geocode</span><span class="p">(</span><span class="n">address</span><span class="p">)</span>
        <span class="k">return</span> <span class="p">{</span><span class="s">"latitude"</span><span class="p">:</span> <span class="n">location</span><span class="p">.</span><span class="n">latitude</span><span class="p">,</span> <span class="s">"longitude"</span><span class="p">:</span> <span class="n">location</span><span class="p">.</span><span class="n">longitude</span><span class="p">}</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/ai-agent-blog/ai-agent-geo-coords.png" alt="Geo coordinates lookup tool" /></p>

<h3 id="map-response-generator-tool">Map Response Generator Tool</h3>

<p>This tool shows a map in Copilot Chat UI centered at geo-coordinates. In <code class="language-plaintext highlighter-rouge">pre_invoke</code> method this method only shows a notification message in Chat UI. In <code class="language-plaintext highlighter-rouge">handle_tool_call</code> method, this tool returns a <code class="language-plaintext highlighter-rouge">HTMLFrame</code> response that uses HTML to show a map centered at the requested location using Google Maps.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MapResponseGeneratorTool</span><span class="p">(</span><span class="n">Tool</span><span class="p">):</span>
    <span class="p">...</span>

    <span class="k">def</span> <span class="nf">pre_invoke</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">tool_args</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Union</span><span class="p">[</span><span class="n">ToolPreInvokeResponse</span><span class="p">,</span> <span class="bp">None</span><span class="p">]:</span>
        <span class="n">geo_coordinates</span> <span class="o">=</span> <span class="n">tool_args</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'geo_coordinates'</span><span class="p">)</span>
        <span class="n">latitude</span> <span class="o">=</span> <span class="n">geo_coordinates</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'latitude'</span><span class="p">)</span>
        <span class="n">longitude</span> <span class="o">=</span> <span class="n">geo_coordinates</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'longitude'</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">ToolPreInvokeResponse</span><span class="p">(</span>
            <span class="n">message</span><span class="o">=</span><span class="sa">f</span><span class="s">"Showing a map centered at latitude: </span><span class="si">{</span><span class="n">latitude</span><span class="si">}</span><span class="s"> and longitude: </span><span class="si">{</span><span class="n">longitude</span><span class="si">}</span><span class="s">"</span>
        <span class="p">)</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">handle_tool_call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">response</span><span class="p">:</span> <span class="n">ChatResponse</span><span class="p">,</span> <span class="n">tool_context</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">tool_args</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
        <span class="n">geo_coordinates</span> <span class="o">=</span> <span class="n">tool_args</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'geo_coordinates'</span><span class="p">)</span>
        <span class="n">latitude</span> <span class="o">=</span> <span class="n">geo_coordinates</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'latitude'</span><span class="p">)</span>
        <span class="n">longitude</span> <span class="o">=</span> <span class="n">geo_coordinates</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'longitude'</span><span class="p">)</span>

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

        <span class="k">return</span> <span class="p">{</span><span class="s">"result"</span><span class="p">:</span> <span class="s">"I showed the map"</span><span class="p">}</span>
</code></pre></div></div>

<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/ai-agent-blog/ai-agent-show-map.png" alt="Show map response tool" /></p>

<h3 id="map-notebook-creator-tool">Map Notebook Creator Tool</h3>

<p>This tool creates a notebook centered at the specified geo-coordinates. In <code class="language-plaintext highlighter-rouge">pre_invoke</code> method this method only shows a notification message in Chat UI. In <code class="language-plaintext highlighter-rouge">handle_tool_call</code> method, the tool creates a notebook using <code class="language-plaintext highlighter-rouge">nbformat</code> library, saves it to disk and then opens the notebook in JupyterLab UI using the <code class="language-plaintext highlighter-rouge">response.run_ui_command</code> NBI method.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MapNotebookCreatorTool</span><span class="p">(</span><span class="n">Tool</span><span class="p">):</span>
    <span class="p">...</span>

    <span class="k">def</span> <span class="nf">pre_invoke</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">tool_args</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Union</span><span class="p">[</span><span class="n">ToolPreInvokeResponse</span><span class="p">,</span> <span class="bp">None</span><span class="p">]:</span>
        <span class="n">geo_coordinates</span> <span class="o">=</span> <span class="n">tool_args</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'geo_coordinates'</span><span class="p">)</span>
        <span class="n">latitude</span> <span class="o">=</span> <span class="n">geo_coordinates</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'latitude'</span><span class="p">)</span>
        <span class="n">longitude</span> <span class="o">=</span> <span class="n">geo_coordinates</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'longitude'</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">ToolPreInvokeResponse</span><span class="p">(</span>
            <span class="n">message</span><span class="o">=</span><span class="sa">f</span><span class="s">"Creating a map notebook for latitude: </span><span class="si">{</span><span class="n">latitude</span><span class="si">}</span><span class="s"> and longitude: </span><span class="si">{</span><span class="n">longitude</span><span class="si">}</span><span class="s">"</span>
        <span class="p">)</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">handle_tool_call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">response</span><span class="p">:</span> <span class="n">ChatResponse</span><span class="p">,</span> <span class="n">tool_context</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">tool_args</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
        <span class="n">geo_coordinates</span> <span class="o">=</span> <span class="n">tool_args</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'geo_coordinates'</span><span class="p">)</span>
        <span class="n">latitude</span> <span class="o">=</span> <span class="n">geo_coordinates</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'latitude'</span><span class="p">)</span>
        <span class="n">longitude</span> <span class="o">=</span> <span class="n">geo_coordinates</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'longitude'</span><span class="p">)</span>

        <span class="n">now</span> <span class="o">=</span> <span class="n">dt</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">()</span>
        <span class="n">map_file_name</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"map_</span><span class="si">{</span><span class="n">now</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">'%Y%m%d_%H%M%S'</span><span class="p">)</span><span class="si">}</span><span class="s">.ipynb"</span>

        <span class="n">nb</span> <span class="o">=</span> <span class="n">nbf</span><span class="p">.</span><span class="n">v4</span><span class="p">.</span><span class="n">new_notebook</span><span class="p">()</span>
        <span class="n">header</span> <span class="o">=</span> <span class="s">"""</span><span class="se">\
</span><span class="s">        ### This map notebook was created by an AI Agent using [Notebook Intelligence](https://github.com/notebook-intelligence)
        """</span>

        <span class="n">install_code_cell</span> <span class="o">=</span> <span class="s">"%%capture</span><span class="se">\n</span><span class="s">%pip install folium"</span>

        <span class="n">map_code_cell</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"""</span><span class="se">\
</span><span class="s">        import folium

        map = folium.Map(location=[</span><span class="si">{</span><span class="n">latitude</span><span class="si">}</span><span class="s">, </span><span class="si">{</span><span class="n">longitude</span><span class="si">}</span><span class="s">], zoom_start=13)
        map"""</span>

        <span class="n">nb</span><span class="p">[</span><span class="s">'cells'</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
            <span class="n">nbf</span><span class="p">.</span><span class="n">v4</span><span class="p">.</span><span class="n">new_markdown_cell</span><span class="p">(</span><span class="n">header</span><span class="p">),</span>
            <span class="n">nbf</span><span class="p">.</span><span class="n">v4</span><span class="p">.</span><span class="n">new_code_cell</span><span class="p">(</span><span class="n">install_code_cell</span><span class="p">),</span>
            <span class="n">nbf</span><span class="p">.</span><span class="n">v4</span><span class="p">.</span><span class="n">new_code_cell</span><span class="p">(</span><span class="n">map_code_cell</span><span class="p">)</span>
        <span class="p">]</span>
        <span class="n">nb</span><span class="p">.</span><span class="n">metadata</span><span class="p">[</span><span class="s">"kernelspec"</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"name"</span><span class="p">:</span> <span class="s">"python3"</span><span class="p">}</span>
        <span class="n">nbf</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">nb</span><span class="p">,</span> <span class="n">map_file_name</span><span class="p">)</span>

        <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">run_ui_command</span><span class="p">(</span><span class="s">"docmanager:open"</span><span class="p">,</span> <span class="p">{</span><span class="s">"path"</span><span class="p">:</span> <span class="n">map_file_name</span><span class="p">})</span>

        <span class="k">return</span> <span class="p">{</span><span class="s">"result"</span><span class="p">:</span> <span class="s">"I created and opened the map notebook"</span><span class="p">}</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/ai-agent-blog/ai-agent-create-notebook.png" alt="Create map notebook tool" /></p>

<h3 id="notebook-share-tool">Notebook Share Tool</h3>

<p>This tool shares a notebook publicly by uploading it to <a href="https://notebooksharing.space" target="_blank">notebooksharing.space</a> and displays the link to the shared notebook.</p>

<p>In the <code class="language-plaintext highlighter-rouge">pre_invoke</code> method implementation, this tool asks for confirmation first as this operation is an undoable share of the notebook publicly. Only after the user confirms, <code class="language-plaintext highlighter-rouge">handle_tool_call</code> is executed. In <code class="language-plaintext highlighter-rouge">handle_tool_call</code> method the tool uploads the notebook at the <code class="language-plaintext highlighter-rouge">notebook_file_path</code> using <a href="https://github.com/notebook-sharing-space/nbss-upload" target="_blank">nbss_upload</a> library and then shows the link to the shared notebook on <a href="https://notebooksharing.space" target="_blank">notebooksharing.space</a>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">NotebookShareTool</span><span class="p">(</span><span class="n">Tool</span><span class="p">):</span>
    <span class="p">...</span>
    
    <span class="k">def</span> <span class="nf">pre_invoke</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">tool_args</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Union</span><span class="p">[</span><span class="n">ToolPreInvokeResponse</span><span class="p">,</span> <span class="bp">None</span><span class="p">]:</span>
        <span class="n">file_path</span> <span class="o">=</span> <span class="n">tool_args</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'notebook_file_path'</span><span class="p">)</span>
        <span class="n">file_name</span> <span class="o">=</span> <span class="n">path</span><span class="p">.</span><span class="n">basename</span><span class="p">(</span><span class="n">file_path</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">ToolPreInvokeResponse</span><span class="p">(</span>
            <span class="n">message</span><span class="o">=</span><span class="sa">f</span><span class="s">"Sharing notebook '</span><span class="si">{</span><span class="n">file_name</span><span class="si">}</span><span class="s">'"</span><span class="p">,</span>
            <span class="n">confirmationTitle</span><span class="o">=</span><span class="s">"Confirm sharing"</span><span class="p">,</span>
            <span class="n">confirmationMessage</span><span class="o">=</span><span class="sa">f</span><span class="s">"Are you sure you want to share the notebook at '</span><span class="si">{</span><span class="n">file_path</span><span class="si">}</span><span class="s">'? This will upload the notebook to public internet and cannot be undone."</span>
        <span class="p">)</span>

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

        <span class="k">return</span> <span class="p">{</span><span class="s">"result"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"Notebook '</span><span class="si">{</span><span class="n">file_name</span><span class="si">}</span><span class="s">' has been shared at: </span><span class="si">{</span><span class="n">share_url</span><span class="si">}</span><span class="s">"</span><span class="p">}</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/ai-agent-blog/ai-agent-share.gif" alt="Share notebook publicly" /></p>

<h2 id="tool-call-schema-definitions">Tool call schema definitions</h2>

<p>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.</p>

<p>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.</p>

<h2 id="tool-chaining">Tool chaining</h2>

<p>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.</p>

<p>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.</p>

<h2 id="chat-participant">Chat Participant</h2>

<p>In NBI AI framework, AI Agents are defined as chat participants and tools are tied to specific chat participants. For our extension we create <code class="language-plaintext highlighter-rouge">AIAgentChatParticipant</code> as our participant (for more details on NBI extensions and chat participants see <a href="/blog/archive/building-ai-extensions-for-jupyterlab/">this blog</a>). Our chat participant returns list of tools it defines from the <code class="language-plaintext highlighter-rouge">tools</code> property.</p>

<p>In <code class="language-plaintext highlighter-rouge">handle_chat_request</code> method our chat participant passes the request to the base <code class="language-plaintext highlighter-rouge">ChatParticipant</code> class to handle tool calling for us.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AIAgentChatParticipant</span><span class="p">(</span><span class="n">ChatParticipant</span><span class="p">):</span>
    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">id</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"ai-agent"</span>
    <span class="p">...</span>
    
    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">tools</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="n">Tool</span><span class="p">]:</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">GeoCoordinateLookupTool</span><span class="p">(),</span> <span class="n">MapResponseGeneratorTool</span><span class="p">(),</span> <span class="n">MapNotebookCreatorTool</span><span class="p">(),</span> <span class="n">NotebookShareTool</span><span class="p">()]</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">handle_chat_request</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">response</span><span class="p">:</span> <span class="n">ChatResponse</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="p">...</span>
        <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">handle_chat_request_with_tools</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="nbi-extension">NBI Extension</h2>

<p>Finally we create our NBI extension class <code class="language-plaintext highlighter-rouge">AIAgentExtension</code>. This class basically registers our chat participant to NBI on extension activation.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AIAgentExtension</span><span class="p">(</span><span class="n">NotebookIntelligenceExtension</span><span class="p">):</span>
    <span class="p">...</span>

    <span class="k">def</span> <span class="nf">activate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">host</span><span class="p">:</span> <span class="n">Host</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">participant</span> <span class="o">=</span> <span class="n">AIAgentChatParticipant</span><span class="p">(</span><span class="n">host</span><span class="p">)</span>
        <span class="n">host</span><span class="p">.</span><span class="n">register_chat_participant</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">participant</span><span class="p">)</span>
        <span class="n">log</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">"AI Agent example extension activated"</span><span class="p">)</span>
</code></pre></div></div>

<p>That is all there is to create an AI Agent for JupyterLab using Notebook Intelligence. The <a href="https://github.com/notebook-intelligence/nbi-ai-agent-example">full source code</a> for this example is available along with installation instructions for you to use as a reference and/or build on top.</p>

<h2 id="try-it-out-and-share-your-feedback">Try it out and share your feedback!</h2>

<p>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 <a href="https://github.com/notebook-intelligence/notebook-intelligence/issues">GitHub issues</a>! User feedback from the community will shape the project’s roadmap.</p>

<h2 id="about-theauthor">About the Author</h2>

<p><a href="https://www.linkedin.com/in/mehmet-bektas">Mehmet Bektas</a> 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.</p>

<p><em>The source code for the example extension in this post is available <a href="https://github.com/notebook-intelligence/nbi-ai-agent-example">on GitHub</a>.</em></p>]]></content><author><name>Mehmet Bektaş</name></author><summary type="html"><![CDATA[Notebook Intelligence (NBI) is an AI coding assistant and extensible AI framework for JupyterLab. (For an introduction to NBI see Introducing Notebook Intelligence and for basics of extending NBI see Building AI Extensions for JupyterLab blog posts.)]]></summary></entry><entry><title type="html">Building AI Extensions for JupyterLab</title><link href="https://plmbr.dev/blog/archive/building-ai-extensions-for-jupyterlab/" rel="alternate" type="text/html" title="Building AI Extensions for JupyterLab" /><published>2025-02-05T02:49:32+00:00</published><updated>2025-02-05T02:49:32+00:00</updated><id>https://plmbr.dev/blog/archive/building-ai-extensions-for-jupyterlab</id><content type="html" xml:base="https://plmbr.dev/blog/archive/building-ai-extensions-for-jupyterlab/"><![CDATA[<p><a href="https://github.com/notebook-intelligence/notebook-intelligence">Notebook Intelligence</a> (NBI) is an AI coding assistant and extensible AI framework for JupyterLab. For an introduction to NBI see <a href="/blog/archive/introducing-notebook-intelligence/">Introducing Notebook Intelligence blog post</a> first.</p>

<p>GitHub Copilot and other AI coding assistants generate chat responses based on publicly available knowledge and they do not have access to your workspace, tools and services. NBI provides Extension APIs to build AI extensions for JupyterLab. By extending NBI, you can build custom chat interactions and provide access to proprietary or external data, tools and services. This lets you build custom, AI powered chat experiences, natural language interface to JupyterLab and your tools.</p>

<h2 id="nbi-extensions-overview">NBI Extensions Overview</h2>

<p>NBI can be extended by building an NBI extension using Python. Extensions are distributed as Python packages and they need to be installed on the same Python environment as the NBI and JupyterLab. Once installed they add a <code class="language-plaintext highlighter-rouge">extension.json</code> file with the extension metadata so that NBI can load the extension during initialization. This metadata file needs to be installed into <code class="language-plaintext highlighter-rouge">&lt;python-env&gt;/share/jupyter/nbi_extensions/&lt;extension-name&gt;/extension.json</code>.</p>

<p>extension.json file contains the extension’s class name.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"class"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;extension-name&gt;.&lt;ExtensionClass&gt;"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span></code></pre></div></div>

<p>NBI extensions are executed in the backend as part of Jupyter server process. NBI instantiates an instance of each NBI extension during Jupyter server launch and interacts with the extensions as needed.</p>

<p>Extensions can add custom chat participants and commands specific to that participant.</p>

<p><img src="/notebook-intelligence/assets/images/response-types-demo.gif" alt="Extension demo" /></p>

<h3 id="ui-access-by-extensions">UI Access by extensions</h3>

<p>NBI extensions can show UI components on the chat interface as part of the chat responses they generate. Extensions can also trigger JupyterLab UI commands and listen for the command responses.</p>

<h3 id="anatomy-of-a-chat-request">Anatomy of a chat request</h3>

<p>A chat request input in Copilot Chat is made up of three parts and in the order: participant, command and prompt</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@&lt;participant-id&gt; /&lt;command-name&gt; &lt;prompt&gt;
</code></pre></div></div>
<p>Example: <code class="language-plaintext highlighter-rouge">@example /repeat Hello world!</code></p>

<p>Participants and commands are defined by the Copilot Chat or NBI extensions. They are automatically listed on the chat auto-complete list.</p>

<p>If no participant is specified then the request is handled by the default participant which is GitHub Copilot. Commands are specific to a participant type. NBI parses the user input and routes the requests to user specified participants in the prompt.</p>

<h2 id="extension-example">Extension Example</h2>

<p>Let’s walk through the NBI extensibility features with an example extension. The full source code for this extension is <a href="https://github.com/notebook-intelligence/nbi-extension-example" target="_blank">available here</a>.</p>

<h3 id="extension-class">Extension class</h3>

<p>Extensions are defined as a class derived from  <code class="language-plaintext highlighter-rouge">NotebookIntelligenceExtension</code> which is an abstract class and an extension needs to implement the methods and properties defined in this base class. Extension class provides the metadata information on the extension and implements the <code class="language-plaintext highlighter-rouge">activate</code> method. <code class="language-plaintext highlighter-rouge">activate</code> method is called when NBI initializes the extension.</p>

<p><code class="language-plaintext highlighter-rouge">activate</code> method is called with a <code class="language-plaintext highlighter-rouge">host</code> parameter which is the extension host provided by NBI. Using the host methods, you can register your chat participants.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ExampleExtension</span><span class="p">(</span><span class="n">NotebookIntelligenceExtension</span><span class="p">):</span>
    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">id</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"example-extension"</span>

    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">name</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"Example Extension"</span>

    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">provider</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"Mehmet Bektas"</span>

    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">url</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"https://github.com/mbektas"</span>

    <span class="k">def</span> <span class="nf">activate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">host</span><span class="p">:</span> <span class="n">Host</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">participant</span> <span class="o">=</span> <span class="n">ExampleChatParticipant</span><span class="p">(</span><span class="n">host</span><span class="p">)</span>
        <span class="n">host</span><span class="p">.</span><span class="n">register_chat_participant</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">participant</span><span class="p">)</span>
        <span class="n">log</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">"Example extension activated"</span><span class="p">)</span>

</code></pre></div></div>

<h3 id="chat-participant">Chat Participant</h3>

<p>NBI lists all available chat participants with their commands in the prompt auto-complete list. Chat participants are identified by <code class="language-plaintext highlighter-rouge">@participant-id</code>. If a chat prompt starts with <code class="language-plaintext highlighter-rouge">@participant-id</code> the request is routed to the particular chat participant with that id.</p>

<p><img src="/notebook-intelligence/assets/images/chat-participants.png" alt="Chat participants" width="400" /></p>

<p>Extensions can define chat participants to handle the chat requests. Chat participants can define commands. Commands start with <code class="language-plaintext highlighter-rouge">/</code> and they can be designed to work with or without a prompt. It lets the chat participant developer have more control over the prompt handling. Commands defined by chat participants are listed in the prompt auto-complete list as well.</p>

<p>A chat participant class derives from the abstract base class <code class="language-plaintext highlighter-rouge">ChatParticipant</code> and implements the required methods and properties of it. In the example below <code class="language-plaintext highlighter-rouge">ExampleChatParticipant</code> defines a command named <code class="language-plaintext highlighter-rouge">repeat</code>. If the prompt is called with <code class="language-plaintext highlighter-rouge">repeat</code> command (<code class="language-plaintext highlighter-rouge">@example /repeat some prompt</code>) it simply echoes the prompt. If no command is specified it passes the request to the default chat participant which is GitHub Copilot.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ExampleChatParticipant</span><span class="p">(</span><span class="n">ChatParticipant</span><span class="p">):</span>
    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">id</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"example"</span>

    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">name</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"Example Participant"</span>
    
    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">description</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"An example participant"</span>

    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">icon_path</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">PARTICIPANT_ICON_URL</span>

    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">commands</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="n">ChatCommand</span><span class="p">]:</span>
        <span class="k">return</span> <span class="p">[</span>
            <span class="n">ChatCommand</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">'repeat'</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s">'Repeats the prompt'</span><span class="p">)</span>
        <span class="p">]</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">handle_chat_request</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">response</span><span class="p">:</span> <span class="n">ChatResponse</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">command</span> <span class="o">==</span> <span class="s">'repeat'</span><span class="p">):</span>
            <span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">MarkdownData</span><span class="p">(</span><span class="sa">f</span><span class="s">"Repeating: </span><span class="si">{</span><span class="n">request</span><span class="p">.</span><span class="n">prompt</span><span class="si">}</span><span class="s">"</span><span class="p">))</span>
            <span class="n">response</span><span class="p">.</span><span class="n">finish</span><span class="p">()</span>
            <span class="k">return</span>

        <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">host</span><span class="p">.</span><span class="n">default_chat_participant</span><span class="p">.</span><span class="n">handle_chat_request</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="chat-response-types">Chat response types</h3>

<p>NBI routes requests to chat participants as needed and passes a <code class="language-plaintext highlighter-rouge">response</code> object of type <code class="language-plaintext highlighter-rouge">ChatResponse</code> to the participants. Chat participant sends messages to the UI using the <code class="language-plaintext highlighter-rouge">response</code> object as it is generating a response for the user prompt. NBI supports various types of response message types as listed below. Once a chat participant is done generating, it calls <code class="language-plaintext highlighter-rouge">response.finish()</code> method to signal to the UI that it is done.</p>

<p>Chat participant calls the <code class="language-plaintext highlighter-rouge">response.stream(&lt;message type&gt;)</code> method to send streaming responses to the UI. Below are the message types with examples.</p>

<h4 id="markdown">Markdown</h4>
<p>This is the most common response type, any text response with formatting can be sent in response to a prompt with this message type.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">MarkdownData</span><span class="p">(</span><span class="s">"Hello world!"</span><span class="p">))</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/response-types/response-type-markdown.png" alt="Markdown response type" width="400" /></p>

<p>Markdown response can contain code sections. Code sections will be rendered in a special frame with action buttons on the header area for easy integration into the workspace.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">MarkdownData</span><span class="p">(</span><span class="s">"""Here is a Python method I generated. </span><span class="se">\n</span><span class="s">```python</span><span class="se">\n</span><span class="s">def show_message():</span><span class="se">\n</span><span class="s">  print('Hello world!')</span><span class="se">\n</span><span class="s">```</span><span class="se">\n</span><span class="s">"""</span><span class="p">))</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/response-types/response-type-markdown-code.png" alt="Markdown code response" width="400" /></p>

<h4 id="htmlframe">HTMLFrame</h4>

<p>HTML content can be sent using this message type. Note that the content will always be rendered in a sandboxed iframe that allows scripts. You can also specify height of the frame in pixels as it won’t auto resize to content.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">HTMLFrameData</span><span class="p">(</span><span class="sa">f</span><span class="s">"""
    &lt;div&gt;
        &lt;img style="width: 100%" src="https://jupyter.org/assets/homepage/main-logo.svg" /&gt;
    &lt;/div&gt;
    """</span><span class="p">,</span> <span class="n">height</span><span class="o">=</span><span class="mi">400</span><span class="p">))</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/response-types/response-type-htmlframe.png" alt="HTMLFrame response type" width="400" /></p>

<h4 id="button">Button</h4>
<p>This response type lets you show an action button on chat interface. You can specify the title of the button, the UI command ID to trigger when the button is clicked and also any arguments to pass to the UI command. The example button below shows a notification message on JupyterLab UI when clicked.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">ButtonData</span><span class="p">(</span>
    <span class="n">title</span><span class="o">=</span><span class="s">"Button title"</span><span class="p">,</span>
    <span class="n">commandId</span><span class="o">=</span><span class="s">"apputils:notify"</span><span class="p">,</span>
    <span class="n">args</span> <span class="o">=</span><span class="p">{</span>
        <span class="s">"message"</span><span class="p">:</span> <span class="s">'Copilot chat button was clicked'</span><span class="p">,</span>
        <span class="s">"type"</span><span class="p">:</span> <span class="s">'success'</span><span class="p">,</span>
        <span class="s">"options"</span><span class="p">:</span> <span class="p">{</span> <span class="s">"autoClose"</span><span class="p">:</span> <span class="bp">False</span> <span class="p">}</span>
    <span class="p">})</span>
<span class="p">)</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/response-types/response-type-button.png" alt="Button response type" /></p>

<h4 id="anchor">Anchor</h4>
<p>Anchor response type lets you show a link on the chat interface. You can specify the URL and the title of the link. These links are always opened on a new browser tab.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">AnchorData</span><span class="p">(</span><span class="s">"https://www.jupyter.org"</span><span class="p">,</span> <span class="s">"Click me! I am a link!"</span><span class="p">))</span>
</code></pre></div></div>
<p><img src="/notebook-intelligence/assets/images/response-types/response-type-anchor.png" alt="Anchor response type" width="400" /></p>

<h4 id="progress">Progress</h4>
<p>Progress response type lets you show a progress message. You can send this response before executing a long running task. It will be automatically removed once a new response is received by the UI.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">ProgressData</span><span class="p">(</span><span class="s">"Running..."</span><span class="p">))</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/response-types/response-type-progress.png" alt="Progress response type" width="400" /></p>

<h4 id="confirmation">Confirmation</h4>
<p>Confirmation response type shows a confirmation message with confirm and cancel buttons on the chat UI and waits for the user to click an option. You can use this before applying irreversible changes or time consuming tasks for example.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">callback_id</span> <span class="o">=</span> <span class="n">uuid</span><span class="p">.</span><span class="n">uuid4</span><span class="p">().</span><span class="nb">hex</span>
<span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">ConfirmationData</span><span class="p">(</span>
    <span class="n">title</span><span class="o">=</span><span class="s">"Confirm"</span><span class="p">,</span>
    <span class="n">message</span><span class="o">=</span><span class="s">"Are you sure you want to continue?"</span><span class="p">,</span>
    <span class="n">confirmArgs</span><span class="o">=</span><span class="p">{</span><span class="s">"id"</span><span class="p">:</span> <span class="n">response</span><span class="p">.</span><span class="n">message_id</span><span class="p">,</span> <span class="s">"data"</span><span class="p">:</span> <span class="p">{</span> <span class="s">"callback_id"</span><span class="p">:</span> <span class="n">callback_id</span><span class="p">,</span> <span class="s">"data"</span><span class="p">:</span> <span class="p">{</span><span class="s">"confirmed"</span><span class="p">:</span> <span class="bp">True</span><span class="p">}}},</span>
    <span class="n">cancelArgs</span><span class="o">=</span><span class="p">{</span><span class="s">"id"</span><span class="p">:</span> <span class="n">response</span><span class="p">.</span><span class="n">message_id</span><span class="p">,</span> <span class="s">"data"</span><span class="p">:</span> <span class="p">{</span> <span class="s">"callback_id"</span><span class="p">:</span> <span class="n">callback_id</span><span class="p">,</span> <span class="s">"data"</span><span class="p">:</span> <span class="p">{</span><span class="s">"confirmed"</span><span class="p">:</span> <span class="bp">False</span><span class="p">}}},</span>
    <span class="n">confirmLabel</span><span class="o">=</span><span class="s">"Confirm"</span><span class="p">,</span>
    <span class="n">cancelLabel</span><span class="o">=</span><span class="s">"Cancel"</span>
<span class="p">))</span>

<span class="n">user_input</span> <span class="o">=</span> <span class="k">await</span> <span class="n">ChatResponse</span><span class="p">.</span><span class="n">wait_for_chat_user_input</span><span class="p">(</span><span class="n">response</span><span class="p">,</span> <span class="n">callback_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user_input</span><span class="p">[</span><span class="s">'confirmed'</span><span class="p">]</span> <span class="o">==</span> <span class="bp">False</span><span class="p">:</span>
    <span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">MarkdownData</span><span class="p">(</span><span class="s">"User cancelled the action."</span><span class="p">))</span>
    <span class="n">response</span><span class="p">.</span><span class="n">finish</span><span class="p">()</span>
<span class="k">else</span><span class="p">:</span>
    <span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">MarkdownData</span><span class="p">(</span><span class="s">"User confirmed the action."</span><span class="p">))</span>
    <span class="n">response</span><span class="p">.</span><span class="n">finish</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/response-types/response-type-confirmation.gif" alt="Confirmation response type" /></p>

<h3 id="running-ui-commands">Running UI Commands</h3>
<p>You can trigger Jupyter UI commands directly from your extension when processing prompt requests. You can do that by calling <code class="language-plaintext highlighter-rouge">response.run_ui_command</code> method. The response for the command will be returned if any.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ui_cmd_response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">run_ui_command</span><span class="p">(</span>
    <span class="n">command</span><span class="o">=</span><span class="s">'apputils:notify'</span><span class="p">,</span>
    <span class="n">args</span><span class="o">=</span><span class="p">{</span>
        <span class="s">"message"</span><span class="p">:</span> <span class="s">'Copilot chat button was clicked'</span><span class="p">,</span>
        <span class="s">"type"</span><span class="p">:</span> <span class="s">'success'</span><span class="p">,</span>
        <span class="s">"options"</span><span class="p">:</span> <span class="p">{</span> <span class="s">"autoClose"</span><span class="p">:</span> <span class="bp">False</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="handling-chat-request-cancellations">Handling chat request cancellations</h3>

<p>Users might want to cancel chat requests for various reasons such as if they are taking longer than expected. NBI lets you easily handle cancel requests coming from the user. ChatRequest object has a member named <code class="language-plaintext highlighter-rouge">cancel_token</code> which passes cancel signals from the user to your extension. In your chat request handler you should check for cancellation flag (<code class="language-plaintext highlighter-rouge">request.cancel_token.is_cancel_requested</code>) and handle cancellations before running sections of your code and especially before time consuming steps. You can also listen for cancellation signal (<code class="language-plaintext highlighter-rouge">request.cancel_token.cancellation_signal.connect</code>) if that works for your use case.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">handle_chat_request</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">ChatRequest</span><span class="p">,</span> <span class="n">response</span><span class="p">:</span> <span class="n">ChatResponse</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">cancellation_handler</span><span class="p">():</span>
        <span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">MarkdownData</span><span class="p">(</span><span class="s">"Cancel event received"</span><span class="p">)</span>

    <span class="n">request</span><span class="p">.</span><span class="n">cancel_token</span><span class="p">.</span><span class="n">cancellation_signal</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">cancellation_handler</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="n">cancel_token</span><span class="p">.</span><span class="n">is_cancel_requested</span><span class="p">:</span>
        <span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">MarkdownData</span><span class="p">(</span><span class="s">"Cancelled"</span><span class="p">))</span>
        <span class="n">response</span><span class="p">.</span><span class="n">finish</span><span class="p">()</span>
        <span class="k">return</span>

    <span class="c1"># time consuming execution
</span>    <span class="p">...</span>
    <span class="n">response</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="n">MarkdownData</span><span class="p">(</span><span class="s">"Handled chat request"</span><span class="p">))</span>
    <span class="n">response</span><span class="p">.</span><span class="n">finish</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/notebook-intelligence/assets/images/canceling-chat-request.gif" alt="Cancelling request" /></p>

<h2 id="try-it-out-and-share-your-feedback">Try it out and share your feedback!</h2>

<p>I am looking forward to seeing the extensions built by the community. Please try the extension APIs and share your feedback using project’s <a href="https://github.com/notebook-intelligence/notebook-intelligence/issues">GitHub issues</a>! User feedback from the community will shape the project’s roadmap.</p>

<h2 id="about-the-author">About the Author</h2>

<p><a href="https://www.linkedin.com/in/mehmet-bektas">Mehmet Bektas</a> 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.</p>

<p>The source code for the example extension in this post is available <a href="https://github.com/notebook-intelligence/nbi-extension-example">on GitHub</a>.</p>]]></content><author><name>Mehmet Bektaş</name></author><summary type="html"><![CDATA[Notebook Intelligence (NBI) is an AI coding assistant and extensible AI framework for JupyterLab. For an introduction to NBI see Introducing Notebook Intelligence blog post first.]]></summary></entry><entry><title type="html">Introducing Notebook Intelligence</title><link href="https://plmbr.dev/blog/archive/introducing-notebook-intelligence/" rel="alternate" type="text/html" title="Introducing Notebook Intelligence" /><published>2025-01-08T02:49:32+00:00</published><updated>2025-01-08T02:49:32+00:00</updated><id>https://plmbr.dev/blog/archive/introducing-notebook-intelligence</id><content type="html" xml:base="https://plmbr.dev/blog/archive/introducing-notebook-intelligence/"><![CDATA[<p>I am thrilled to announce the release of <a href="https://github.com/notebook-intelligence/notebook-intelligence" target="_blank">Notebook Intelligence</a> (NBI)! NBI is an AI coding assistant and extensible AI framework for JupyterLab. It uses <a href="https://github.com/features/copilot" target="_blank">GitHub Copilot</a> 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.</p>

<p><img src="/notebook-intelligence/assets/images/generate-code.gif" alt="Generate code" /></p>

<h2 id="generate-code-iterate-onit">Generate code, iterate on it</h2>
<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/generate-code-popover.png" alt="Generate code popover" /></p>

<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/generate-code4-cropped.png" alt="Generate code with diff viewer" /></p>

<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/fix-diff.gif" alt="Fix code in cell with NBI" /></p>

<h2 id="explain-and-fix-code-troubleshoot-errorsreported">Explain and fix code, troubleshoot errors reported</h2>

<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/copilot-context-menu.png" alt="Copilot context menu" /></p>

<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/explain-fix-troubleshoot.gif" alt="Explain, fix, troubleshoot" /></p>

<h2 id="inline-completions">Inline Completions</h2>

<p>Notebook Intelligence integrates with JupyterLab’s inline completion APIs and provides code suggestions as you type in a code cell or a Python file.</p>

<p><img src="/notebook-intelligence/assets/images/inline-completion.png" alt="Inline completions" /></p>

<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/inline-completions.gif" alt="Inline completions example" /></p>

<h2 id="copilot-chat">Copilot Chat</h2>

<p>NBI provides a user friendly chat interface to chat with GitHub Copilot. You can ask questions related to coding.</p>

<p><img src="/notebook-intelligence/assets/images/copilot-chat.gif" alt="Copilot Chat" /></p>

<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/copilot-chat-toolbar.gif" alt="Copilot Chat toolbar actions" /></p>

<h2 id="chat-commands">Chat Commands</h2>

<p>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.</p>

<p><img src="/notebook-intelligence/assets/images/copilot-chat.png" alt="Chat command auto-complete" /></p>

<h3 id="newnotebook-command">/newNotebook command</h3>

<p>You can generate new notebooks from a prompt with the <strong>/newNotebook</strong> 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.</p>

<p><img src="/notebook-intelligence/assets/images/new-notebook.gif" alt="Generate notebook example" /></p>

<h3 id="newpythonfile-command">/newPythonFile command</h3>

<p>You can also create new Python files from a prompt using the <strong>/newPythonFile</strong> command.</p>

<p><img src="/notebook-intelligence/assets/images/new-python-file.gif" alt="Generate Python file example" /></p>

<h2 id="getting-started-with-notebook-intelligence">Getting Started with Notebook Intelligence</h2>

<p>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).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>notebook-intelligence
</code></pre></div></div>

<h2 id="authentication-with-githubcopilot">Authentication with GitHub Copilot</h2>

<p>Notebook Intelligence requires a <a href="https://github.com/features/copilot" target="_blank">GitHub Copilot</a> subscription. NBI provides a user friendly interface to sign into your GitHub Copilot account from JupyterLab UI to activate access to your subscription.</p>

<p><img src="/notebook-intelligence/assets/images/github-auth.gif" alt="GitHub Copilot authentication" /></p>

<h2 id="extensible-framework">Extensible Framework</h2>

<p>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.</p>

<h2 id="try-it-out-and-share-your-feedback">Try it out and share your feedback!</h2>

<p><a href="https://github.com/notebook-intelligence/notebook-intelligence" target="_blank">Notebook Intelligence</a> 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 <a href="https://github.com/notebook-intelligence/notebook-intelligence/issues" target="_blank">GitHub issues</a>. User feedback from the community will shape the project’s roadmap.</p>

<h2 id="about-theauthor">About the Author</h2>

<p><a href="https://www.linkedin.com/in/mehmet-bektas" target="_blank">Mehmet Bektas</a> 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.</p>]]></content><author><name>Mehmet Bektaş</name></author><summary type="html"><![CDATA[I am thrilled to announce the release of Notebook Intelligence (NBI)! NBI is an AI coding assistant and extensible AI framework for JupyterLab. It uses GitHub Copilot 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.]]></summary></entry></feed>