wordslab-notebooks-lib.notebook

Access wordslab-notebooks Jupyterlab extension version, current notebook path, json content and cell id, and create or update cells.

Work with AI in a Jupyterlab notebook - the Solveit method

A Jupyter notebook is a convenient way to build context for a LLM one cell after the other: you are working in a fully editable conversation, while interacting with AI and with code.

Jeremy Howard and his team at Answer.ai explored how to work efficiently in this kind of conversation: they developed a method and platform called Solveit.

https://solve.it.com/

We would like to replicate this approach to working with AI in a wordslab notebooks environment.

Here is how we chose to do it: - in a jupyterlab notebook, there are two types of cells: markdown and code - we want to simulate a third type of cell: a “prompt” cell - the content of this cell is a prompt (text in markdown format) which is sent to an llm when the cell is executed, along with the text of all the cells situated above in the notebook (context) - the llm response is streamed just below and formatted as markdown.

To simulate this “prompt” cell we need to develop a Jupyterlab frontend extension which implements the following behaviors : - three buttons are added to the cell toolbar: “note”, “prompt”, “code” - a click on one of these buttons changes the type of the cell - “note” selects a classic markdown cell - “prompt” selects a code cell, modified with the special “prompt behavior” defined below - “code” selects a classic code cell - a “prompt” cell is distinguished from a regular code cell by a metadata property registered in the ipynb file - each cell type is visualized by a specific color in the left border of the cell - “note” cell has a green border - “prompt” cell a red border - “code” cell has a blue border - the “prompt” cell is a code cell with the specific modified behaviors - the code syntax highlighting is replaced by markdown syntax highlighting when the user types text in this cell - when the user executes this cell, the frontend extension does the following - calls the kernel to inject the following variables - __notebook_path with the path and name of the notebook in the workspace - __notebook_content with the full json representation of the notebook - __cell_id with the id of the current cell - then calls the kernel to execute a specific chat(message) python function - where the message parameter is the content of the cell - and the content of the notebook above the current cell is inluded as context - the python chat() function streams the response tokens from the llm to the output section of the code cell, with markdown rendering

See the section “Develop a Jupyterlab frontend extension” at the bottom of this page to understand how the extension was developed.

Install the Jupyterlab extension - wordslab-notebooks-lib

If you want to use “prompt” cells, you will first need to install the Jupyterlab frontend extension: - activate your Jupyterlab python virtual environment - pip install wordslab-notebooks-lib - restart your Jupyterlab server

The extension is already pre-installed in the wordslab-notebooks environment.

To be clear: the wordslab-notebooks-lib package contains both: the Javascript Jupyterlab frontend extension AND the python library wich is loaded in the python kernel.

The Jupyterlab frontend extension is reloaded and re-initialized each time you refresh your browser page: - to check is the extension is installed and running, look at the browser console and llok for the message ‘Wordslab notebooks extension vx.y.z activated’ - hit the refresh button if you encounter a bug and the extension stops working

Extend the ipython kernel with useful utilities

You also need to install the wordslab-notebooks-lib library in the virtual environnement used by the ipython kernel which runs each notebook in which you want to use the Solveit method.

It is the client of the Jupyterlab extension, and provides many utilties and tools which support this new way of working with AI.

The main python object used to interact with an ipython kernel is the InteractiveShell. You get an instance of it with the get_ipython() method.

shell = get_ipython()
type(shell)
ipykernel.zmqshell.ZMQInteractiveShell

We will start by extending this shell with capabilities useful to work in a notebook with the Solveit method. These extensions are inspired by the library ipykernel_helper from Answer.ai. As of december 2025, this library is not open source, but it is available to users in the solve.it.com environment and is a dependency of other Apache 2.0 libraries, so I think it is OK to use it as an inspiration.

escape??
def escape(s, quote=True):
    """
    Replace special characters "&", "<" and ">" to HTML-safe sequences.
    If the optional flag quote is true (the default), the quotation mark
    characters, both double quote (") and single quote (') characters are also
    translated.
    """
    s = s.replace("&", "&amp;") # Must be done first!
    s = s.replace("<", "&lt;")
    s = s.replace(">", "&gt;")
    if quote:
        s = s.replace('"', "&quot;")
        s = s.replace('\'', "&#x27;")
    return s

File: /home/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/html/__init__.py


InteractiveShell.user_items


def user_items(
    max_str_len:int=200, xtra_ignore:tuple=()
):

Get an overview of the variables & functions defined by the user so far in the notebook. The value addded by this function is to filter out all internal ipython and wordslab variables. Returns a tuple of dictionaries (user_variables, user_functions): - the keys are the variables or function names - the value is a truncated string representation of the variable value or the function signature The max_str_len parameter is used to truncate the string representation of the variables. The xtra_ignore parameter is used to hide additional names from the result.

variables, functions = shell.user_items()
variables, functions
({'Code': "functools.partial(<function ft>, 'code', void_=False)",
  'Source': "functools.partial(<function ft>, 'source', void_=True)",
  'shell': '<ipykernel.zmqshell.ZMQInteractiveShell object>',
  'user_items': 'None'},
 {'_safe_str': '(obj, max_str_len=200)'})

InteractiveShell.get_variables_values


def get_variables_values(
    var_names:list, literal:bool=True
):

Get a safe and serializable representation of variables values from the user namespace. This method preserves real Python values when they are safe literals, otherwise it falls back to strings. You can call it in two modes: - literal = True : Preserve actual Python values when safe, best for internal tools - literal = False : Force everything to strings, best for logging / UI display / debug output

shell.get_variables_values(var_names=["variables", "functions"])
{'variables': {'Code': "functools.partial(<function ft>, 'code', void_=False)",
  'Source': "functools.partial(<function ft>, 'source', void_=True)",
  'shell': '<ipykernel.zmqshell.ZMQInteractiveShell object>',
  'user_items': 'None'},
 'functions': {'_safe_str': '(obj, max_str_len=200)'}}

InteractiveShell.get_tools_schemas_and_functions


def get_tools_schemas_and_functions(
    func_names:list
):

Get a json schema and a function object for the functions defined in the user namespace which can be used as tools.

# Example tool definition
def example_sum(
    a: int,  # First thing to sum
    b: int = 1,  # Second thing to sum
) -> int:  # The sum of the inputs
    "Adds a + b."
    return a + b
shell.get_tools_schemas_and_functions(["example_sum"])
{'example_sum': ({'type': 'function',
   'function': {'name': 'example_sum',
    'description': 'Adds a + b.\n\nReturns:\n- type: integer',
    'parameters': {'type': 'object',
     'properties': {'a': {'type': 'integer',
       'description': 'First thing to sum'},
      'b': {'type': 'integer',
       'description': 'Second thing to sum',
       'default': 1}},
     'required': ['a']}}},
  <function __main__.example_sum(a: int, b: int = 1) -> int>)}

Access the notebook path, cells content and current cell id

The 4 notebook properties below are silently injected by the Jupyterlab frontend extension before each code cell is executed.

This will not work if the wordslab-notebooks-lib Jupyterlab extension is not installed.


find_var


def find_var(
    var:str
):

Search for var in all frames of the call stack


WordslabNotebook


def WordslabNotebook(
    
):

Jupyterlab notebook introspection and metaprogramming.

notebook = WordslabNotebook()
notebook.jupyterlab_extension_version
'0.0.13'
notebook.path
'wordslab-notebooks-lib/nbs/04_notebook.ipynb'
notebook.content.metadata
{'kernelspec': {'display_name': 'wordslab-notebooks-lib',
  'language': 'python',
  'name': 'wordslab-notebooks-lib'},
 'language_info': {'codemirror_mode': {'name': 'ipython', 'version': 3},
  'file_extension': '.py',
  'mimetype': 'text/x-python',
  'name': 'python',
  'nbconvert_exporter': 'python',
  'pygments_lexer': 'ipython3',
  'version': '3.12.12'}}
notebook.cell_id
'd16ad869-d651-40bd-af2c-623d82b4edf0'
notebook.cell_id
'59347d6e-d1f7-4d23-b041-143e42887f6d'

Create, update, delete and run notebook cells

These methods can be used to manipulate the notebook cells when the wordslab-notebooks-lib Jupyterlab frontend extension is installed.

They are inspired by the library dialoghelper from Answer.ai, but are adapted to the standard Jupyterlab context.


WordslabNotebook.add_cell


def add_cell(
    content:str, # Content of the cell (i.e the prompt, code, or note cell text)
    placement:str='add_after', # Can be 'add_after', 'add_before', 'at_start', 'at_end'
    cell_id:str=None, # id of the cell that placement is relative to (if None, uses current cell)
    cell_type:str='note', # Cell type, can be 'code', 'note', or 'prompt'
    notebook_path:str='', # Notebook to update, defaults to current notebook
):

Add a cell to the current notebook or any other opened notebook (notebook_path), at the start/end of the notebook or before/after any cell (placementand cell_id), with a cell_type (note|prompt|code) and content (text). Returns the new cell id.

await notebook.add_cell("Test note")
'a1cc961e-e918-4ac7-9398-7c6fa574f6f2'

Test note

await notebook.add_cell("Test note 2", placement="add_after")
'984dea0c-450c-488f-8115-82e92d56c0ea'

Test note 2

Test note 3

await notebook.add_cell("Test note 3", placement="add_before")
'1e24f157-66dc-4200-8b12-523c92bb447d'
await notebook.add_cell("Test note 4", placement="at_start")
'01d8ebd2-7a4d-479a-939a-d345a9660211'
await notebook.add_cell("Test note 5", placement="at_end")
'b135d91b-63c0-4a73-8172-d7e73e3a5cf3'
# Creates a new cell at the end of the notebook
await notebook.add_cell("Test note somewhere", placement="somewhere")
'28690fe7-bf10-4781-8067-432f45d18c7f'
await notebook.add_cell("Test prompt", cell_type='prompt')
'c41b364b-74da-4886-a974-1683e8143864'
Test prompt
await notebook.add_cell("Test code", cell_type='code')
'cd02e818-4989-40a8-90ce-8e92a99bcd59'
Test code
# Create a new cell of type note
await notebook.add_cell("Test sometype", cell_type='sometype')
'e80dbc6b-0832-423f-8e33-aacc0ea52039'

Test sometype

await notebook.add_cell("Test bad cell id", cell_id="bad_cell_id")
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[69], line 1
----> 1 await notebook.add_cell("Test bad cell id", cell_id="bad_cell_id")

Cell In[65], line 25, in add_cell(self, content, placement, cell_id, cell_type, notebook_path)
     23     return result['cell_id']
     24 elif 'error' in result:
---> 25     raise RuntimeError(result['error'])

RuntimeError: Cell not found: bad_cell_id
await notebook.add_cell("Test other notebook (bad notebook name)", placement="at_start", notebook_path="wordslab-notebooks-lib/nbs/temp.ipynb")
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[73], line 1
----> 1 await notebook.add_cell("Test other notebook (bad notebook name)", placement="at_start", notebook_path="wordslab-notebooks-lib/nbs/temp.ipynb")

Cell In[65], line 25, in add_cell(self, content, placement, cell_id, cell_type, notebook_path)
     23     return result['cell_id']
     24 elif 'error' in result:
---> 25     raise RuntimeError(result['error'])

RuntimeError: Notebook not found: wordslab-notebooks-lib/nbs/temp.ipynb. Make sure the notebook is opened in Jupyterlab.
await notebook.add_cell("Test other notebook (open)", placement="at_start", notebook_path="wordslab-notebooks-lib/nbs/test.ipynb")
'637eb2a9-421c-4a7c-99aa-e2426b9eab64'
await notebook.add_cell("Test other notebook (closed)", placement="at_start", notebook_path="wordslab-notebooks-lib/nbs/01_env.ipynb")
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[75], line 1
----> 1 await notebook.add_cell("Test other notebook (closed)", placement="at_start", notebook_path="wordslab-notebooks-lib/nbs/01_env.ipynb")

Cell In[65], line 25, in add_cell(self, content, placement, cell_id, cell_type, notebook_path)
     23     return result['cell_id']
     24 elif 'error' in result:
---> 25     raise RuntimeError(result['error'])

RuntimeError: Notebook not found: wordslab-notebooks-lib/nbs/01_env.ipynb. Make sure the notebook is opened in Jupyterlab.

WordslabNotebook.update_cell


def update_cell(
    cell_id:str=None, # id of the cell to update (if None, uses current cell)
    content:str=None, # Content of the cell (i.e the prompt, code, or note cell text)
    notebook_path:str='', # Notebook to update, defaults to current notebook
):

Update the cell identified by cell_id, in the current notebook or any other opened notebook (notebook_path), with a new content. Returns the updated cell id.

orig_cell_id = await notebook.add_cell("original cell content")

updated cell content

await notebook.update_cell(cell_id=orig_cell_id, content="updated cell content")
'293c77e8-5449-407a-8b4f-0d628c45bfc7'
await notebook.update_cell(content="no id")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[95], line 1
----> 1 await notebook.update_cell(content="no id")

Cell In[94], line 13, in update_cell(self, cell_id, content, notebook_path)
     11 self._ensure_jupyterlab_extension()
     12 if not cell_id:
---> 13      raise ValueError("`cell_id` parameter is mandatory")
     14 result = await self._comm.send({'action': 'update_cell', 'cell_id': cell_id, 'content': content, 'notebook_path': notebook_path })
     15 if 'success' in result and result['success']:

ValueError: `cell_id` parameter is mandatory
await notebook.update_cell(cell_id="bad_cell_id", content="bad id")
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[96], line 1
----> 1 await notebook.update_cell(cell_id="bad_cell_id", content="bad id")

Cell In[94], line 18, in update_cell(self, cell_id, content, notebook_path)
     16     return result['cell_id']
     17 elif 'error' in result:
---> 18     raise RuntimeError(result['error'])

RuntimeError: Cell not found: bad_cell_id
orig_cell_id = await notebook.add_cell("original cell content in other notebook", placement="at_start", notebook_path="wordslab-notebooks-lib/nbs/test.ipynb")
await notebook.update_cell(cell_id=orig_cell_id, content="updated cell content in other notebook", notebook_path="wordslab-notebooks-lib/nbs/test.ipynb")
'55d9b34d-7e39-4cb6-9c95-eb0b9e0176f3'

WordslabNotebook.delete_cell


def delete_cell(
    cell_id:str=None, # id of cell to delete
    notebook_path:str='', # Notebook to update, defaults to current notebook
):

“Update the cell identified by cell_id, in the current notebook or any other opened notebook (notebook_path). Returns the deleted cell id.

await notebook.delete_cell(cell_id="01d8ebd2-7a4d-479a-939a-d345a9660211")
'01d8ebd2-7a4d-479a-939a-d345a9660211'
await notebook.delete_cell(cell_id="55d9b34d-7e39-4cb6-9c95-eb0b9e0176f3", notebook_path="wordslab-notebooks-lib/nbs/test.ipynb")
'55d9b34d-7e39-4cb6-9c95-eb0b9e0176f3'

WordslabNotebook.run_cell


def run_cell(
    cell_id:str=None, # id of cell to execute
):

“Adds the cell identified by cell_id to the run queue, only in the current notebook (jupyterlab ‘run-cell’ command limitation). Returns the cell id. DOES NOT return the result of the execution: the target cell will only be run after the current cell execution finishes. Use the read_cell method later with the same cell_id to get the result of the execution.

code_cell_id = await notebook.add_cell(cell_type="code", content="1+1")
1+1
2
await notebook.run_cell(cell_id=code_cell_id)
'2a7bc090-ef99-4a21-bf9e-475d24de51c5'

WordslabNotebook.read_cell


def read_cell(
    cell_id:str=None, # id of cell to delete
):

“Read the text content of the cell identified by cell_id, only in the current notebook. Returns the text content of the cell as a single multiline string.

above_cell_id = "df396dbb-8ecf-41df-b4cf-1f484dabb0fb"
notebook.read_cell(above_cell_id)
'#|export\n@patch\ndef read_cell(\n    self: WordslabNotebook,\n    cell_id: str = None,  # id of cell to delete\n):\n    """"Read the text content of the cell identified by `cell_id`, only in the current notebook.\n    Returns the text content of the cell as a single multiline string."""\n    self._ensure_jupyterlab_extension()\n    if not cell_id:\n        raise ValueError("`cell_id` parameter is mandatory")\n    cell = next((c for c in self.content.cells if c.id == cell_id), None)\n    if not cell:\n        raise ValueError(f"Cell not found: {cell_id}")\n    return cell.source'

Explore the notebook variables, functions and objects


WordslabNotebook.show_variables_and_functions


def show_variables_and_functions(
    
):

Display the variables and functions defined by the user so far in the notebook.

notebook.show_variables_and_functions()

Variables

Name Value
Code functools.partial(<function ft at 0x7b36600a9580>, 'code', void_=False)
Source functools.partial(<function ft at 0x7b36600a9580>, 'source', void_=True)
shell <ipykernel.zmqshell.ZMQInteractiveShell object at 0x7b3670078680>
user_items None
variables {'Code': "functools.partial(<function ft at 0x7b36600a9580>, 'code', void_=False)", 'Source': "functools.partial(<function ft at 0x7b36600a9580>, 'source', void_=True)", 'shell': '<ipykernel.zmqshell.…
functions {'_safe_str': '(obj, max_str_len=200)'}
get_variables_values None
get_tools_schemas_and_functions None
notebook <__main__.WordslabNotebook object at 0x7b3640f956a0>
add_cell None
update_cell None
delete_cell None
run_cell None
read_cell None
show_variables_and_functions None

Functions

Name Signature
_safe_str (obj, max_str_len=200)
_get_schema (ns: dict, t)
example_sum (a: int, b: int = 1) -> int
_find_frame_dict (var: str)
find_var (var: str)

WordslabNotebook.show_object_members


def show_object_members(
    obj
):

Display the attributes and methods of a given python object

notebook.show_object_members(notebook)

Object of type: WordslabNotebook

Jupyterlab notebook introspection and metaprogramming.

Attributes

Name Type Value Doc
cell_id str b602a192-b7e7-4976-a26d-4fa1854879b4 Unique ID of the current notebook cell, useful to locate the current cell in the full notebook content
chat_model str qwen3:30b
chat_thinking bool True
content NotebookNode {'metadata': {'kernelspec': {'display_name': 'wordslab-notebooks-lib', 'language': 'python', 'name': 'wordslab-notebooks-lib'}, 'language_info': {'codemirror_mode': {'name': 'ipython', 'version': 3}, … Full content of the notebook returned as a NotebookNode object from the nbformat library
jupyterlab_extension_installed bool True
jupyterlab_extension_version str 0.0.13 wordslab-notebooks-lib version number injected by the Jupyterlab frontend extension
path str wordslab-notebooks-lib/nbs/04_notebook.ipynb Relative path of the notebook .ipynb file in the notebook workspace

Methods

Name Signatue Type Doc
JupyterlabExtensionComm (target_name='wordslab_notebooks', timeout=2.0) instance method
add_cell (content: str, placement: str = 'add_after', cell_id: str = None, cell_type: str = 'note', notebook_path: str = '') instance method Add a cell to the current notebook or any other opened notebook (`notebook_path`), at the start/end of the notebook or before/after any cell (`placement`and `cell_id`), with a `cell_type` (note|prompt…
delete_cell (cell_id: str = None, notebook_path: str = '') instance method "Update the cell identified by `cell_id`, in the current notebook or any other opened notebook (`notebook_path`). Returns the deleted cell id.
read_cell (cell_id: str = None) instance method "Read the text content of the cell identified by `cell_id`, only in the current notebook. Returns the text content of the cell as a single multiline string.
run_cell (cell_id: str = None) instance method "Adds the cell identified by `cell_id` to the run queue, only in the current notebook (jupyterlab 'run-cell' command limitation). Returns the cell id. DOES NOT return the result of the execution: the …
show_object_members (obj) instance method Display the attributes and methods of a given python object
show_variables_and_functions () instance method Display the variables and functions defined by the user so far in the notebook.
update_cell (cell_id: str = None, content: str = None, notebook_path: str = '') instance method Update the cell identified by `cell_id`, in the current notebook or any other opened notebook (`notebook_path`), with a new `content`. Returns the updated cell id.

Get $variable values and &tool schemas referenced in prompt cells


WordslabNotebook.get_tools_schemas_and_functions


def get_tools_schemas_and_functions(
    func_names:list
):

Get a json schema of functions which can be used as tools.


WordslabNotebook.get_variables_values


def get_variables_values(
    var_names:list
):

Get a safe and serializable representation of variables values.

notebook.get_variables_values(var_names=["variables", "functions"])
{'variables': {'Code': "functools.partial(<function ft>, 'code', void_=False)",
  'Source': "functools.partial(<function ft>, 'source', void_=True)",
  'shell': '<ipykernel.zmqshell.ZMQInteractiveShell object>',
  'user_items': 'None'},
 'functions': {'_safe_str': '(obj, max_str_len=200)'}}
notebook.get_tools_schemas_and_functions(["example_sum"])
{'example_sum': ({'type': 'function',
   'function': {'name': 'example_sum',
    'description': 'Adds a + b.\n\nReturns:\n- type: integer',
    'parameters': {'type': 'object',
     'properties': {'a': {'type': 'integer',
       'description': 'First thing to sum'},
      'b': {'type': 'integer',
       'description': 'Second thing to sum',
       'default': 1}},
     'required': ['a']}}},
  <function __main__.example_sum(a: int, b: int = 1) -> int>)}

Collect the notebook context for LLM calls

The notebook cells format is documented here:

https://nbformat.readthedocs.io/en/latest/format_description.html

Code cell outputs are a list, where each item has an output_type. The main types are:

  • stream — stdout/stderr text (e.g., from print())

Has name (stdout/stderr) and text fields

  • execute_result — the return value of the last expression

Has data dict with MIME types like text/plain, text/html, image/png

  • display_data — from display() calls

Same data dict structure as execute_result

  • error — exceptions

Has ename, evalue, and traceback fields

The tricky part is that execute_result and display_data can contain multiple representations of the same data (e.g., a pandas DataFrame might have both text/plain and text/html versions).

Here is an example of “note” cell

{'id': 'eb560f48-42a2-4573-bf12-b3edb40bff20',
 'cell_type': 'markdown',
 'source': 'Code cell outputs in nbformat are a list, where each item has an output_type. The main types are:\n\n- stream — stdout/stderr text (e.g., from print())\n\nHas name (stdout/stderr) and text fields\n\n- execute_result — the return value of the last expression\n\nHas data dict with MIME types like text/plain, text/html, image/png\n\n- display_data — from display() calls\n\nSame data dict structure as execute_result\n\n- error — exceptions\n\nHas ename, evalue, and traceback fields\n\nThe tricky part is that execute_result and display_data can contain multiple representations of the same data (e.g., a pandas DataFrame might have both text/plain and text/html versions).',
 'metadata': {}}

Here is an example of “prompt” cell

{'id': '3d5a241d-890c-46db-acf5-d92886f9a77d',
 'cell_type': 'code',
 'source': '# This is an example of prompt\nprint("and this is an example of answer")',
 'metadata': {'trusted': True,
  'wordslab_cell_type': 'prompt',
  'execution': {'iopub.status.busy': '2025-12-29T15:47:42.347364Z',
   'iopub.execute_input': '2025-12-29T15:47:42.347549Z',
   'iopub.status.idle': '2025-12-29T15:47:42.350815Z',
   'shell.execute_reply.started': '2025-12-29T15:47:42.347534Z',
   'shell.execute_reply': '2025-12-29T15:47:42.349884Z'}},
 'outputs': [{'name': 'stdout',
   'output_type': 'stream',
   'text': 'and this is an example of answer\n'}],
 'execution_count': 248}

Here is an example of code cell

This code

import sys
from IPython.display import display, HTML, Markdown

# stream (stdout)
print("This is stdout")

# stream (stderr)
print("This is stderr", file=sys.stderr)

# display_data (multiple formats)
display(HTML("<b>Bold HTML</b>"))
display(Markdown("**Bold Markdown**"))

# execute_result (last expression)
{"key": "value", "number": 42}

Produces these outputs

[{'name': 'stdout', 'output_type': 'stream', 'text': 'This is stdout\n'},
 {'name': 'stderr', 'output_type': 'stream', 'text': 'This is stderr\n'},
 {'output_type': 'display_data',
  'data': {'text/plain': '<IPython.core.display.HTML object>',
   'text/html': '<b>Bold HTML</b>'},
  'metadata': {}},
 {'output_type': 'display_data',
  'data': {'text/plain': '<IPython.core.display.Markdown object>',
   'text/markdown': '**Bold Markdown**'},
  'metadata': {}},
 {'execution_count': 223,
  'output_type': 'execute_result',
  'data': {'text/plain': "{'key': 'value', 'number': 42}"},
  'metadata': {}},
 {'traceback': ['\x1b[31m---------------------------------------------------------------------------\x1b[39m',
   '\x1b[31mValueError\x1b[39m                                Traceback (most recent call last)',
   '\x1b[36mCell\x1b[39m\x1b[36m \x1b[39m\x1b[32mIn[224]\x1b[39m\x1b[32m, line 1\x1b[39m\n\x1b[32m----> \x1b[39m\x1b[32m1\x1b[39m \x1b[38;5;28;01mraise\x1b[39;00m \x1b[38;5;167;01mValueError\x1b[39;00m(\x1b[33m"\x1b[39m\x1b[33mExample error message\x1b[39m\x1b[33m"\x1b[39m)\n',
   '\x1b[31mValueError\x1b[39m: Example error message'],
  'ename': 'ValueError',
  'evalue': 'Example error message',
  'output_type': 'error'}]

In this code cell

{'id': 'a1d9fbe2-9a84-4d7d-9415-a2e4693ba7ac',
 'cell_type': 'code',
 'source': 'import sys\nfrom IPython.display import display, HTML, Markdown\n\n# stream (stdout)\nprint("This is stdout")\n\n# stream (stderr)\nprint("This is stderr", file=sys.stderr)\n\n# display_data (multiple formats)\ndisplay(HTML("<b>Bold HTML</b>"))\ndisplay(Markdown("**Bold Markdown**"))\n\n# execute_result (last expression)\n{"key": "value", "number": 42}',
 'metadata': {'trusted': True,
  'execution': {'iopub.status.busy': '2025-12-29T15:07:25.670149Z',
   'iopub.execute_input': '2025-12-29T15:07:25.670496Z',
   'iopub.status.idle': '2025-12-29T15:07:25.678474Z',
   'shell.execute_reply.started': '2025-12-29T15:07:25.670471Z',
   'shell.execute_reply': '2025-12-29T15:07:25.677678Z'}},
 'outputs': [...],
 'execution_count': 223}

The following methods are inspired by the library toolslm from Answer.ai, but are adapted to our specific goal with prompt cells.

Test

[to_xml(_cell_output_to_xml(o)) for o in example_output]

Result

['<out type="stdout">This is stdout\n</out>',
 '<out type="stderr">This is stderr\n</out>',
 '<out type="html">&lt;b&gt;Bold HTML&lt;/b&gt;</out>',
 '<out type="markdown">**Bold Markdown**</out>',
 '<out type="text">{\'key\': \'value\', \'number\': 42}</out>',
 '<out type="error">ValueError: Example error message</out>']

Test

to_xml(_cell_to_xml(note_cell))

Result

'<note>Code cell outputs in nbformat are a list, where each item has an output_type. The main types are:\n\n- stream — stdout/stderr text (e.g., from print())\n\nHas name (stdout/stderr) and text fields\n\n- execute_result — the return value of the last expression\n\nHas data dict with MIME types like text/plain, text/html, image/png\n\n- display_data — from display() calls\n\nSame data dict structure as execute_result\n\n- error — exceptions\n\nHas ename, evalue, and traceback fields\n\nThe tricky part is that execute_result and display_data can contain multiple representations of the same data (e.g., a pandas DataFrame might have both text/plain and text/html versions).</note>'

Test

to_xml(_cell_to_xml(prompt_cell))

Result

'<prompt><user># This is an example of prompt\nprint("and this is an example of answer")</user><assistant><out type="stdout">and this is an example of answer\n</out></assistant></prompt>'

Test

to_xml(_cells_to_notebook_xml([note_cell, prompt_cell, code_cell]))

Result

'<note>Code cell outputs in nbformat are a list, where each item has an output_type. The main types are:\n\n- stream — stdout/stderr text (e.g., from print())\n\nHas name (stdout/stderr) and text fields\n\n- execute_result — the return value of the last expression\n\nHas data dict with MIME types like text/plain, text/html, image/png\n\n- display_data — from display() calls\n\nSame data dict structure as execute_result\n\n- error — exceptions\n\nHas ename, evalue, and traceback fields\n\nThe tricky part is that execute_result and display_data can contain multiple representations of the same data (e.g., a pandas DataFrame might have both text/plain and text/html versions).</note><prompt><user># This is an example of prompt\nprint("and this is an example of answer")</user><assistant><out type="stdout">and this is an example of answer\n</out></assistant></prompt><code><source>import sys\nfrom IPython.display import display, HTML, Markdown\n\n# stream (stdout)\nprint("This is stdout")\n\n# stream (stderr)\nprint("This is stderr", file=sys.stderr)\n\n# display_data (multiple formats)\ndisplay(HTML("&lt;b&gt;Bold HTML&lt;/b&gt;"))\ndisplay(Markdown("**Bold Markdown**"))\n\n# execute_result (last expression)\n{"key": "value", "number": 42}<outputs><out type="stdout">This is stdout\n</out><out type="stderr">This is stderr\n</out><out type="html">&lt;b&gt;Bold HTML&lt;/b&gt;</out><out type="markdown">**Bold Markdown**</out><out type="text">{\'key\': \'value\', \'number\': 42}</out></outputs></code>'

WordslabNotebook.get_context_for_llm


def get_context_for_llm(
    
):
# Estimated number of tokens for this notebook
int(len(notebook.get_context_for_llm())/3)
16843

notebook.chat - the notebook assistant

Prompt template

Syntax to reference tools and variables

Tools and variables

Define test tools and variables

def add(a: int,  # The first number
        b: int   # The second number
       ) -> int: # The sum of the two numbers
  """Add two numbers"""
  return a + b


def multiply(a: int,  # The first number 
             b: int   # The second number
            ) -> int: # The product of the two numbers
  """Multiply two numbers"""
  return a * b

cat_name = "My cat is named Jerry"
dog_name = "My dog is named Rex"

Mention them so they are available to the AI assistant:

  • you can use functions &add, &multiply as tools
  • you can use variables $cat_name, $dog_nameas variables
context = notebook.get_context_for_llm()
funcs_names = FUNC_RE.findall(context)
vars_names = VAR_RE.findall(context)

print(funcs_names)
print(vars_names)
['myfunc', 'add', 'multiply']
['myvar', 'cat_name', 'dog_name']
tools_schemas_and_functions = notebook.get_tools_schemas_and_functions(funcs_names)
tools_schemas_and_functions
{'add': ({'type': 'function',
   'function': {'name': 'add',
    'description': 'Add two numbers\n\nReturns:\n- type: integer',
    'parameters': {'type': 'object',
     'properties': {'a': {'type': 'integer',
       'description': 'The first number'},
      'b': {'type': 'integer', 'description': 'The second number'}},
     'required': ['a', 'b']}}},
  <function __main__.add(a: int, b: int) -> int>),
 'multiply': ({'type': 'function',
   'function': {'name': 'multiply',
    'description': 'Multiply two numbers\n\nReturns:\n- type: integer',
    'parameters': {'type': 'object',
     'properties': {'a': {'type': 'integer',
       'description': 'The first number'},
      'b': {'type': 'integer', 'description': 'The second number'}},
     'required': ['a', 'b']}}},
  <function __main__.multiply(a: int, b: int) -> int>)}
tools = Tools([t[1] for t in tools_schemas_and_functions.values()])
tools.get_functions()
[<function __main__.add(a: int, b: int) -> int>,
 <function __main__.multiply(a: int, b: int) -> int>]
var_values = notebook.get_variables_values(vars_names)
var_values
{'cat_name': 'My cat is named Jerry', 'dog_name': 'My dog is named Rex'}
referenced_variables = "\n".join(L([Var(value, name=name) for name,value in var_values.items()]).map(to_xml))
referenced_variables
'<var name="cat_name">My cat is named Jerry</var>\n<var name="dog_name">My dog is named Rex</var>'

WordslabNotebook.set_ollama_chat_model


def set_ollama_chat_model(
    model:str, think:Union=None,
    context_size:int=32768, # This is the default value for the ollama server in wordslab-notebooks
    web_search:bool=False, # The ollama API key is necessary to activate web search
    base_url:str='http://localhost:11434',
    api_key:Optional=None, # If not provided, the optional key will be pulled from WordslabEnv
):

WordslabNotebook.set_openrouter_chat_model


def set_openrouter_chat_model(
    model:str, think:Union=None,
    context_size:Optional=None, # For OpenRouter this parameter is ignored, we inherit the remote model config
    web_search:bool=True, # Web search is activated by default four cloud models in openrouter
    base_url:str='https://openrouter.ai/api/v1',
    api_key:Optional=None, # If not provided, the mandatory key will be pulled from WordslabEnv
):

WordslabNotebook.chat


def chat(
    user_instruction:str
):
import importlib
import wordslab_notebooks_lib.chat
importlib.reload(wordslab_notebooks_lib.chat)
from wordslab_notebooks_lib.chat import OllamaModelClient, OpenRouterModelClient, Tools
notebook = WordslabNotebook()
notebook.chat("Using only the provided tools to make no mistake, what is (11545468+78782431)*418742?")

[Thinking] … thought in 1285 words

[Tool call] … add returned 90327899

[Tool call] … multiply returned 37824085083058

[Thinking] … thought in 191 words

37824085083058

def get_weather(
    city: str # A city name
) -> str: # A sentence describing the weather
    "A service predicting the weather city by city"
    return f"The weather is nice in {city} today"

You can use the service &get_weather.

notebook.chat("What will the weather be like tomorrow in Paris?")

[Thinking] … thought in 309 words

[Tool call] … get_weather returned The weather is nice in Paris today

[Thinking] … thought in 199 words

The weather is nice in Paris today.

notebook.chat("What's the name of my dog ?")

[Thinking] … thought in 272 words

Rex

Here is the check that the frontend extension will do before executing notebook.chat:

("notebook" in globals()) and ("WordslabNotebook" in str(type(notebook)))
True

Develop a Jupyterlab frontend extension

Understand Jupyterlab kernels and frontend extensions

Jupyter kernels technical implementation details

https://chatgpt.com/share/692bea08-4510-8004-b9ab-c02feeb97c08

Jupyterlab extension development tutorial

https://jupyterlab.readthedocs.io/en/latest/extension/extension_tutorial.html

Intialize the components of a frontend extension

The source code of the Jupyterlab frontend extension can be found in the following files:

Typescript source code, dependencies, and compilation config:

  • src/index.ts
  • package.json
  • tsconfig.json
  • .yarnrc.yml

Extension manifest and Javascript compiled code

  • wordslab_notebooks_lib/labextension
    • package.json
    • static/remoteEntry.97d57e417eaf8ebadeb6.js

This is how the extension files are included in the python package:

  • MANIFEST.in
include install.json
include package.json
recursive-include wordslab_notebooks_lib/labextension *

graft wordslab_notebooks_lib/labextension
graft src

This is how the extension files are installed in Jupyterlab extensions directory when the python package is installed:

  • pyproject.toml
[tool.setuptools]
include-package-data = true 

[tool.setuptools.data-files]
"share/jupyter/labextensions/wordslab-notebooks-lib" = [
  "wordslab_notebooks_lib/labextension/package.json",
  "install.json"
]
"share/jupyter/labextensions/wordslab-notebooks-lib/static" = [
  "wordslab_notebooks_lib/labextension/static/*"
]

This how the command jupyter labextension develop finds the directory where the extension files live:

  • wordslab_notebooks_lib\__init__.py
def _jupyter_labextension_paths():
    return [{
        "src": "labextension",
        "dest": "wordslab-notebooks-lib"
    }]

This is how the python package is identified as a Jupyterlab extension in pypi:

  • pyproject.toml
classifiers = [ "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt" ]

Install the Jupyterlab frontend extension in development mode

Open a Terminal

cd $WORDSLAB_WORKSPACE/wordslab-notebooks-lib
source $JUPYTERLAB_ENV/.venv/bin/activate

# Install Javascript dependencies
jlpm install

# Build TypeScript extension
jlpm build

# Register the extension with JupyterLab during development
# jupyter labextension develop . --overwrite
rm $JUPYTERLAB_ENV/.venv/share/jupyter/labextensions/wordslab-notebooks-lib
ln -s $WORDSLAB_WORKSPACE/wordslab-notebooks-lib/wordslab_notebooks_lib/labextension/ $JUPYTERLAB_ENV/.venv/share/jupyter/labextensions/wordslab-notebooks-lib

# Verify extension is found
jupyter labextension list

Test the Jupyterlab frontend extension

After installing the extension in development mode once, you can iterate fast: - update the code in src/index.ts - build the extension with jlpm build

cd $WORDSLAB_WORKSPACE/wordslab-notebooks-lib
source $JUPYTERLAB_ENV/.venv/bin/activate

# Build TypeScript extension
jlpm build
  • refresh the Jupyterlab single page app in your browser
  • test the updated extension

No need to reinstall the extension or to restart Jupyterlab itself, just refresh your browser page.

Explore the notebook format

https://nbformat.readthedocs.io/en/latest/format_description.html

nb = nbformat.from_dict(__notebook_content)

code_language = nb.metadata.language_info.name
print("> " + code_language + " notebook")

for cell in nb.cells:
    if cell.id == __cell_id: break
        
    is_markdown = cell.cell_type == "markdown"
    is_code = cell.cell_type == "code"
    is_raw = cell.cell_type == "raw"

    print("---------------------")
    print("cell", cell.id, cell.cell_type)
    print("---------------------")
    if is_markdown:
        print(cell.source[:100])
    elif is_code:
        print(f"```{code_language}\n" + cell.source[:100] + "\n```")
    elif is_raw:
        print(cell.source[:100])
    if is_code and cell.execution_count>0 and len(cell.outputs)>0:
        print("---------------------")
        print("cell outputs", cell.id, cell.execution_count)
        print("---------------------")
        for output in cell.outputs:
            if output.output_type == "stream":
                print(f"<{output.name}>")
                print(output.text[:100])
                print(f"</{output.name}>")
            elif output.output_type == "display_data":
                print("<display>")
                if "data" in output:
                    print("  <data>")
                    repr(output.data)
                    print("  </data>")
                if "metadata" in output and len(output.metadata)>0:
                    print("  <metadata>")
                    repr(output.metadata)
                    print("  </metadata>")
                print("</display>")
            elif output.output_type == "execute_result":
                print("<result>")
                if "data" in output:
                    print("  <data>")
                    print(output.data)
                    print("  </data>")
                if "metadata" in output and len(output.metadata)>0:
                    print("  <metadata>")
                    print(output.metadata)
                    print("  </metadata>")
                print("</result>")
            elif output.output_type == "error":
                print("<error>")
                print(output.ename)
                print(output.evalue)
                for frame in output.traceback:
                    print(frame)
                print("</error>")
        print("---------------------")
> python notebook
---------------------
cell 9d8a6aa0-8f58-4860-bcc1-2bfbdcb438b6 markdown
---------------------
# wordslab-notebooks-lib.jupyterlab

> Access wordslab-notebooks Jupyterlab extension version, curre
---------------------
cell 68f3493d-c252-4eb4-844b-abbd68ed3a70 markdown
---------------------
## Work together with AI in a Jupyterlab notebook - the Solveit method

A Jupyter notebook is a conv
---------------------
cell 0ff6fbdc-4a54-4e29-acbb-07529df8cfdd markdown
---------------------
## Install the Jupyterlab extension - wordslab-notebooks-lib

If you want to use "prompt" cells, you
---------------------
cell 65cd4cf8-d77b-4026-b428-bbd9550ea971 markdown
---------------------
## Communicate with the Jupyterlab extension
---------------------
cell ece4d545-8f78-4232-82fb-e837ea0185e4 code
---------------------
```python
#| export
import nbformat
```
---------------------
cell af2f3f45-f0da-4d37-9e39-b37d19ba5650 code
---------------------
```python
class JupyterlabNotebook:
    def __init__(self):
        if not "__wordslab_extension_version" in g
```
---------------------
cell 16180d26-5d5b-4c01-93d6-60c910f257bf code
---------------------
```python
notebook = JupyterlabNotebook()
```
---------------------
cell 13af8845-c7f8-440b-a80f-097c7cf1a541 code
---------------------
```python
notebook.jupyterlab_extension_version
```
---------------------
cell outputs 13af8845-c7f8-440b-a80f-097c7cf1a541 39
---------------------
<result>
  <data>
{'text/plain': "'0.0.11'"}
  </data>
</result>
---------------------
---------------------
cell 2906d67c-0764-4816-92c6-4063f17621aa code
---------------------
```python
notebook.path
```
---------------------
cell outputs 2906d67c-0764-4816-92c6-4063f17621aa 40
---------------------
<result>
  <data>
{'text/plain': "'wordslab-notebooks-lib/nbs/02_jupyterlab.ipynb'"}
  </data>
</result>
---------------------
---------------------
cell 45a764c9-e0ab-415c-947c-bdb12d26a2dd code
---------------------
```python
notebook.content.metadata
```
---------------------
cell outputs 45a764c9-e0ab-415c-947c-bdb12d26a2dd 41
---------------------
<result>
  <data>
{'text/plain': "{'kernelspec': {'display_name': 'wordslab-notebooks-lib',\n  'language': 'python',\n  'name': 'wordslab-notebooks-lib'},\n 'language_info': {'codemirror_mode': {'name': 'ipython', 'version': 3},\n  'file_extension': '.py',\n  'mimetype': 'text/x-python',\n  'name': 'python',\n  'nbconvert_exporter': 'python',\n  'pygments_lexer': 'ipython3',\n  'version': '3.12.12'}}"}
  </data>
</result>
---------------------
---------------------
cell d16ad869-d651-40bd-af2c-623d82b4edf0 code
---------------------
```python
notebook.cell_id
```
---------------------
cell outputs d16ad869-d651-40bd-af2c-623d82b4edf0 42
---------------------
<result>
  <data>
{'text/plain': "'d16ad869-d651-40bd-af2c-623d82b4edf0'"}
  </data>
</result>
---------------------
---------------------
cell 59347d6e-d1f7-4d23-b041-143e42887f6d code
---------------------
```python
notebook.cell_id
```
---------------------
cell outputs 59347d6e-d1f7-4d23-b041-143e42887f6d 43
---------------------
<result>
  <data>
{'text/plain': "'59347d6e-d1f7-4d23-b041-143e42887f6d'"}
  </data>
</result>
---------------------
---------------------
cell 9843c2c4-ac54-46d6-9725-0e957e944e3a markdown
---------------------
## Develop a Jupyterlab frontend extension
---------------------
cell 4178ac20-4612-4c8d-8d48-0fd2d2605aa9 markdown
---------------------
### Understand Jupyterlab kernels and frontend extensions

Jupyter kernels technical implementation 
---------------------
cell da7ecd61-80f6-4a00-a795-6866d62b32bb markdown
---------------------
### Intialize the components of a frontend extension

The source code of the Jupyterlab frontend ext
---------------------
cell 7a4146ce-96e0-4c13-9296-67374c833560 markdown
---------------------
### Install the Jupyterlab frontend extension in development mode

Open a Terminal

```bash
cd $WORD
---------------------
cell 6a891df5-84b9-4579-a1ec-18fd7a13ebc2 markdown
---------------------
### Test the Jupyterlab frontend extension 

After installing the extension in development mode once
---------------------
cell 61a8b5d6-7821-4c76-acde-9080fb8ad95b markdown
---------------------
## Explore the notebook format
---------------------
cell bfe108e4-6b27-403d-b5d7-fde736c1f01c markdown
---------------------
https://nbformat.readthedocs.io/en/latest/format_description.html