Skip to content

Commit f51c847

Browse files
committed
Add workspace directory context to file tools
Added cwd parameter to read, edit, and write tools to support relative file paths. The tools now resolve relative paths against the workspace directory, allowing the LLM to reference files without absolute paths. Changes: - Added cwd parameter to read(), edit(), and write() in default_toolkit.py - Updated _build_toolkit() to use functools.partial with a helper function - Replaced verbose wrapper functions with cleaner partial binding approach - All file tools now receive workspace directory context automatically
1 parent c1468ff commit f51c847

File tree

2 files changed

+35
-39
lines changed

2 files changed

+35
-39
lines changed

packages/jupyter-ai/jupyter_ai/personas/jupyternaut/jupyternaut.py

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
from typing import Optional
23
from jupyterlab_chat.models import Message
34

@@ -71,40 +72,19 @@ def _build_toolkit(self) -> Toolkit:
7172
# Get workspace directory for this chat
7273
workspace_dir = self.get_workspace_dir()
7374

74-
# Create wrapper functions that bind workspace_dir
75-
# We can't use functools.partial because litellm.function_to_dict expects __name__
76-
async def bash(command: str, timeout: Optional[int] = None) -> str:
77-
"""Executes a bash command and returns the result
78-
79-
Args:
80-
command: The bash command to execute
81-
timeout: Optional timeout in seconds
82-
83-
Returns:
84-
The command output (stdout and stderr combined)
85-
"""
86-
from ...tools.default_toolkit import bash as bash_orig
87-
return await bash_orig(command, timeout=timeout, cwd=workspace_dir)
88-
89-
async def search_grep(pattern: str, include: str = "*") -> str:
90-
"""Search for text patterns in files using ripgrep.
91-
92-
Args:
93-
pattern: A regular expression pattern to search for
94-
include: A glob pattern to filter which files to search
95-
96-
Returns:
97-
The raw output from ripgrep, including file paths, line numbers, and matching lines
98-
"""
99-
from ...tools.default_toolkit import search_grep as search_grep_orig
100-
return await search_grep_orig(pattern, include=include, cwd=workspace_dir)
75+
def bind_cwd(func, **kwargs):
76+
"""Create a partial function with custom __name__ and __doc__ preserved"""
77+
bound_func = partial(func, **kwargs)
78+
bound_func.__name__ = func.__name__
79+
bound_func.__doc__ = func.__doc__
80+
return bound_func
10181

10282
# Create toolkit with workspace-aware tools
10383
toolkit = Toolkit(name="jupyter-ai-contextual-toolkit")
104-
toolkit.add_tool(Tool(callable=bash))
105-
toolkit.add_tool(Tool(callable=search_grep))
106-
toolkit.add_tool(Tool(callable=read))
107-
toolkit.add_tool(Tool(callable=edit))
108-
toolkit.add_tool(Tool(callable=write))
84+
toolkit.add_tool(Tool(callable=bind_cwd(bash, cwd=workspace_dir)))
85+
toolkit.add_tool(Tool(callable=bind_cwd(search_grep, cwd=workspace_dir)))
86+
toolkit.add_tool(Tool(callable=bind_cwd(read, cwd=workspace_dir)))
87+
toolkit.add_tool(Tool(callable=bind_cwd(edit, cwd=workspace_dir)))
88+
toolkit.add_tool(Tool(callable=bind_cwd(write, cwd=workspace_dir)))
10989

11090
return toolkit

packages/jupyter-ai/jupyter_ai/tools/default_toolkit.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,23 @@
66
from .models import Tool, Toolkit
77

88

9-
def read(file_path: str, offset: int, limit: int) -> str:
9+
def read(file_path: str, offset: int, limit: int, cwd: Optional[str] = None) -> str:
1010
"""
1111
Read a subset of lines from a text file.
1212
1313
Parameters
1414
----------
1515
file_path : str
16-
Absolute path to the file that should be read.
16+
Path to the file that should be read. Can be absolute or relative to cwd.
1717
offset : int
1818
The line number at which to start reading (1-based indexing).
1919
limit : int
20-
Number of lines to read starting from *offset*.
20+
Number of lines to read starting from *offset.
2121
If *offset + limit* exceeds the number of lines in the file,
2222
all available lines after *offset* are returned.
23+
cwd : str, optional
24+
The directory to use as the base for relative paths. If not provided,
25+
file_path must be absolute.
2326
2427
Returns
2528
-------
@@ -33,6 +36,8 @@ def read(file_path: str, offset: int, limit: int) -> str:
3336
['third line\n', 'fourth line\n', 'fifth line\n', 'sixth line\n']
3437
"""
3538
path = pathlib.Path(file_path)
39+
if cwd and not path.is_absolute():
40+
path = pathlib.Path(cwd) / path
3641
if not path.is_file():
3742
raise FileNotFoundError(f"File not found: {file_path}")
3843

@@ -69,21 +74,25 @@ def edit(
6974
old_string: str,
7075
new_string: str,
7176
replace_all: bool = False,
77+
cwd: Optional[str] = None,
7278
) -> None:
7379
"""
7480
Replace occurrences of a substring in a file.
7581
7682
Parameters
7783
----------
7884
file_path : str
79-
Absolute path to the file that should be edited.
85+
Path to the file that should be edited. Can be absolute or relative to cwd.
8086
old_string : str
8187
Text that should be replaced.
8288
new_string : str
8389
Text that will replace *old_string*.
8490
replace_all : bool, optional
8591
If ``True`` all occurrences of *old_string* are replaced.
8692
If ``False`` (default), only the first occurrence in the file is replaced.
93+
cwd : str, optional
94+
The directory to use as the base for relative paths. If not provided,
95+
file_path must be absolute.
8796
8897
Returns
8998
-------
@@ -110,6 +119,8 @@ def edit(
110119
>>> edit('/tmp/test.txt', 'foo', 'bar', replace_all=True)
111120
"""
112121
path = pathlib.Path(file_path)
122+
if cwd and not path.is_absolute():
123+
path = pathlib.Path(cwd) / path
113124
if not path.is_file():
114125
raise FileNotFoundError(f"File not found: {file_path}")
115126

@@ -129,16 +140,19 @@ def edit(
129140
path.write_text(new_content, encoding="utf-8")
130141

131142

132-
def write(file_path: str, content: str) -> None:
143+
def write(file_path: str, content: str, cwd: Optional[str] = None) -> None:
133144
"""
134145
Write content to a file, creating it if it doesn't exist.
135146
136147
Parameters
137148
----------
138149
file_path : str
139-
Absolute path to the file that should be written.
150+
Path to the file that should be written. Can be absolute or relative to cwd.
140151
content : str
141152
Content to write to the file.
153+
cwd : str, optional
154+
The directory to use as the base for relative paths. If not provided,
155+
file_path must be absolute.
142156
143157
Returns
144158
-------
@@ -160,7 +174,9 @@ def write(file_path: str, content: str) -> None:
160174
>>> write('/tmp/data.json', '{"key": "value"}')
161175
"""
162176
path = pathlib.Path(file_path)
163-
177+
if cwd and not path.is_absolute():
178+
path = pathlib.Path(cwd) / path
179+
164180
# Write the content to the file
165181
path.write_text(content, encoding="utf-8")
166182

0 commit comments

Comments
 (0)