Skip to content

Commit 3286283

Browse files
3coinsdlqqq
authored andcommitted
Added run, select cell tools
1 parent f2cbd3b commit 3286283

File tree

3 files changed

+174
-64
lines changed

3 files changed

+174
-64
lines changed

jupyter_ai_jupyternaut/jupyternaut/jupyternaut.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,14 @@ def get_system_prompt(
182182
"""
183183
Returns the system prompt, including attachments as a string.
184184
"""
185+
186+
context = self.process_attachments(message) or ""
187+
context = f"User's username is '{message.sender}'\n\n" + context
188+
185189
system_msg_args = JupyternautSystemPromptArgs(
186190
model_id=model_id,
187191
persona_name=self.name,
188-
context=self.process_attachments(message),
192+
context=context,
189193
).model_dump()
190194

191195
return JUPYTERNAUT_SYSTEM_PROMPT_TEMPLATE.render(**system_msg_args)

jupyter_ai_jupyternaut/jupyternaut/toolkits/jupyterlab.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from typing import Optional
23
from jupyterlab_commands_toolkit.tools import execute_command
34

45
from .utils import get_serverapp
@@ -49,36 +50,49 @@ async def run_all_cells():
4950
return await execute_command("notebook:run-all-cells")
5051

5152

52-
async def get_active_cell_index() -> int:
53-
pass
53+
async def run_cell(cell_id: str, username: Optional[str] = None) -> dict:
54+
"""
55+
Runs a specific cell in the active notebook by selecting it and executing it.
56+
57+
Args:
58+
cell_id: The UUID of the cell to run, or a numeric index as string
59+
username: Optional username to get the active cell for that specific user
5460
55-
async def run_active_cell():
56-
"""Runs the currently selected/active cell"""
61+
Returns:
62+
dict: A dictionary containing the response from the run-cell command
5763
64+
Raises:
65+
ValueError: If the cell_id is not found in the notebook
66+
RuntimeError: If there is no active notebook or notebook is not currently open
67+
"""
68+
from .notebook import select_cell
69+
70+
# First, select the target cell
71+
await select_cell(cell_id, username)
72+
73+
# Then run the currently selected cell
5874
return await execute_command("notebook:run-cell")
5975

76+
6077
async def select_cell_below():
6178
"""
6279
Moves the cursor down to select the cell below the currently selected cell
6380
"""
6481
return await execute_command("notebook:move-cursor-down")
6582

83+
6684
async def select_cell_above():
6785
"""
6886
Moves the cursor up to select the cell above the currently selected cell
6987
"""
7088
return await execute_command("notebook:move-cursor-up")
7189

90+
7291
async def restart_kernel():
7392
"""
7493
Restarts the notebook kernel, useful when new packages are installed
7594
"""
7695
return await execute_command("notebook:restart-kernel")
7796

7897

79-
toolkit = [
80-
open_file,
81-
run_active_cell,
82-
run_all_cells,
83-
restart_kernel
84-
]
98+
toolkit = [open_file, run_cell, run_all_cells, restart_kernel]

jupyter_ai_jupyternaut/jupyternaut/toolkits/notebook.py

Lines changed: 145 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .utils import (
1515
get_file_id,
16+
get_global_awareness,
1617
get_jupyter_ydoc,
1718
normalize_filepath,
1819
)
@@ -862,6 +863,147 @@ def _safe_set_cursor(
862863
# Cursor positioning is a visual enhancement, not critical functionality
863864
pass
864865

866+
async def get_active_notebook(username: Optional[str] = None) -> Optional[str]:
867+
"""
868+
Returns path for the currently active notebook.
869+
870+
Args:
871+
username: Optional username to return a specific user's active notebook
872+
873+
Returns:
874+
File path for the first active notebook. If username is provided, then
875+
returns the active notebook for that specific user.
876+
"""
877+
awareness = await get_global_awareness()
878+
if not awareness:
879+
return
880+
for _, state in awareness.states.items():
881+
_username = state.get("user", {}).get("username", None)
882+
if(username and username != _username):
883+
continue
884+
885+
if active_notebook := state.get("current"):
886+
return active_notebook.replace("notebook:", "")
887+
888+
def _get_active_cell_id_from_ydoc(ydoc: YNotebook, username: Optional[str] = None) -> Optional[str]:
889+
"""Internal helper: Returns the active cell id from a ydoc instance
890+
891+
Args:
892+
ydoc: The YNotebook instance
893+
username: Optional username to return a specific user's active cell
894+
895+
Returns:
896+
The active cell ID for the notebook, or None if no active cell found
897+
"""
898+
if not ydoc or not ydoc.awareness:
899+
return None
900+
901+
awareness_states = ydoc.awareness.states
902+
for _, state in awareness_states.items():
903+
_username = state.get("user", {}).get("username", None)
904+
if(username and username != _username):
905+
continue
906+
907+
if active_cell_id := state.get("activeCellId"):
908+
return active_cell_id
909+
910+
return None
911+
912+
913+
async def get_active_cell_id(notebook_path: str, username: Optional[str] = None) -> Optional[str]:
914+
"""Returns the active cell id
915+
916+
Args:
917+
notebook_path: Path to the notebook file
918+
username: Optional username to return a specific user's active cell
919+
920+
Returns:
921+
The active cell ID for the notebook, or None if no active cell found
922+
"""
923+
file_path = normalize_filepath(notebook_path)
924+
file_id = await get_file_id(file_path)
925+
ydoc = await get_jupyter_ydoc(file_id)
926+
927+
return _get_active_cell_id_from_ydoc(ydoc, username)
928+
929+
930+
async def select_cell(cell_id: str, username: Optional[str] = None) -> dict:
931+
"""
932+
Selects a cell in the active notebook by navigating to it using cursor movements.
933+
934+
This function finds the target cell by ID and navigates to it from the currently
935+
active cell using select_cell_above or select_cell_below commands.
936+
937+
Args:
938+
cell_id: The UUID of the cell to select, or a numeric index as string
939+
username: Optional username to get the active cell for that specific user
940+
941+
Returns:
942+
dict: A dictionary containing the response from the last cursor movement
943+
944+
Raises:
945+
ValueError: If the cell_id is not found in the notebook
946+
RuntimeError: If there is no active notebook or notebook is not currently open
947+
"""
948+
from jupyterlab_commands_toolkit.tools import execute_command
949+
950+
try:
951+
# Get the active notebook path
952+
file_path = await get_active_notebook(username)
953+
if not file_path:
954+
raise RuntimeError(
955+
"No active notebook found. Please open a notebook first."
956+
)
957+
958+
# Resolve cell_id in case it's an index
959+
resolved_cell_id = await _resolve_cell_id(file_path, cell_id)
960+
961+
# Get the YDoc for the notebook
962+
file_id = await get_file_id(file_path)
963+
ydoc = await get_jupyter_ydoc(file_id)
964+
965+
if not ydoc:
966+
raise RuntimeError(f"Notebook at {file_path} is not currently open")
967+
968+
# Get the target cell index
969+
target_cell_index = _get_cell_index_from_id_ydoc(ydoc, resolved_cell_id)
970+
if target_cell_index is None:
971+
raise ValueError(f"Cell with ID {cell_id} not found in notebook")
972+
973+
# Get the currently active cell ID and index
974+
active_cell_id = _get_active_cell_id_from_ydoc(ydoc, username)
975+
if not active_cell_id:
976+
# If no active cell, we can't navigate - the notebook might need to be focused first
977+
raise RuntimeError(
978+
"No active cell found. Make sure the notebook is focused."
979+
)
980+
981+
active_cell_index = _get_cell_index_from_id_ydoc(ydoc, active_cell_id)
982+
if active_cell_index is None:
983+
raise RuntimeError(f"Active cell {active_cell_id} not found in notebook")
984+
985+
# Calculate the distance and direction to move
986+
distance = target_cell_index - active_cell_index
987+
988+
# If already at the target cell, no need to move
989+
if distance == 0:
990+
return {"success": True, "result": "Already at target cell"}
991+
992+
# Navigate to the target cell
993+
result = None
994+
if distance > 0:
995+
# Move down
996+
for _ in range(distance):
997+
result = await execute_command("notebook:move-cursor-down")
998+
else:
999+
# Move up
1000+
for _ in range(abs(distance)):
1001+
result = await execute_command("notebook:move-cursor-up")
1002+
1003+
return result
1004+
1005+
except Exception:
1006+
raise
8651007

8661008
async def edit_cell(file_path: str, cell_id: str, content: str) -> None:
8671009
"""Edits the content of a notebook cell with the specified ID
@@ -914,59 +1056,6 @@ async def edit_cell(file_path: str, cell_id: str, content: str) -> None:
9141056
except Exception:
9151057
raise
9161058

917-
918-
# Note: This is currently failing with server outputs, use `read_cell` instead
919-
def read_cell_nbformat(file_path: str, cell_id: str) -> Dict[str, Any]:
920-
"""Returns the content and metadata of a cell with the specified ID.
921-
922-
This function reads a specific cell from a Jupyter notebook file using the nbformat
923-
library and returns the cell's content and metadata.
924-
925-
Note: This function is currently not functioning properly with server outputs.
926-
Use `read_cell` instead.
927-
928-
Args:
929-
file_path:
930-
The relative path to the notebook file on the filesystem.
931-
cell_id:
932-
The UUID of the cell to read.
933-
934-
Returns:
935-
The cell as a dictionary containing its content and metadata.
936-
937-
Raises:
938-
ValueError: If no cell with the given ID is found.
939-
"""
940-
file_path = normalize_filepath(file_path)
941-
with open(file_path, "r", encoding="utf-8") as f:
942-
notebook = nbformat.read(f, as_version=nbformat.NO_CONVERT)
943-
944-
cell_index = _get_cell_index_from_id_nbformat(notebook, cell_id)
945-
if cell_index is not None:
946-
cell = notebook.cells[cell_index]
947-
return cell
948-
else:
949-
raise ValueError(f"Cell with {cell_id=} not found in notebook at {file_path=}")
950-
951-
952-
def _get_cell_index_from_id_json(notebook_json, cell_id: str) -> int | None:
953-
"""Get cell index from cell_id by notebook json dict.
954-
955-
Args:
956-
notebook_json:
957-
The notebook as a JSON dictionary.
958-
cell_id:
959-
The UUID of the cell to find.
960-
961-
Returns:
962-
The index of the cell in the notebook, or None if not found.
963-
"""
964-
for i, cell in enumerate(notebook_json["cells"]):
965-
if "id" in cell and cell["id"] == cell_id:
966-
return i
967-
return None
968-
969-
9701059
def _get_cell_index_from_id_ydoc(ydoc, cell_id: str) -> int | None:
9711060
"""Get cell index from cell_id using YDoc interface.
9721061
@@ -1084,4 +1173,7 @@ async def create_notebook(file_path: str) -> str:
10841173
delete_cell,
10851174
edit_cell,
10861175
create_notebook,
1176+
get_active_notebook,
1177+
get_active_cell_id,
1178+
select_cell
10871179
]

0 commit comments

Comments
 (0)