hdsp-jupyter-extension 2.0.7__py3-none-any.whl → 2.0.10__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.
- agent_server/core/embedding_service.py +67 -46
- agent_server/core/rag_manager.py +40 -17
- agent_server/core/retriever.py +12 -6
- agent_server/core/vllm_embedding_service.py +246 -0
- agent_server/langchain/ARCHITECTURE.md +7 -51
- agent_server/langchain/agent.py +39 -20
- agent_server/langchain/custom_middleware.py +206 -62
- agent_server/langchain/hitl_config.py +6 -9
- agent_server/langchain/llm_factory.py +85 -1
- agent_server/langchain/logging_utils.py +52 -13
- agent_server/langchain/prompts.py +85 -45
- agent_server/langchain/tools/__init__.py +14 -10
- agent_server/langchain/tools/file_tools.py +266 -40
- agent_server/langchain/tools/file_utils.py +334 -0
- agent_server/langchain/tools/jupyter_tools.py +0 -1
- agent_server/langchain/tools/lsp_tools.py +264 -0
- agent_server/langchain/tools/resource_tools.py +12 -12
- agent_server/langchain/tools/search_tools.py +3 -158
- agent_server/main.py +7 -0
- agent_server/routers/langchain_agent.py +207 -102
- agent_server/routers/rag.py +8 -3
- hdsp_agent_core/models/rag.py +15 -1
- hdsp_agent_core/services/rag_service.py +6 -1
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js +251 -5
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js +1831 -274
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js +11 -9
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
- {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/METADATA +1 -3
- hdsp_jupyter_extension-2.0.10.dist-info/RECORD +144 -0
- jupyter_ext/__init__.py +18 -0
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +176 -1
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +3 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.4770ec0fb2d173b6deb4.js → frontend_styles_index_js.2d9fb488c82498c45c2d.js} +251 -5
- jupyter_ext/labextension/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
- jupyter_ext/labextension/static/{lib_index_js.29cf4312af19e86f82af.js → lib_index_js.dc6434bee96ab03a0539.js} +1831 -274
- jupyter_ext/labextension/static/lib_index_js.dc6434bee96ab03a0539.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.61343eb4cf0577e74b50.js → remoteEntry.4a252df3ade74efee8d6.js} +11 -9
- jupyter_ext/labextension/static/remoteEntry.4a252df3ade74efee8d6.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.dist-info/RECORD +0 -141
- jupyter_ext/labextension/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
- jupyter_ext/labextension/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
- {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,24 +4,32 @@ File Tools for LangChain Agent
|
|
|
4
4
|
Provides tools for file system operations:
|
|
5
5
|
- read_file: Read file content
|
|
6
6
|
- write_file: Write content to file (requires approval)
|
|
7
|
-
-
|
|
7
|
+
- edit_file: Edit file with string replacement (requires approval)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
|
-
from typing import Any, Dict, Optional
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
13
|
from langchain_core.tools import tool
|
|
14
14
|
from pydantic import BaseModel, Field
|
|
15
15
|
|
|
16
|
+
# Default constants for file reading (aligned with DeepAgents best practices)
|
|
17
|
+
DEFAULT_READ_LIMIT = 500 # Conservative default to prevent context overflow
|
|
18
|
+
DEFAULT_READ_OFFSET = 0
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
class ReadFileInput(BaseModel):
|
|
18
22
|
"""Input schema for read_file tool"""
|
|
19
23
|
|
|
20
24
|
path: str = Field(description="Relative path to the file to read")
|
|
21
25
|
encoding: str = Field(default="utf-8", description="File encoding")
|
|
22
|
-
|
|
23
|
-
default=
|
|
24
|
-
description="
|
|
26
|
+
offset: int = Field(
|
|
27
|
+
default=DEFAULT_READ_OFFSET,
|
|
28
|
+
description="Line offset to start reading from (0-indexed). Use for pagination.",
|
|
29
|
+
)
|
|
30
|
+
limit: int = Field(
|
|
31
|
+
default=DEFAULT_READ_LIMIT,
|
|
32
|
+
description="Maximum number of lines to read (default: 500). Use pagination for large files.",
|
|
25
33
|
)
|
|
26
34
|
execution_result: Optional[Dict[str, Any]] = Field(
|
|
27
35
|
default=None,
|
|
@@ -45,14 +53,15 @@ class WriteFileInput(BaseModel):
|
|
|
45
53
|
)
|
|
46
54
|
|
|
47
55
|
|
|
48
|
-
class
|
|
49
|
-
"""Input schema for
|
|
56
|
+
class EditFileInput(BaseModel):
|
|
57
|
+
"""Input schema for edit_file tool"""
|
|
50
58
|
|
|
51
|
-
path: str = Field(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
path: str = Field(description="Relative path to the file to edit")
|
|
60
|
+
old_string: str = Field(description="The exact string to find and replace")
|
|
61
|
+
new_string: str = Field(description="The replacement string")
|
|
62
|
+
replace_all: bool = Field(
|
|
63
|
+
default=False,
|
|
64
|
+
description="Whether to replace all occurrences (default: false, requires unique match)",
|
|
56
65
|
)
|
|
57
66
|
execution_result: Optional[Dict[str, Any]] = Field(
|
|
58
67
|
default=None,
|
|
@@ -93,19 +102,27 @@ def _validate_path(path: str, workspace_root: str = ".") -> str:
|
|
|
93
102
|
def read_file_tool(
|
|
94
103
|
path: str,
|
|
95
104
|
encoding: str = "utf-8",
|
|
96
|
-
|
|
105
|
+
offset: int = DEFAULT_READ_OFFSET,
|
|
106
|
+
limit: int = DEFAULT_READ_LIMIT,
|
|
97
107
|
execution_result: Optional[Dict[str, Any]] = None,
|
|
98
108
|
workspace_root: str = ".",
|
|
99
109
|
) -> Dict[str, Any]:
|
|
100
110
|
"""
|
|
101
|
-
Read content from a file.
|
|
111
|
+
Read content from a file with pagination support.
|
|
102
112
|
|
|
103
113
|
Only relative paths within the workspace are allowed.
|
|
104
114
|
Absolute paths and parent directory traversal (..) are blocked.
|
|
105
115
|
|
|
116
|
+
**IMPORTANT for large files**: Use pagination with offset and limit to avoid context overflow.
|
|
117
|
+
- First scan: read_file(path, limit=100) to see file structure
|
|
118
|
+
- Read more: read_file(path, offset=100, limit=200) for next 200 lines
|
|
119
|
+
- Only omit limit when necessary for immediate editing
|
|
120
|
+
|
|
106
121
|
Args:
|
|
107
122
|
path: Relative path to the file
|
|
108
123
|
encoding: File encoding (default: utf-8)
|
|
124
|
+
offset: Line offset to start reading from (0-indexed)
|
|
125
|
+
limit: Maximum number of lines to read (default: 500)
|
|
109
126
|
|
|
110
127
|
Returns:
|
|
111
128
|
Dict with file content or error
|
|
@@ -130,7 +147,8 @@ def read_file_tool(
|
|
|
130
147
|
"parameters": {
|
|
131
148
|
"path": path,
|
|
132
149
|
"encoding": encoding,
|
|
133
|
-
"
|
|
150
|
+
"offset": offset,
|
|
151
|
+
"limit": limit,
|
|
134
152
|
},
|
|
135
153
|
"status": "pending_execution",
|
|
136
154
|
"message": "File read queued for execution by client",
|
|
@@ -197,45 +215,253 @@ def write_file_tool(
|
|
|
197
215
|
}
|
|
198
216
|
|
|
199
217
|
|
|
200
|
-
@tool(args_schema=
|
|
201
|
-
def
|
|
202
|
-
path: str
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
@tool(args_schema=EditFileInput)
|
|
219
|
+
def edit_file_tool(
|
|
220
|
+
path: str,
|
|
221
|
+
old_string: str,
|
|
222
|
+
new_string: str,
|
|
223
|
+
replace_all: bool = False,
|
|
205
224
|
execution_result: Optional[Dict[str, Any]] = None,
|
|
206
225
|
workspace_root: str = ".",
|
|
207
226
|
) -> Dict[str, Any]:
|
|
208
227
|
"""
|
|
209
|
-
|
|
228
|
+
Edit a file by replacing a specific string with another.
|
|
229
|
+
|
|
230
|
+
This operation requires user approval before execution.
|
|
231
|
+
The old_string must be unique in the file unless replace_all=True.
|
|
210
232
|
|
|
211
233
|
Args:
|
|
212
|
-
path:
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
path: Relative path to the file
|
|
235
|
+
old_string: The exact string to find and replace
|
|
236
|
+
new_string: The replacement string
|
|
237
|
+
replace_all: Whether to replace all occurrences
|
|
215
238
|
|
|
216
239
|
Returns:
|
|
217
|
-
Dict with
|
|
240
|
+
Dict with operation status and diff preview (pending approval)
|
|
218
241
|
"""
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
242
|
+
# Security validation
|
|
243
|
+
if os.path.isabs(path):
|
|
244
|
+
return {
|
|
245
|
+
"tool": "edit_file_tool",
|
|
246
|
+
"success": False,
|
|
247
|
+
"error": f"Absolute paths not allowed: {path}",
|
|
222
248
|
"path": path,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
249
|
+
}
|
|
250
|
+
if ".." in path:
|
|
251
|
+
return {
|
|
252
|
+
"tool": "edit_file_tool",
|
|
253
|
+
"success": False,
|
|
254
|
+
"error": f"Parent directory traversal not allowed: {path}",
|
|
255
|
+
"path": path,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
resolved_path = _validate_path(path, workspace_root)
|
|
260
|
+
|
|
261
|
+
# Build response with diff preview
|
|
262
|
+
# Note: actual file content will be read by client for diff generation
|
|
263
|
+
old_preview = old_string[:200] + "..." if len(old_string) > 200 else old_string
|
|
264
|
+
new_preview = new_string[:200] + "..." if len(new_string) > 200 else new_string
|
|
265
|
+
|
|
266
|
+
response: Dict[str, Any] = {
|
|
267
|
+
"tool": "edit_file_tool",
|
|
268
|
+
"parameters": {
|
|
269
|
+
"path": path,
|
|
270
|
+
"old_string": old_string,
|
|
271
|
+
"new_string": new_string,
|
|
272
|
+
"replace_all": replace_all,
|
|
273
|
+
},
|
|
274
|
+
"status": "pending_approval",
|
|
275
|
+
"path": path,
|
|
276
|
+
"resolved_path": resolved_path,
|
|
277
|
+
"old_string_preview": old_preview,
|
|
278
|
+
"new_string_preview": new_preview,
|
|
279
|
+
"replace_all": replace_all,
|
|
280
|
+
"message": "File edit operation requires user approval",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if execution_result is not None:
|
|
284
|
+
response["execution_result"] = execution_result
|
|
285
|
+
response["status"] = "complete"
|
|
286
|
+
response["message"] = "File edit executed with client-reported results"
|
|
287
|
+
# Include diff if provided by client
|
|
288
|
+
if "diff" in execution_result:
|
|
289
|
+
response["diff"] = execution_result["diff"]
|
|
290
|
+
|
|
291
|
+
return response
|
|
292
|
+
|
|
293
|
+
except ValueError as e:
|
|
294
|
+
return {
|
|
295
|
+
"tool": "edit_file_tool",
|
|
296
|
+
"success": False,
|
|
297
|
+
"error": str(e),
|
|
298
|
+
"path": path,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class EditOperation(BaseModel):
|
|
303
|
+
"""Single edit operation for multiedit_file tool"""
|
|
304
|
+
|
|
305
|
+
old_string: str = Field(description="The exact string to find and replace")
|
|
306
|
+
new_string: str = Field(description="The replacement string")
|
|
307
|
+
replace_all: bool = Field(
|
|
308
|
+
default=False, description="Whether to replace all occurrences (default: false)"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class MultiEditInput(BaseModel):
|
|
313
|
+
"""Input schema for multiedit_file tool"""
|
|
314
|
+
|
|
315
|
+
path: str = Field(description="Relative path to the file to edit")
|
|
316
|
+
edits: List[EditOperation] = Field(
|
|
317
|
+
description="List of edit operations to apply sequentially"
|
|
318
|
+
)
|
|
319
|
+
execution_result: Optional[Dict[str, Any]] = Field(
|
|
320
|
+
default=None,
|
|
321
|
+
description="Optional execution result payload from the client",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@tool(args_schema=MultiEditInput)
|
|
326
|
+
def multiedit_file_tool(
|
|
327
|
+
path: str,
|
|
328
|
+
edits: List[EditOperation],
|
|
329
|
+
execution_result: Optional[Dict[str, Any]] = None,
|
|
330
|
+
workspace_root: str = ".",
|
|
331
|
+
) -> Dict[str, Any]:
|
|
332
|
+
"""
|
|
333
|
+
Apply multiple sequential edits to a single file atomically.
|
|
334
|
+
|
|
335
|
+
This is more efficient than multiple edit_file_tool calls when you need
|
|
336
|
+
to make several changes to the same file. All edits are validated before
|
|
337
|
+
any are applied - if one fails, none are applied.
|
|
338
|
+
|
|
339
|
+
Use this tool when:
|
|
340
|
+
- Making multiple related changes to a file
|
|
341
|
+
- Updating several config values at once
|
|
342
|
+
- Refactoring multiple sections of code
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
path: Relative path to the file
|
|
346
|
+
edits: List of edit operations, each containing:
|
|
347
|
+
- old_string: The exact string to find and replace
|
|
348
|
+
- new_string: The replacement string
|
|
349
|
+
- replace_all: (optional) Whether to replace all occurrences
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Dict with operation status, edits_applied count, and diff preview
|
|
353
|
+
|
|
354
|
+
Example:
|
|
355
|
+
multiedit_file_tool(
|
|
356
|
+
path="config.py",
|
|
357
|
+
edits=[
|
|
358
|
+
{"old_string": "DEBUG = True", "new_string": "DEBUG = False"},
|
|
359
|
+
{"old_string": "LOG_LEVEL = 'INFO'", "new_string": "LOG_LEVEL = 'WARNING'"}
|
|
360
|
+
]
|
|
361
|
+
)
|
|
362
|
+
"""
|
|
363
|
+
# Security validation
|
|
364
|
+
if os.path.isabs(path):
|
|
365
|
+
return {
|
|
366
|
+
"tool": "multiedit_file_tool",
|
|
367
|
+
"success": False,
|
|
368
|
+
"error": f"Absolute paths not allowed: {path}",
|
|
369
|
+
"path": path,
|
|
370
|
+
}
|
|
371
|
+
if ".." in path:
|
|
372
|
+
return {
|
|
373
|
+
"tool": "multiedit_file_tool",
|
|
374
|
+
"success": False,
|
|
375
|
+
"error": f"Parent directory traversal not allowed: {path}",
|
|
376
|
+
"path": path,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if not edits or len(edits) == 0:
|
|
380
|
+
return {
|
|
381
|
+
"tool": "multiedit_file_tool",
|
|
382
|
+
"success": False,
|
|
383
|
+
"error": "At least one edit is required",
|
|
384
|
+
"path": path,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
resolved_path = _validate_path(path, workspace_root)
|
|
389
|
+
|
|
390
|
+
# Build edits preview (handle both EditOperation objects and dicts)
|
|
391
|
+
edits_preview = []
|
|
392
|
+
edits_as_dicts = []
|
|
393
|
+
for i, edit in enumerate(edits[:5]): # Preview first 5
|
|
394
|
+
# Support both Pydantic model and dict access
|
|
395
|
+
if hasattr(edit, "old_string"):
|
|
396
|
+
old_str = edit.old_string
|
|
397
|
+
new_str = edit.new_string
|
|
398
|
+
replace_all_val = edit.replace_all
|
|
399
|
+
else:
|
|
400
|
+
old_str = edit.get("old_string", "")
|
|
401
|
+
new_str = edit.get("new_string", "")
|
|
402
|
+
replace_all_val = edit.get("replace_all", False)
|
|
403
|
+
|
|
404
|
+
old_preview = (old_str[:50] + "...") if len(old_str) > 50 else old_str
|
|
405
|
+
new_preview = (new_str[:50] + "...") if len(new_str) > 50 else new_str
|
|
406
|
+
edits_preview.append(
|
|
407
|
+
{
|
|
408
|
+
"index": i,
|
|
409
|
+
"old_preview": old_preview,
|
|
410
|
+
"new_preview": new_preview,
|
|
411
|
+
"replace_all": replace_all_val,
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Convert all edits to dicts for serialization
|
|
416
|
+
for edit in edits:
|
|
417
|
+
if hasattr(edit, "model_dump"):
|
|
418
|
+
edits_as_dicts.append(edit.model_dump())
|
|
419
|
+
elif hasattr(edit, "dict"):
|
|
420
|
+
edits_as_dicts.append(edit.dict())
|
|
421
|
+
else:
|
|
422
|
+
edits_as_dicts.append(edit)
|
|
423
|
+
|
|
424
|
+
response: Dict[str, Any] = {
|
|
425
|
+
"tool": "multiedit_file_tool",
|
|
426
|
+
"parameters": {
|
|
427
|
+
"path": path,
|
|
428
|
+
"edits_count": len(edits),
|
|
429
|
+
"edits": edits_as_dicts,
|
|
430
|
+
},
|
|
431
|
+
"status": "pending_approval",
|
|
432
|
+
"path": path,
|
|
433
|
+
"resolved_path": resolved_path,
|
|
434
|
+
"edits_preview": edits_preview,
|
|
435
|
+
"total_edits": len(edits),
|
|
436
|
+
"message": f"Multi-edit operation ({len(edits)} edits) requires user approval",
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if execution_result is not None:
|
|
440
|
+
response["execution_result"] = execution_result
|
|
441
|
+
response["status"] = "complete"
|
|
442
|
+
response["message"] = "Multi-edit executed with client-reported results"
|
|
443
|
+
if "diff" in execution_result:
|
|
444
|
+
response["diff"] = execution_result["diff"]
|
|
445
|
+
if "edits_applied" in execution_result:
|
|
446
|
+
response["edits_applied"] = execution_result["edits_applied"]
|
|
447
|
+
if "edits_failed" in execution_result:
|
|
448
|
+
response["edits_failed"] = execution_result["edits_failed"]
|
|
449
|
+
|
|
450
|
+
return response
|
|
451
|
+
|
|
452
|
+
except ValueError as e:
|
|
453
|
+
return {
|
|
454
|
+
"tool": "multiedit_file_tool",
|
|
455
|
+
"success": False,
|
|
456
|
+
"error": str(e),
|
|
457
|
+
"path": path,
|
|
458
|
+
}
|
|
234
459
|
|
|
235
460
|
|
|
236
461
|
# Export all tools
|
|
237
462
|
FILE_TOOLS = [
|
|
238
463
|
read_file_tool,
|
|
239
464
|
write_file_tool,
|
|
240
|
-
|
|
465
|
+
edit_file_tool,
|
|
466
|
+
multiedit_file_tool,
|
|
241
467
|
]
|