mojentic 0.7.1__py3-none-any.whl → 0.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _examples/broker_as_tool.py +13 -10
- _examples/coding_file_tool.py +170 -77
- _examples/file_tool.py +5 -3
- mojentic/__init__.py +2 -7
- mojentic/agents/__init__.py +11 -2
- mojentic/context/__init__.py +4 -0
- mojentic/llm/__init__.py +14 -2
- mojentic/llm/gateways/__init__.py +22 -0
- mojentic/llm/message_composers.py +1 -1
- mojentic/llm/registry/__init__.py +6 -0
- mojentic/llm/registry/populate_registry_from_ollama.py +13 -12
- mojentic/llm/tools/__init__.py +18 -0
- mojentic/llm/tools/date_resolver.py +5 -2
- mojentic/llm/tools/ephemeral_task_manager/__init__.py +8 -8
- mojentic/llm/tools/file_manager.py +603 -42
- mojentic/llm/tools/file_manager_spec.py +723 -0
- mojentic/llm/tools/tool_wrapper.py +7 -3
- mojentic/tracer/__init__.py +8 -3
- {mojentic-0.7.1.dist-info → mojentic-0.7.2.dist-info}/METADATA +3 -2
- {mojentic-0.7.1.dist-info → mojentic-0.7.2.dist-info}/RECORD +23 -22
- {mojentic-0.7.1.dist-info → mojentic-0.7.2.dist-info}/WHEEL +0 -0
- {mojentic-0.7.1.dist-info → mojentic-0.7.2.dist-info}/licenses/LICENSE.md +0 -0
- {mojentic-0.7.1.dist-info → mojentic-0.7.2.dist-info}/top_level.txt +0 -0
|
@@ -1,41 +1,306 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
3
|
+
import glob
|
|
4
|
+
import difflib
|
|
2
5
|
|
|
3
6
|
from mojentic.llm.tools.llm_tool import LLMTool
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
class FilesystemGateway:
|
|
7
|
-
def
|
|
8
|
-
|
|
10
|
+
def __init__(self, base_path: str):
|
|
11
|
+
self.base_path = base_path
|
|
9
12
|
|
|
10
|
-
def
|
|
11
|
-
|
|
13
|
+
def _resolve_path(self, path: str) -> str:
|
|
14
|
+
"""Resolve a path relative to the base path."""
|
|
15
|
+
# Ensure the path is within the sandbox
|
|
16
|
+
resolved_path = os.path.normpath(os.path.join(self.base_path, path))
|
|
17
|
+
# Verify the path is within the sandbox
|
|
18
|
+
if not resolved_path.startswith(os.path.normpath(self.base_path)):
|
|
19
|
+
raise ValueError(f"Path {path} attempts to escape the sandbox")
|
|
20
|
+
return resolved_path
|
|
21
|
+
|
|
22
|
+
def ls(self, path: str) -> list[str]:
|
|
23
|
+
resolved_path = self._resolve_path(path)
|
|
24
|
+
files = os.listdir(resolved_path)
|
|
25
|
+
|
|
26
|
+
# Convert the filenames to paths relative to the base_path using list comprehension
|
|
27
|
+
relative_files = [os.path.relpath(os.path.join(resolved_path, file), self.base_path)
|
|
28
|
+
for file in files]
|
|
29
|
+
|
|
30
|
+
return relative_files
|
|
31
|
+
|
|
32
|
+
def list_all_files(self, path: str) -> list[str]:
|
|
33
|
+
"""List all files recursively in the given path."""
|
|
34
|
+
resolved_path = self._resolve_path(path)
|
|
35
|
+
all_files = []
|
|
36
|
+
|
|
37
|
+
for root, _, files in os.walk(resolved_path):
|
|
38
|
+
for file in files:
|
|
39
|
+
full_path = os.path.join(root, file)
|
|
40
|
+
relative_path = os.path.relpath(full_path, self.base_path)
|
|
41
|
+
all_files.append(relative_path)
|
|
42
|
+
|
|
43
|
+
return all_files
|
|
44
|
+
|
|
45
|
+
def find_files_by_glob(self, path: str, pattern: str) -> list[str]:
|
|
46
|
+
"""Find files matching a glob pattern."""
|
|
47
|
+
resolved_path = self._resolve_path(path)
|
|
48
|
+
# Use glob to find files matching the pattern
|
|
49
|
+
matching_files = glob.glob(os.path.join(resolved_path, pattern), recursive=True)
|
|
50
|
+
|
|
51
|
+
# Convert to paths relative to base_path
|
|
52
|
+
relative_files = [os.path.relpath(file, self.base_path) for file in matching_files]
|
|
53
|
+
return relative_files
|
|
54
|
+
|
|
55
|
+
def find_files_containing(self, path: str, pattern: str) -> list[str]:
|
|
56
|
+
"""Find files containing text matching a regex pattern."""
|
|
57
|
+
resolved_path = self._resolve_path(path)
|
|
58
|
+
matching_files = []
|
|
59
|
+
regex = re.compile(pattern)
|
|
60
|
+
|
|
61
|
+
for root, _, files in os.walk(resolved_path):
|
|
62
|
+
for file in files:
|
|
63
|
+
full_path = os.path.join(root, file)
|
|
64
|
+
try:
|
|
65
|
+
with open(full_path, 'r', errors='ignore') as f:
|
|
66
|
+
content = f.read()
|
|
67
|
+
if regex.search(content):
|
|
68
|
+
relative_path = os.path.relpath(full_path, self.base_path)
|
|
69
|
+
matching_files.append(relative_path)
|
|
70
|
+
except (IOError, UnicodeDecodeError):
|
|
71
|
+
# Skip files that can't be read as text
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
return matching_files
|
|
75
|
+
|
|
76
|
+
def find_lines_matching(self, path: str, file_name: str, pattern: str) -> list[dict]:
|
|
77
|
+
"""Find all lines in a file matching a regex pattern."""
|
|
78
|
+
resolved_path = self._resolve_path(path)
|
|
79
|
+
file_path = os.path.join(resolved_path, file_name)
|
|
80
|
+
matching_lines = []
|
|
81
|
+
regex = re.compile(pattern)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
with open(file_path, 'r', errors='ignore') as f:
|
|
85
|
+
for i, line in enumerate(f, 1):
|
|
86
|
+
if regex.search(line):
|
|
87
|
+
matching_lines.append({
|
|
88
|
+
'line_number': i,
|
|
89
|
+
'content': line.rstrip('\n')
|
|
90
|
+
})
|
|
91
|
+
except (IOError, UnicodeDecodeError) as e:
|
|
92
|
+
raise ValueError(f"Error reading file {file_name}: {str(e)}")
|
|
93
|
+
|
|
94
|
+
return matching_lines
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def edit_file_with_diff(self, path: str, file_name: str, diff: str) -> str:
|
|
98
|
+
"""Edit a file by applying a diff to it."""
|
|
99
|
+
resolved_path = self._resolve_path(path)
|
|
100
|
+
file_path = os.path.join(resolved_path, file_name)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Read the original content
|
|
104
|
+
with open(file_path, 'r') as f:
|
|
105
|
+
original_content = f.read()
|
|
106
|
+
|
|
107
|
+
# Check if the original file ends with a newline
|
|
108
|
+
ends_with_newline = original_content.endswith('\n')
|
|
109
|
+
|
|
110
|
+
# Apply the diff using difflib's unified_diff parser
|
|
111
|
+
original_lines = original_content.splitlines()
|
|
112
|
+
patched_content = self._apply_unified_diff(original_lines, diff)
|
|
113
|
+
|
|
114
|
+
# Preserve the trailing newline if it existed
|
|
115
|
+
if ends_with_newline and not patched_content.endswith('\n'):
|
|
116
|
+
patched_content += '\n'
|
|
117
|
+
|
|
118
|
+
# Write the modified content
|
|
119
|
+
with open(file_path, 'w') as f:
|
|
120
|
+
f.write(patched_content)
|
|
121
|
+
|
|
122
|
+
return f"Successfully applied diff to {file_name}"
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise ValueError(f"Error applying diff to {file_name}: {str(e)}")
|
|
125
|
+
|
|
126
|
+
def _apply_unified_diff(self, original_lines, diff_text):
|
|
127
|
+
"""Apply a unified diff to the original lines."""
|
|
128
|
+
# Split the diff into lines
|
|
129
|
+
diff_lines = diff_text.splitlines()
|
|
130
|
+
|
|
131
|
+
# Create a copy of the original lines to modify
|
|
132
|
+
result_lines = original_lines.copy()
|
|
133
|
+
|
|
134
|
+
# Parse the diff and apply changes
|
|
135
|
+
i = 0
|
|
136
|
+
while i < len(diff_lines):
|
|
137
|
+
line = diff_lines[i]
|
|
138
|
+
|
|
139
|
+
# Skip file headers
|
|
140
|
+
if line.startswith('---') or line.startswith('+++'):
|
|
141
|
+
i += 1
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# Parse hunk header
|
|
145
|
+
if line.startswith('@@'):
|
|
146
|
+
# Format: @@ -start,count +start,count @@
|
|
147
|
+
match = re.match(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@', line)
|
|
148
|
+
if not match:
|
|
149
|
+
# Try alternative format without counts
|
|
150
|
+
match = re.match(r'@@ -(\d+) \+(\d+) @@', line)
|
|
151
|
+
if match:
|
|
152
|
+
start_line = int(match.group(1))
|
|
153
|
+
start_line_new = int(match.group(2))
|
|
154
|
+
else:
|
|
155
|
+
# Skip invalid hunk header
|
|
156
|
+
i += 1
|
|
157
|
+
continue
|
|
158
|
+
else:
|
|
159
|
+
start_line = int(match.group(1))
|
|
160
|
+
start_line_new = int(match.group(3))
|
|
161
|
+
|
|
162
|
+
# Line numbers in diff are 1-based, but our array is 0-based
|
|
163
|
+
start_line -= 1
|
|
164
|
+
|
|
165
|
+
# Extract the hunk content
|
|
166
|
+
hunk_lines = []
|
|
167
|
+
i += 1
|
|
168
|
+
while i < len(diff_lines) and not diff_lines[i].startswith('@@'):
|
|
169
|
+
hunk_lines.append(diff_lines[i])
|
|
170
|
+
i += 1
|
|
171
|
+
|
|
172
|
+
# Apply this hunk
|
|
173
|
+
self._apply_hunk(result_lines, hunk_lines, start_line)
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
i += 1
|
|
177
|
+
|
|
178
|
+
# Join the result lines into a single string
|
|
179
|
+
return '\n'.join(result_lines)
|
|
180
|
+
|
|
181
|
+
def _apply_hunk(self, result_lines, hunk_lines, start_line):
|
|
182
|
+
"""Apply a single hunk to the result_lines list."""
|
|
183
|
+
# Find the actual position in the file by matching context lines
|
|
184
|
+
actual_pos = self._find_hunk_position(result_lines, hunk_lines, start_line)
|
|
185
|
+
if actual_pos == -1:
|
|
186
|
+
# Could not find the position, use the start_line as a fallback
|
|
187
|
+
actual_pos = start_line
|
|
188
|
+
|
|
189
|
+
# Apply the changes
|
|
190
|
+
removed = 0
|
|
191
|
+
added = 0
|
|
192
|
+
pos = actual_pos
|
|
193
|
+
|
|
194
|
+
for line in hunk_lines:
|
|
195
|
+
if line.startswith('-'):
|
|
196
|
+
# Remove line
|
|
197
|
+
if pos < len(result_lines):
|
|
198
|
+
result_lines.pop(pos)
|
|
199
|
+
removed += 1
|
|
200
|
+
# Don't increment pos as we're removing this line
|
|
201
|
+
elif line.startswith('+'):
|
|
202
|
+
# Add line
|
|
203
|
+
result_lines.insert(pos, line[1:])
|
|
204
|
+
pos += 1
|
|
205
|
+
added += 1
|
|
206
|
+
else:
|
|
207
|
+
# Context line (unchanged)
|
|
208
|
+
if line.startswith(' '):
|
|
209
|
+
line = line[1:]
|
|
210
|
+
pos += 1
|
|
211
|
+
|
|
212
|
+
def _find_hunk_position(self, result_lines, hunk_lines, start_line):
|
|
213
|
+
"""Find the actual position of a hunk in the file by matching context lines."""
|
|
214
|
+
# Extract context lines from the beginning of the hunk
|
|
215
|
+
context_lines = []
|
|
216
|
+
for line in hunk_lines:
|
|
217
|
+
if line.startswith(' '):
|
|
218
|
+
context_lines.append(line[1:])
|
|
219
|
+
else:
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
if not context_lines:
|
|
223
|
+
return start_line
|
|
224
|
+
|
|
225
|
+
# Try to find the context lines in the file
|
|
226
|
+
for i in range(max(0, start_line - 10), min(len(result_lines), start_line + 10)):
|
|
227
|
+
if i + len(context_lines) <= len(result_lines):
|
|
228
|
+
match = True
|
|
229
|
+
for j, context_line in enumerate(context_lines):
|
|
230
|
+
if result_lines[i + j] != context_line:
|
|
231
|
+
match = False
|
|
232
|
+
break
|
|
233
|
+
if match:
|
|
234
|
+
return i
|
|
235
|
+
|
|
236
|
+
return -1
|
|
237
|
+
|
|
238
|
+
def read(self, path: str, file_name: str) -> str:
|
|
239
|
+
resolved_path = self._resolve_path(path)
|
|
240
|
+
with open(os.path.join(resolved_path, file_name), 'r') as f:
|
|
12
241
|
return f.read()
|
|
13
242
|
|
|
14
|
-
def write(self, path, file_name, content):
|
|
15
|
-
|
|
243
|
+
def write(self, path: str, file_name: str, content: str) -> None:
|
|
244
|
+
resolved_path = self._resolve_path(path)
|
|
245
|
+
with open(os.path.join(resolved_path, file_name), 'w') as f:
|
|
16
246
|
f.write(content)
|
|
17
247
|
|
|
18
248
|
|
|
19
249
|
class FileManager:
|
|
20
|
-
def __init__(self,
|
|
21
|
-
self.
|
|
22
|
-
self.fs = fs or FilesystemGateway()
|
|
250
|
+
def __init__(self, fs: FilesystemGateway):
|
|
251
|
+
self.fs = fs
|
|
23
252
|
|
|
24
|
-
def ls(self, extension):
|
|
25
|
-
entries = self.fs.ls(
|
|
253
|
+
def ls(self, path: str, extension: str = None) -> list[str]:
|
|
254
|
+
entries = self.fs.ls(path)
|
|
255
|
+
if extension is None:
|
|
256
|
+
return entries
|
|
26
257
|
return [f for f in entries if f.endswith(extension)]
|
|
27
258
|
|
|
28
|
-
def
|
|
29
|
-
|
|
259
|
+
def list_all_files(self, path: str) -> list[str]:
|
|
260
|
+
"""List all files recursively in the sandbox."""
|
|
261
|
+
return self.fs.list_all_files(path)
|
|
30
262
|
|
|
31
|
-
def
|
|
32
|
-
|
|
263
|
+
def find_files_by_glob(self, path: str, pattern: str) -> list[str]:
|
|
264
|
+
"""Find files matching a glob pattern."""
|
|
265
|
+
return self.fs.find_files_by_glob(path, pattern)
|
|
33
266
|
|
|
267
|
+
def find_files_containing(self, path: str, pattern: str) -> list[str]:
|
|
268
|
+
"""Find files containing text matching a regex pattern."""
|
|
269
|
+
return self.fs.find_files_containing(path, pattern)
|
|
34
270
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
271
|
+
def find_lines_matching(self, path: str, file_name: str, pattern: str) -> list[dict]:
|
|
272
|
+
"""Find all lines in a file matching a regex pattern."""
|
|
273
|
+
return self.fs.find_lines_matching(path, file_name, pattern)
|
|
274
|
+
|
|
275
|
+
def edit_file_with_diff(self, path: str, file_name: str, diff: str) -> str:
|
|
276
|
+
"""Edit a file by applying a diff to it."""
|
|
277
|
+
return self.fs.edit_file_with_diff(path, file_name, diff)
|
|
278
|
+
|
|
279
|
+
def read(self, path: str, file_name: str) -> str:
|
|
280
|
+
return self.fs.read(path, file_name)
|
|
281
|
+
|
|
282
|
+
def write(self, path: str, file_name: str, content: str) -> None:
|
|
283
|
+
self.fs.write(path, file_name, content)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class ListFilesTool(LLMTool):
|
|
287
|
+
def __init__(self, fs: FilesystemGateway):
|
|
288
|
+
self.fs = fs
|
|
289
|
+
|
|
290
|
+
def run(self, path: str, extension: str = None) -> list[str]:
|
|
291
|
+
try:
|
|
292
|
+
entries = self.fs.ls(path)
|
|
293
|
+
if extension is None:
|
|
294
|
+
return entries
|
|
295
|
+
return [f for f in entries if f.endswith(extension)]
|
|
296
|
+
except ValueError as e:
|
|
297
|
+
return f"Error: {str(e)}"
|
|
298
|
+
except FileNotFoundError:
|
|
299
|
+
return f"Error: Directory '{path}' not found"
|
|
300
|
+
except PermissionError:
|
|
301
|
+
return f"Error: Permission denied when accessing directory '{path}'"
|
|
302
|
+
except Exception as e:
|
|
303
|
+
return f"Error listing files in '{path}': {str(e)}"
|
|
39
304
|
|
|
40
305
|
@property
|
|
41
306
|
def descriptor(self):
|
|
@@ -43,29 +308,47 @@ class ListFilesTool(FileManager):
|
|
|
43
308
|
"type": "function",
|
|
44
309
|
"function": {
|
|
45
310
|
"name": "list_files",
|
|
46
|
-
"description": "List files in
|
|
311
|
+
"description": "List files in the specified directory (non-recursive), optionally filtered by extension. Use this when you need to see what files are available in a specific directory without including subdirectories.",
|
|
47
312
|
"parameters": {
|
|
48
313
|
"type": "object",
|
|
49
314
|
"properties": {
|
|
315
|
+
"path": {
|
|
316
|
+
"type": "string",
|
|
317
|
+
"description": "The path relative to the sandbox root to list files from. For example, '.' for the root directory, 'src' for the src directory, or 'docs/images' for a nested directory."
|
|
318
|
+
},
|
|
50
319
|
"extension": {
|
|
51
320
|
"type": "string",
|
|
52
|
-
"description": "The file extension to filter by."
|
|
321
|
+
"description": "The file extension to filter by (e.g., '.py', '.txt', '.md'). If not provided, all files will be listed. For example, using '.py' will only list Python files in the directory."
|
|
53
322
|
}
|
|
54
323
|
},
|
|
55
324
|
"additionalProperties": False,
|
|
56
|
-
"required": ["
|
|
325
|
+
"required": ["path"]
|
|
57
326
|
},
|
|
58
327
|
},
|
|
59
328
|
}
|
|
60
329
|
|
|
61
330
|
|
|
62
331
|
class ReadFileTool(LLMTool):
|
|
63
|
-
def __init__(self,
|
|
64
|
-
self.
|
|
65
|
-
self.fs = fs or FilesystemGateway()
|
|
332
|
+
def __init__(self, fs: FilesystemGateway):
|
|
333
|
+
self.fs = fs
|
|
66
334
|
|
|
67
|
-
def run(self,
|
|
68
|
-
|
|
335
|
+
def run(self, path: str) -> str:
|
|
336
|
+
try:
|
|
337
|
+
# Split the path into directory and filename
|
|
338
|
+
directory, file_name = os.path.split(path)
|
|
339
|
+
return self.fs.read(directory, file_name)
|
|
340
|
+
except ValueError as e:
|
|
341
|
+
return f"Error: {str(e)}"
|
|
342
|
+
except FileNotFoundError:
|
|
343
|
+
return f"Error: File '{path}' not found"
|
|
344
|
+
except PermissionError:
|
|
345
|
+
return f"Error: Permission denied when accessing file '{path}'"
|
|
346
|
+
except IOError as e:
|
|
347
|
+
return f"Error reading file '{path}': {str(e)}"
|
|
348
|
+
except UnicodeDecodeError:
|
|
349
|
+
return f"Error: File '{path}' contains non-text content that cannot be read as text"
|
|
350
|
+
except Exception as e:
|
|
351
|
+
return f"Error reading file '{path}': {str(e)}"
|
|
69
352
|
|
|
70
353
|
@property
|
|
71
354
|
def descriptor(self):
|
|
@@ -73,30 +356,42 @@ class ReadFileTool(LLMTool):
|
|
|
73
356
|
"type": "function",
|
|
74
357
|
"function": {
|
|
75
358
|
"name": "read_file",
|
|
76
|
-
"description": "Read the content of a file.",
|
|
359
|
+
"description": "Read the entire content of a file as a string. Use this when you need to access or analyze the complete contents of a file.",
|
|
77
360
|
"parameters": {
|
|
78
361
|
"type": "object",
|
|
79
362
|
"properties": {
|
|
80
|
-
"
|
|
363
|
+
"path": {
|
|
81
364
|
"type": "string",
|
|
82
|
-
"description": "The
|
|
365
|
+
"description": "The full relative path including the filename of the file to read. For example, 'README.md' for a file in the root directory, 'src/main.py' for a file in the src directory, or 'docs/images/diagram.png' for a file in a nested directory."
|
|
83
366
|
}
|
|
84
367
|
},
|
|
85
368
|
"additionalProperties": False,
|
|
86
|
-
"required": ["
|
|
369
|
+
"required": ["path"]
|
|
87
370
|
},
|
|
88
371
|
},
|
|
89
372
|
}
|
|
90
373
|
|
|
91
374
|
|
|
92
375
|
class WriteFileTool(LLMTool):
|
|
93
|
-
def __init__(self,
|
|
94
|
-
self.
|
|
95
|
-
self.fs = fs or FilesystemGateway()
|
|
376
|
+
def __init__(self, fs: FilesystemGateway):
|
|
377
|
+
self.fs = fs
|
|
96
378
|
|
|
97
|
-
def run(self,
|
|
98
|
-
|
|
99
|
-
|
|
379
|
+
def run(self, path: str, content: str) -> str:
|
|
380
|
+
try:
|
|
381
|
+
# Split the path into directory and filename
|
|
382
|
+
directory, file_name = os.path.split(path)
|
|
383
|
+
self.fs.write(directory, file_name, content)
|
|
384
|
+
return f"Successfully wrote to {path}"
|
|
385
|
+
except ValueError as e:
|
|
386
|
+
return f"Error: {str(e)}"
|
|
387
|
+
except FileNotFoundError:
|
|
388
|
+
return f"Error: Directory not found. Cannot write file '{path}'"
|
|
389
|
+
except PermissionError:
|
|
390
|
+
return f"Error: Permission denied when writing to file '{path}'"
|
|
391
|
+
except IOError as e:
|
|
392
|
+
return f"Error writing to file '{path}': {str(e)}"
|
|
393
|
+
except Exception as e:
|
|
394
|
+
return f"Error writing to file '{path}': {str(e)}"
|
|
100
395
|
|
|
101
396
|
@property
|
|
102
397
|
def descriptor(self):
|
|
@@ -104,21 +399,287 @@ class WriteFileTool(LLMTool):
|
|
|
104
399
|
"type": "function",
|
|
105
400
|
"function": {
|
|
106
401
|
"name": "write_file",
|
|
107
|
-
"description": "Write content to a file.",
|
|
402
|
+
"description": "Write content to a file, completely overwriting any existing content. Use this when you want to replace the entire contents of a file with new content.",
|
|
108
403
|
"parameters": {
|
|
109
404
|
"type": "object",
|
|
110
405
|
"properties": {
|
|
111
|
-
"
|
|
406
|
+
"path": {
|
|
112
407
|
"type": "string",
|
|
113
|
-
"description": "The
|
|
408
|
+
"description": "The full relative path including the filename where the file should be written. For example, 'output.txt' for a file in the root directory, 'src/main.py' for a file in the src directory, or 'docs/images/diagram.png' for a file in a nested directory."
|
|
114
409
|
},
|
|
115
410
|
"content": {
|
|
116
411
|
"type": "string",
|
|
117
|
-
"description": "The content to write to the file."
|
|
412
|
+
"description": "The content to write to the file. This will completely replace any existing content in the file. For example, 'Hello, world!' for a simple text file, or a JSON string for a configuration file."
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
"additionalProperties": False,
|
|
416
|
+
"required": ["path", "content"]
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class ListAllFilesTool(LLMTool):
|
|
423
|
+
def __init__(self, fs: FilesystemGateway):
|
|
424
|
+
self.fs = fs
|
|
425
|
+
|
|
426
|
+
def run(self, path: str) -> list[str]:
|
|
427
|
+
try:
|
|
428
|
+
return self.fs.list_all_files(path)
|
|
429
|
+
except ValueError as e:
|
|
430
|
+
return f"Error: {str(e)}"
|
|
431
|
+
except FileNotFoundError:
|
|
432
|
+
return f"Error: Directory '{path}' not found"
|
|
433
|
+
except PermissionError:
|
|
434
|
+
return f"Error: Permission denied when accessing directory '{path}'"
|
|
435
|
+
except Exception as e:
|
|
436
|
+
return f"Error listing files recursively in '{path}': {str(e)}"
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def descriptor(self):
|
|
440
|
+
return {
|
|
441
|
+
"type": "function",
|
|
442
|
+
"function": {
|
|
443
|
+
"name": "list_all_files",
|
|
444
|
+
"description": "List all files recursively in the specified directory, including files in subdirectories. Use this when you need a complete inventory of all files in a directory and its subdirectories.",
|
|
445
|
+
"parameters": {
|
|
446
|
+
"type": "object",
|
|
447
|
+
"properties": {
|
|
448
|
+
"path": {
|
|
449
|
+
"type": "string",
|
|
450
|
+
"description": "The path relative to the sandbox root to list files from recursively. For example, '.' for the root directory and all subdirectories, 'src' for the src directory and all its subdirectories, or 'docs/images' for a nested directory and its subdirectories."
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
"additionalProperties": False,
|
|
454
|
+
"required": ["path"]
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class FindFilesByGlobTool(LLMTool):
|
|
461
|
+
def __init__(self, fs: FilesystemGateway):
|
|
462
|
+
self.fs = fs
|
|
463
|
+
|
|
464
|
+
def run(self, path: str, pattern: str) -> list[str]:
|
|
465
|
+
try:
|
|
466
|
+
return self.fs.find_files_by_glob(path, pattern)
|
|
467
|
+
except ValueError as e:
|
|
468
|
+
return f"Error: {str(e)}"
|
|
469
|
+
except FileNotFoundError:
|
|
470
|
+
return f"Error: Directory '{path}' not found"
|
|
471
|
+
except PermissionError:
|
|
472
|
+
return f"Error: Permission denied when accessing directory '{path}'"
|
|
473
|
+
except Exception as e:
|
|
474
|
+
return f"Error finding files with pattern '{pattern}' in '{path}': {str(e)}"
|
|
475
|
+
|
|
476
|
+
@property
|
|
477
|
+
def descriptor(self):
|
|
478
|
+
return {
|
|
479
|
+
"type": "function",
|
|
480
|
+
"function": {
|
|
481
|
+
"name": "find_files_by_glob",
|
|
482
|
+
"description": "Find files matching a glob pattern in the specified directory. Use this when you need to locate files with specific patterns in their names or paths (e.g., all Python files with '*.py' or all text files in any subdirectory with '**/*.txt').",
|
|
483
|
+
"parameters": {
|
|
484
|
+
"type": "object",
|
|
485
|
+
"properties": {
|
|
486
|
+
"path": {
|
|
487
|
+
"type": "string",
|
|
488
|
+
"description": "The path relative to the sandbox root to search in. For example, '.' for the root directory, 'src' for the src directory, or 'docs/images' for a nested directory."
|
|
489
|
+
},
|
|
490
|
+
"pattern": {
|
|
491
|
+
"type": "string",
|
|
492
|
+
"description": "The glob pattern to match files against. Examples: '*.py' for all Python files in the specified directory, '**/*.txt' for all text files in the specified directory and any subdirectory, or '**/*test*.py' for all Python files with 'test' in their name in the specified directory and any subdirectory."
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
"additionalProperties": False,
|
|
496
|
+
"required": ["path", "pattern"]
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class FindFilesContainingTool(LLMTool):
|
|
503
|
+
def __init__(self, fs: FilesystemGateway):
|
|
504
|
+
self.fs = fs
|
|
505
|
+
|
|
506
|
+
def run(self, path: str, pattern: str) -> list[str]:
|
|
507
|
+
try:
|
|
508
|
+
return self.fs.find_files_containing(path, pattern)
|
|
509
|
+
except ValueError as e:
|
|
510
|
+
return f"Error: {str(e)}"
|
|
511
|
+
except FileNotFoundError:
|
|
512
|
+
return f"Error: Directory '{path}' not found"
|
|
513
|
+
except PermissionError:
|
|
514
|
+
return f"Error: Permission denied when accessing directory '{path}'"
|
|
515
|
+
except re.error as e:
|
|
516
|
+
return f"Error: Invalid regex pattern '{pattern}': {str(e)}"
|
|
517
|
+
except Exception as e:
|
|
518
|
+
return f"Error finding files containing pattern '{pattern}' in '{path}': {str(e)}"
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def descriptor(self):
|
|
522
|
+
return {
|
|
523
|
+
"type": "function",
|
|
524
|
+
"function": {
|
|
525
|
+
"name": "find_files_containing",
|
|
526
|
+
"description": "Find files containing text matching a regex pattern in the specified directory. Use this when you need to search for specific content across multiple files, such as finding all files that contain a particular function name or text string.",
|
|
527
|
+
"parameters": {
|
|
528
|
+
"type": "object",
|
|
529
|
+
"properties": {
|
|
530
|
+
"path": {
|
|
531
|
+
"type": "string",
|
|
532
|
+
"description": "The path relative to the sandbox root to search in. For example, '.' for the root directory, 'src' for the src directory, or 'docs/images' for a nested directory."
|
|
533
|
+
},
|
|
534
|
+
"pattern": {
|
|
535
|
+
"type": "string",
|
|
536
|
+
"description": "The regex pattern to search for in files. Examples: 'function\\s+main' to find files containing a main function, 'import\\s+os' to find files importing the os module, or 'TODO|FIXME' to find files containing TODO or FIXME comments. The pattern uses Python's re module syntax."
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
"additionalProperties": False,
|
|
540
|
+
"required": ["path", "pattern"]
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class FindLinesMatchingTool(LLMTool):
|
|
547
|
+
def __init__(self, fs: FilesystemGateway):
|
|
548
|
+
self.fs = fs
|
|
549
|
+
|
|
550
|
+
def run(self, path: str, pattern: str) -> list[dict]:
|
|
551
|
+
try:
|
|
552
|
+
# Split the path into directory and filename
|
|
553
|
+
directory, file_name = os.path.split(path)
|
|
554
|
+
return self.fs.find_lines_matching(directory, file_name, pattern)
|
|
555
|
+
except ValueError as e:
|
|
556
|
+
return f"Error: {str(e)}"
|
|
557
|
+
except FileNotFoundError:
|
|
558
|
+
return f"Error: File '{path}' not found"
|
|
559
|
+
except PermissionError:
|
|
560
|
+
return f"Error: Permission denied when accessing file '{path}'"
|
|
561
|
+
except re.error as e:
|
|
562
|
+
return f"Error: Invalid regex pattern '{pattern}': {str(e)}"
|
|
563
|
+
except IOError as e:
|
|
564
|
+
return f"Error reading file '{path}': {str(e)}"
|
|
565
|
+
except UnicodeDecodeError:
|
|
566
|
+
return f"Error: File '{path}' contains non-text content that cannot be read as text"
|
|
567
|
+
except Exception as e:
|
|
568
|
+
return f"Error finding lines matching pattern '{pattern}' in file '{path}': {str(e)}"
|
|
569
|
+
|
|
570
|
+
@property
|
|
571
|
+
def descriptor(self):
|
|
572
|
+
return {
|
|
573
|
+
"type": "function",
|
|
574
|
+
"function": {
|
|
575
|
+
"name": "find_lines_matching",
|
|
576
|
+
"description": "Find all lines in a file matching a regex pattern, returning both line numbers and content. Use this when you need to locate specific patterns within a single file and need to know exactly where they appear.",
|
|
577
|
+
"parameters": {
|
|
578
|
+
"type": "object",
|
|
579
|
+
"properties": {
|
|
580
|
+
"path": {
|
|
581
|
+
"type": "string",
|
|
582
|
+
"description": "The full relative path including the filename of the file to search in. For example, 'README.md' for a file in the root directory, 'src/main.py' for a file in the src directory, or 'docs/images/diagram.png' for a file in a nested directory."
|
|
583
|
+
},
|
|
584
|
+
"pattern": {
|
|
585
|
+
"type": "string",
|
|
586
|
+
"description": "The regex pattern to match lines against. Examples: 'def\\s+\\w+' to find all function definitions, 'class\\s+\\w+' to find all class definitions, or 'TODO|FIXME' to find all TODO or FIXME comments. The pattern uses Python's re module syntax."
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
"additionalProperties": False,
|
|
590
|
+
"required": ["path", "pattern"]
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class EditFileWithDiffTool(LLMTool):
|
|
597
|
+
def __init__(self, fs: FilesystemGateway):
|
|
598
|
+
self.fs = fs
|
|
599
|
+
|
|
600
|
+
def run(self, path: str, diff: str) -> str:
|
|
601
|
+
try:
|
|
602
|
+
# Split the path into directory and filename
|
|
603
|
+
directory, file_name = os.path.split(path)
|
|
604
|
+
return self.fs.edit_file_with_diff(directory, file_name, diff)
|
|
605
|
+
except ValueError as e:
|
|
606
|
+
return f"Error: {str(e)}"
|
|
607
|
+
except FileNotFoundError:
|
|
608
|
+
return f"Error: File '{path}' not found"
|
|
609
|
+
except PermissionError:
|
|
610
|
+
return f"Error: Permission denied when accessing file '{path}'"
|
|
611
|
+
except IOError as e:
|
|
612
|
+
return f"Error accessing file '{path}': {str(e)}"
|
|
613
|
+
except UnicodeDecodeError:
|
|
614
|
+
return f"Error: File '{path}' contains non-text content that cannot be edited as text"
|
|
615
|
+
except Exception as e:
|
|
616
|
+
return f"Error applying diff to file '{path}': {str(e)}"
|
|
617
|
+
|
|
618
|
+
@property
|
|
619
|
+
def descriptor(self):
|
|
620
|
+
return {
|
|
621
|
+
"type": "function",
|
|
622
|
+
"function": {
|
|
623
|
+
"name": "edit_file_with_diff",
|
|
624
|
+
"description": "Edit a file by applying a diff to it. Use this for making selective changes to parts of a file while preserving the rest of the content, unlike write_file which completely replaces the file. The diff should be in a unified diff format with lines prefixed by '+' (add), '-' (remove), or ' ' (context).",
|
|
625
|
+
"parameters": {
|
|
626
|
+
"type": "object",
|
|
627
|
+
"properties": {
|
|
628
|
+
"path": {
|
|
629
|
+
"type": "string",
|
|
630
|
+
"description": "The full relative path including the filename of the file to edit. For example, 'README.md' for a file in the root directory, 'src/main.py' for a file in the src directory, or 'docs/images/diagram.png' for a file in a nested directory."
|
|
631
|
+
},
|
|
632
|
+
"diff": {
|
|
633
|
+
"type": "string",
|
|
634
|
+
"description": "The diff to apply to the file in unified diff format. Lines to add should be prefixed with '+', lines to remove with '-', and context lines with ' ' (space). Example:\n\n```\n This is a context line (unchanged)\n-This line will be removed\n+This line will be added\n This is another context line\n```\n\nThe diff should include enough context lines to uniquely identify the section of the file to modify."
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
"additionalProperties": False,
|
|
638
|
+
"required": ["path", "diff"]
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
class CreateDirectoryTool(LLMTool):
|
|
645
|
+
def __init__(self, fs: FilesystemGateway):
|
|
646
|
+
self.fs = fs
|
|
647
|
+
|
|
648
|
+
def run(self, path: str) -> str:
|
|
649
|
+
try:
|
|
650
|
+
# Resolve the path relative to the base path
|
|
651
|
+
resolved_path = self.fs._resolve_path(path)
|
|
652
|
+
|
|
653
|
+
# Create the directory
|
|
654
|
+
os.makedirs(resolved_path, exist_ok=True)
|
|
655
|
+
|
|
656
|
+
return f"Successfully created directory '{path}'"
|
|
657
|
+
except ValueError as e:
|
|
658
|
+
return f"Error: {str(e)}"
|
|
659
|
+
except PermissionError:
|
|
660
|
+
return f"Error: Permission denied when creating directory '{path}'"
|
|
661
|
+
except OSError as e:
|
|
662
|
+
return f"Error creating directory '{path}': {str(e)}"
|
|
663
|
+
except Exception as e:
|
|
664
|
+
return f"Error creating directory '{path}': {str(e)}"
|
|
665
|
+
|
|
666
|
+
@property
|
|
667
|
+
def descriptor(self):
|
|
668
|
+
return {
|
|
669
|
+
"type": "function",
|
|
670
|
+
"function": {
|
|
671
|
+
"name": "create_directory",
|
|
672
|
+
"description": "Create a new directory at the specified path. If the directory already exists, this operation will succeed without error. Use this when you need to create a directory structure before writing files to it.",
|
|
673
|
+
"parameters": {
|
|
674
|
+
"type": "object",
|
|
675
|
+
"properties": {
|
|
676
|
+
"path": {
|
|
677
|
+
"type": "string",
|
|
678
|
+
"description": "The relative path where the directory should be created. For example, 'new_folder' for a directory in the root, 'src/new_folder' for a directory in the src directory, or 'docs/images/new_folder' for a nested directory. Parent directories will be created automatically if they don't exist."
|
|
118
679
|
}
|
|
119
680
|
},
|
|
120
681
|
"additionalProperties": False,
|
|
121
|
-
"required": ["
|
|
682
|
+
"required": ["path"]
|
|
122
683
|
},
|
|
123
684
|
},
|
|
124
685
|
}
|