Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions jupyter_ai_tools/toolkits/file_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from jupyter_ai.tools.models import Tool, Toolkit

from ..utils import normalize_filepath


def read(file_path: str, offset: Optional[int] = None, limit: Optional[int] = None) -> str:
"""Reads a file from the local filesystem
Expand All @@ -21,6 +23,7 @@ def read(file_path: str, offset: Optional[int] = None, limit: Optional[int] = No
The contents of the file, potentially with line numbers
"""
try:
file_path = normalize_filepath(file_path)
if not os.path.exists(file_path):
return f"Error: File not found: {file_path}"

Expand Down Expand Up @@ -73,6 +76,7 @@ def write(file_path: str, content: str) -> str:
A success message or error message
"""
try:
file_path = normalize_filepath(file_path)
# Ensure the directory exists
directory = os.path.dirname(file_path)
if directory and not os.path.exists(directory):
Expand Down Expand Up @@ -107,6 +111,7 @@ def edit(file_path: str, old_string: str, new_string: str, replace_all: bool = F
A success message or error message
"""
try:
file_path = normalize_filepath(file_path)
if not os.path.exists(file_path):
return f"Error: File not found: {file_path}"

Expand Down Expand Up @@ -159,6 +164,7 @@ async def search_and_replace(
A success message or error message
"""
try:
file_path = normalize_filepath(file_path)
if not os.path.exists(file_path):
return f"Error: File not found: {file_path}"

Expand Down Expand Up @@ -212,7 +218,7 @@ async def glob(pattern: str, path: Optional[str] = None) -> str:
A list of matching file paths sorted by modification time
"""
try:
search_path = path or os.getcwd()
search_path = normalize_filepath(path) if path else os.getcwd()
if not os.path.exists(search_path):
return f"Error: Path not found: {search_path}"

Expand Down Expand Up @@ -260,7 +266,7 @@ async def grep(
A list of file paths with at least one match
"""
try:
search_path = path or os.getcwd()
search_path = normalize_filepath(path) if path else os.getcwd()
if not os.path.exists(search_path):
return [f"Error: Path not found: {search_path}"]

Expand Down Expand Up @@ -312,6 +318,7 @@ async def ls(path: str, ignore: Optional[List[str]] = None) -> str:
A list of files and directories in the given path
"""
try:
path = normalize_filepath(path)
if not os.path.exists(path):
return f"Error: Path not found: {path}"

Expand Down
10 changes: 10 additions & 0 deletions jupyter_ai_tools/toolkits/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from jupyter_ai.tools.models import Tool, Toolkit
from jupyterlab_git.git import Git

from ..utils import normalize_filepath

git = Git()


Expand All @@ -19,6 +21,7 @@ async def git_clone(path: str, url: str) -> str:
Returns:
str: Success or error message.
"""
path = normalize_filepath(path)
res = await git.clone(path, repo_url=url)
if res["code"] == 0:
return f"✅ Cloned repo into {res['path']}"
Expand All @@ -36,6 +39,7 @@ async def git_status(path: str) -> str:
Returns:
str: A JSON-formatted string of status or an error message.
"""
path = normalize_filepath(path)
res = await git.status(path)
if res["code"] == 0:
return f"📋 Status:\n{json.dumps(res, indent=2)}"
Expand All @@ -54,6 +58,7 @@ async def git_log(path: str, history_count: int = 10) -> str:
Returns:
str: A JSON-formatted commit log or error message.
"""
path = normalize_filepath(path)
res = await git.log(path, history_count=history_count)
if res["code"] == 0:
return f"🕓 Recent commits:\n{json.dumps(res, indent=2)}"
Expand All @@ -71,6 +76,7 @@ async def git_pull(path: str) -> str:
Returns:
str: Success or error message.
"""
path = normalize_filepath(path)
res = await git.pull(path)
return (
"✅ Pulled latest changes."
Expand All @@ -91,6 +97,7 @@ async def git_push(path: str, branch: str) -> str:
Returns:
str: Success or error message.
"""
path = normalize_filepath(path)
res = await git.push(remote="origin", branch=branch, path=path)
return (
"✅ Pushed changes."
Expand All @@ -111,6 +118,7 @@ async def git_commit(path: str, message: str) -> str:
Returns:
str: Success or error message.
"""
path = normalize_filepath(path)
res = await git.commit(commit_msg=message, amend=False, path=path)
return (
"✅ Commit successful."
Expand All @@ -132,6 +140,7 @@ async def git_add(path: str, add_all: bool = True, filename: str = "") -> str:
Returns:
str: Success or error message.
"""
path = normalize_filepath(path)
if add_all:
res = await git.add_all(path)
elif filename:
Expand All @@ -158,6 +167,7 @@ async def git_get_repo_root(path: str) -> str:
Returns:
str: The path to the Git repository root or an error message.
"""
path = normalize_filepath(path)
dir_path = os.path.dirname(path)
res = await git.show_top_level(dir_path)
if res["code"] == 0 and res.get("path"):
Expand Down
57 changes: 51 additions & 6 deletions jupyter_ai_tools/toolkits/notebook.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import difflib
import json
import os
import re
from typing import Any, Dict, Literal, Optional, Tuple

Expand All @@ -13,6 +14,7 @@
cell_to_md,
get_file_id,
get_jupyter_ydoc,
normalize_filepath,
notebook_json_to_md,
)

Expand Down Expand Up @@ -70,6 +72,7 @@ async def read_notebook(file_path: str, include_outputs=False) -> str:
The notebook content as a markdown string.
"""
try:
file_path = normalize_filepath(file_path)
notebook_dict = await read_notebook_json(file_path)
notebook_md = notebook_json_to_md(notebook_dict, include_outputs=include_outputs)
return notebook_md
Expand All @@ -91,6 +94,7 @@ async def read_notebook_json(file_path: str) -> Dict[str, Any]:
A dictionary containing the complete notebook structure.
"""
try:
file_path = normalize_filepath(file_path)
with open(file_path, "r", encoding="utf-8") as f:
notebook_dict = json.load(f)
return notebook_dict
Expand Down Expand Up @@ -120,6 +124,7 @@ async def read_cell(file_path: str, cell_id: str, include_outputs: bool = True)
LookupError: If no cell with the given ID is found.
"""
try:
file_path = normalize_filepath(file_path)
# Resolve cell_id in case it's an index
resolved_cell_id = await _resolve_cell_id(file_path, cell_id)
cell, cell_index = await read_cell_json(file_path, resolved_cell_id)
Expand Down Expand Up @@ -150,6 +155,7 @@ async def read_cell_json(file_path: str, cell_id: str) -> Tuple[Dict[str, Any],
LookupError: If no cell with the given ID is found.
"""
try:
file_path = normalize_filepath(file_path)
# Resolve cell_id in case it's an index
resolved_cell_id = await _resolve_cell_id(file_path, cell_id)
notebook_json = await read_notebook_json(file_path)
Expand Down Expand Up @@ -182,7 +188,7 @@ async def get_cell_id_from_index(file_path: str, cell_index: int) -> str:
or if the cell does not have an ID.
"""
try:

file_path = normalize_filepath(file_path)
cell_id = None
notebook_json = await read_notebook_json(file_path)
cells = notebook_json["cells"]
Expand Down Expand Up @@ -233,7 +239,7 @@ async def add_cell(
None
"""
try:

file_path = normalize_filepath(file_path)
# Resolve cell_id in case it's an index
resolved_cell_id = await _resolve_cell_id(file_path, cell_id) if cell_id else None

Expand Down Expand Up @@ -304,7 +310,7 @@ async def insert_cell(
None
"""
try:

file_path = normalize_filepath(file_path)
file_id = await get_file_id(file_path)
ydoc = await get_jupyter_ydoc(file_id)

Expand Down Expand Up @@ -357,7 +363,7 @@ async def delete_cell(file_path: str, cell_id: str):
None
"""
try:

file_path = normalize_filepath(file_path)
# Resolve cell_id in case it's an index
resolved_cell_id = await _resolve_cell_id(file_path, cell_id)

Expand Down Expand Up @@ -762,7 +768,7 @@ async def edit_cell(file_path: str, cell_id: str, content: str) -> None:
ValueError: If the cell_id is not found in the notebook.
"""
try:

file_path = normalize_filepath(file_path)
# Resolve cell_id in case it's an index
resolved_cell_id = await _resolve_cell_id(file_path, cell_id)

Expand Down Expand Up @@ -814,7 +820,7 @@ def read_cell_nbformat(file_path: str, cell_id: str) -> Dict[str, Any]:
Raises:
ValueError: If no cell with the given ID is found.
"""

file_path = normalize_filepath(file_path)
with open(file_path, "r", encoding="utf-8") as f:
notebook = nbformat.read(f, as_version=nbformat.NO_CONVERT)

Expand Down Expand Up @@ -906,6 +912,44 @@ def _determine_insert_index(cells_count: int, cell_index: Optional[int], add_abo
return insert_index


async def create_notebook(file_path: str) -> str:
"""Creates a new empty Jupyter notebook at the specified file path.

This function creates a new empty notebook with proper nbformat structure.
If the file already exists, it will return an error message.

Args:
file_path:
The path where the new notebook should be created.

Returns:
A success message or error message.
"""
try:
file_path = normalize_filepath(file_path)

# Check if file already exists
if os.path.exists(file_path):
return f"Error: File already exists at {file_path}"

# Ensure the directory exists
directory = os.path.dirname(file_path)
if directory and not os.path.exists(directory):
os.makedirs(directory, exist_ok=True)

# Create a new empty notebook
notebook = nbformat.v4.new_notebook()

# Write the notebook to the file
with open(file_path, "w", encoding="utf-8") as f:
nbformat.write(notebook, f)

return f"Successfully created new notebook at {file_path}"

except Exception as e:
return f"Error: Failed to create notebook: {str(e)}"



toolkit = Toolkit(
name="notebook_toolkit",
Expand All @@ -918,3 +962,4 @@ def _determine_insert_index(cells_count: int, cell_index: Optional[int], add_abo
toolkit.add_tool(Tool(callable=delete_cell, delete=True))
toolkit.add_tool(Tool(callable=edit_cell, read=True, write=True))
toolkit.add_tool(Tool(callable=get_cell_id_from_index, read=True))
toolkit.add_tool(Tool(callable=create_notebook, write=True))
Loading
Loading