hdsp-jupyter-extension 2.0.6__py3-none-any.whl → 2.0.8__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 +31 -17
- agent_server/core/reflection_engine.py +0 -1
- agent_server/core/retriever.py +13 -8
- agent_server/core/vllm_embedding_service.py +243 -0
- agent_server/knowledge/watchdog_service.py +1 -1
- agent_server/langchain/ARCHITECTURE.md +1193 -0
- agent_server/langchain/agent.py +82 -588
- agent_server/langchain/custom_middleware.py +663 -0
- agent_server/langchain/executors/__init__.py +2 -7
- agent_server/langchain/executors/notebook_searcher.py +46 -38
- agent_server/langchain/hitl_config.py +71 -0
- agent_server/langchain/llm_factory.py +166 -0
- agent_server/langchain/logging_utils.py +223 -0
- agent_server/langchain/prompts.py +150 -0
- agent_server/langchain/state.py +16 -6
- agent_server/langchain/tools/__init__.py +19 -0
- agent_server/langchain/tools/file_tools.py +354 -114
- agent_server/langchain/tools/file_utils.py +334 -0
- agent_server/langchain/tools/jupyter_tools.py +18 -18
- agent_server/langchain/tools/lsp_tools.py +264 -0
- agent_server/langchain/tools/resource_tools.py +161 -0
- agent_server/langchain/tools/search_tools.py +198 -216
- agent_server/langchain/tools/shell_tools.py +54 -0
- agent_server/main.py +11 -1
- agent_server/routers/health.py +1 -1
- agent_server/routers/langchain_agent.py +1040 -289
- agent_server/routers/rag.py +8 -3
- hdsp_agent_core/models/rag.py +15 -1
- hdsp_agent_core/prompts/auto_agent_prompts.py +3 -3
- hdsp_agent_core/services/rag_service.py +6 -1
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js +470 -7
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js +3196 -441
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js +9 -7
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
- {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/METADATA +2 -1
- {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/RECORD +75 -69
- jupyter_ext/__init__.py +18 -0
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +1351 -58
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +3 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.02d346171474a0fb2dc1.js → frontend_styles_index_js.8740a527757068814573.js} +470 -7
- jupyter_ext/labextension/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
- jupyter_ext/labextension/static/{lib_index_js.a223ea20056954479ae9.js → lib_index_js.e4ff4b5779b5e049f84c.js} +3196 -441
- jupyter_ext/labextension/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.addf2fa038fa60304aa2.js → remoteEntry.020cdb0b864cfaa4e41e.js} +9 -7
- jupyter_ext/labextension/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
- jupyter_ext/resource_usage.py +180 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- agent_server/langchain/executors/jupyter_executor.py +0 -429
- agent_server/langchain/middleware/__init__.py +0 -36
- agent_server/langchain/middleware/code_search_middleware.py +0 -278
- agent_server/langchain/middleware/error_handling_middleware.py +0 -338
- agent_server/langchain/middleware/jupyter_execution_middleware.py +0 -301
- agent_server/langchain/middleware/rag_middleware.py +0 -227
- agent_server/langchain/middleware/validation_middleware.py +0 -240
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
- jupyter_ext/labextension/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
- jupyter_ext/labextension/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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.6.data → hdsp_jupyter_extension-2.0.8.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.6.data → hdsp_jupyter_extension-2.0.8.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.6.data → hdsp_jupyter_extension-2.0.8.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.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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.6.data → hdsp_jupyter_extension-2.0.8.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.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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.6.data → hdsp_jupyter_extension-2.0.8.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.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
- {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,6 +4,7 @@ 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
|
+
- edit_file: Edit file with string replacement (requires approval)
|
|
7
8
|
- list_files: List directory contents
|
|
8
9
|
"""
|
|
9
10
|
|
|
@@ -13,34 +14,81 @@ from typing import Any, Dict, List, Optional
|
|
|
13
14
|
from langchain_core.tools import tool
|
|
14
15
|
from pydantic import BaseModel, Field
|
|
15
16
|
|
|
17
|
+
# Default constants for file reading (aligned with DeepAgents best practices)
|
|
18
|
+
DEFAULT_READ_LIMIT = 500 # Conservative default to prevent context overflow
|
|
19
|
+
DEFAULT_READ_OFFSET = 0
|
|
20
|
+
|
|
16
21
|
|
|
17
22
|
class ReadFileInput(BaseModel):
|
|
18
23
|
"""Input schema for read_file tool"""
|
|
24
|
+
|
|
19
25
|
path: str = Field(description="Relative path to the file to read")
|
|
20
26
|
encoding: str = Field(default="utf-8", description="File encoding")
|
|
27
|
+
offset: int = Field(
|
|
28
|
+
default=DEFAULT_READ_OFFSET,
|
|
29
|
+
description="Line offset to start reading from (0-indexed). Use for pagination.",
|
|
30
|
+
)
|
|
31
|
+
limit: int = Field(
|
|
32
|
+
default=DEFAULT_READ_LIMIT,
|
|
33
|
+
description="Maximum number of lines to read (default: 500). Use pagination for large files.",
|
|
34
|
+
)
|
|
35
|
+
execution_result: Optional[Dict[str, Any]] = Field(
|
|
36
|
+
default=None,
|
|
37
|
+
description="Optional execution result payload from the client",
|
|
38
|
+
)
|
|
21
39
|
|
|
22
40
|
|
|
23
41
|
class WriteFileInput(BaseModel):
|
|
24
42
|
"""Input schema for write_file tool"""
|
|
43
|
+
|
|
25
44
|
path: str = Field(description="Relative path to the file to write")
|
|
26
45
|
content: str = Field(description="Content to write to the file")
|
|
27
46
|
encoding: str = Field(default="utf-8", description="File encoding")
|
|
47
|
+
overwrite: bool = Field(
|
|
48
|
+
default=False,
|
|
49
|
+
description="Whether to overwrite an existing file (default: false)",
|
|
50
|
+
)
|
|
51
|
+
execution_result: Optional[Dict[str, Any]] = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
description="Optional execution result payload from the client",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EditFileInput(BaseModel):
|
|
58
|
+
"""Input schema for edit_file tool"""
|
|
59
|
+
|
|
60
|
+
path: str = Field(description="Relative path to the file to edit")
|
|
61
|
+
old_string: str = Field(description="The exact string to find and replace")
|
|
62
|
+
new_string: str = Field(description="The replacement string")
|
|
63
|
+
replace_all: bool = Field(
|
|
64
|
+
default=False,
|
|
65
|
+
description="Whether to replace all occurrences (default: false, requires unique match)",
|
|
66
|
+
)
|
|
67
|
+
execution_result: Optional[Dict[str, Any]] = Field(
|
|
68
|
+
default=None,
|
|
69
|
+
description="Optional execution result payload from the client",
|
|
70
|
+
)
|
|
28
71
|
|
|
29
72
|
|
|
30
73
|
class ListFilesInput(BaseModel):
|
|
31
74
|
"""Input schema for list_files tool"""
|
|
75
|
+
|
|
32
76
|
path: str = Field(default=".", description="Directory path to list")
|
|
33
77
|
recursive: bool = Field(default=False, description="Whether to list recursively")
|
|
34
78
|
pattern: Optional[str] = Field(
|
|
35
79
|
default=None,
|
|
36
|
-
description="Glob pattern to filter files (e.g., '*.py', '*.ipynb')"
|
|
80
|
+
description="Glob pattern to filter files (e.g., '*.py', '*.ipynb')",
|
|
81
|
+
)
|
|
82
|
+
execution_result: Optional[Dict[str, Any]] = Field(
|
|
83
|
+
default=None,
|
|
84
|
+
description="Optional execution result payload from the client",
|
|
37
85
|
)
|
|
38
86
|
|
|
39
87
|
|
|
40
88
|
def _validate_path(path: str, workspace_root: str = ".") -> str:
|
|
41
89
|
"""
|
|
42
90
|
Validate and resolve file path.
|
|
43
|
-
|
|
91
|
+
|
|
44
92
|
Security checks:
|
|
45
93
|
- No absolute paths allowed
|
|
46
94
|
- No parent directory traversal (..)
|
|
@@ -70,93 +118,97 @@ def _validate_path(path: str, workspace_root: str = ".") -> str:
|
|
|
70
118
|
def read_file_tool(
|
|
71
119
|
path: str,
|
|
72
120
|
encoding: str = "utf-8",
|
|
73
|
-
|
|
121
|
+
offset: int = DEFAULT_READ_OFFSET,
|
|
122
|
+
limit: int = DEFAULT_READ_LIMIT,
|
|
123
|
+
execution_result: Optional[Dict[str, Any]] = None,
|
|
124
|
+
workspace_root: str = ".",
|
|
74
125
|
) -> Dict[str, Any]:
|
|
75
126
|
"""
|
|
76
|
-
Read content from a file.
|
|
77
|
-
|
|
127
|
+
Read content from a file with pagination support.
|
|
128
|
+
|
|
78
129
|
Only relative paths within the workspace are allowed.
|
|
79
130
|
Absolute paths and parent directory traversal (..) are blocked.
|
|
80
|
-
|
|
131
|
+
|
|
132
|
+
**IMPORTANT for large files**: Use pagination with offset and limit to avoid context overflow.
|
|
133
|
+
- First scan: read_file(path, limit=100) to see file structure
|
|
134
|
+
- Read more: read_file(path, offset=100, limit=200) for next 200 lines
|
|
135
|
+
- Only omit limit when necessary for immediate editing
|
|
136
|
+
|
|
81
137
|
Args:
|
|
82
138
|
path: Relative path to the file
|
|
83
139
|
encoding: File encoding (default: utf-8)
|
|
84
|
-
|
|
140
|
+
offset: Line offset to start reading from (0-indexed)
|
|
141
|
+
limit: Maximum number of lines to read (default: 500)
|
|
142
|
+
|
|
85
143
|
Returns:
|
|
86
144
|
Dict with file content or error
|
|
87
145
|
"""
|
|
88
|
-
|
|
89
|
-
resolved_path = _validate_path(path, workspace_root)
|
|
90
|
-
|
|
91
|
-
if not os.path.exists(resolved_path):
|
|
92
|
-
return {
|
|
93
|
-
"tool": "read_file",
|
|
94
|
-
"success": False,
|
|
95
|
-
"error": f"File not found: {path}",
|
|
96
|
-
"path": path,
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if not os.path.isfile(resolved_path):
|
|
100
|
-
return {
|
|
101
|
-
"tool": "read_file",
|
|
102
|
-
"success": False,
|
|
103
|
-
"error": f"Not a file: {path}",
|
|
104
|
-
"path": path,
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
with open(resolved_path, "r", encoding=encoding) as f:
|
|
108
|
-
content = f.read()
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
"tool": "read_file",
|
|
112
|
-
"success": True,
|
|
113
|
-
"path": path,
|
|
114
|
-
"content": content,
|
|
115
|
-
"size": len(content),
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
except ValueError as e:
|
|
146
|
+
if os.path.isabs(path):
|
|
119
147
|
return {
|
|
120
|
-
"tool": "
|
|
148
|
+
"tool": "read_file_tool",
|
|
121
149
|
"success": False,
|
|
122
|
-
"error":
|
|
150
|
+
"error": f"Absolute paths not allowed: {path}",
|
|
123
151
|
"path": path,
|
|
124
152
|
}
|
|
125
|
-
|
|
153
|
+
if ".." in path:
|
|
126
154
|
return {
|
|
127
|
-
"tool": "
|
|
155
|
+
"tool": "read_file_tool",
|
|
128
156
|
"success": False,
|
|
129
|
-
"error": f"
|
|
157
|
+
"error": f"Parent directory traversal not allowed: {path}",
|
|
130
158
|
"path": path,
|
|
131
159
|
}
|
|
132
160
|
|
|
161
|
+
response: Dict[str, Any] = {
|
|
162
|
+
"tool": "read_file_tool",
|
|
163
|
+
"parameters": {
|
|
164
|
+
"path": path,
|
|
165
|
+
"encoding": encoding,
|
|
166
|
+
"offset": offset,
|
|
167
|
+
"limit": limit,
|
|
168
|
+
},
|
|
169
|
+
"status": "pending_execution",
|
|
170
|
+
"message": "File read queued for execution by client",
|
|
171
|
+
}
|
|
172
|
+
if execution_result is not None:
|
|
173
|
+
response["execution_result"] = execution_result
|
|
174
|
+
response["status"] = "complete"
|
|
175
|
+
response["message"] = "File read executed with client-reported results"
|
|
176
|
+
return response
|
|
177
|
+
|
|
133
178
|
|
|
134
179
|
@tool(args_schema=WriteFileInput)
|
|
135
180
|
def write_file_tool(
|
|
136
181
|
path: str,
|
|
137
182
|
content: str,
|
|
138
183
|
encoding: str = "utf-8",
|
|
139
|
-
|
|
184
|
+
overwrite: bool = False,
|
|
185
|
+
execution_result: Optional[Dict[str, Any]] = None,
|
|
186
|
+
workspace_root: str = ".",
|
|
140
187
|
) -> Dict[str, Any]:
|
|
141
188
|
"""
|
|
142
189
|
Write content to a file.
|
|
143
|
-
|
|
190
|
+
|
|
144
191
|
This operation requires user approval before execution.
|
|
145
192
|
Only relative paths within the workspace are allowed.
|
|
146
|
-
|
|
193
|
+
|
|
147
194
|
Args:
|
|
148
195
|
path: Relative path to the file
|
|
149
196
|
content: Content to write
|
|
150
197
|
encoding: File encoding (default: utf-8)
|
|
151
|
-
|
|
198
|
+
|
|
152
199
|
Returns:
|
|
153
200
|
Dict with operation status (pending approval)
|
|
154
201
|
"""
|
|
155
202
|
try:
|
|
156
203
|
resolved_path = _validate_path(path, workspace_root)
|
|
157
204
|
|
|
158
|
-
|
|
159
|
-
"tool": "
|
|
205
|
+
response: Dict[str, Any] = {
|
|
206
|
+
"tool": "write_file_tool",
|
|
207
|
+
"parameters": {
|
|
208
|
+
"path": path,
|
|
209
|
+
"encoding": encoding,
|
|
210
|
+
"overwrite": overwrite,
|
|
211
|
+
},
|
|
160
212
|
"status": "pending_approval",
|
|
161
213
|
"path": path,
|
|
162
214
|
"resolved_path": resolved_path,
|
|
@@ -164,10 +216,99 @@ def write_file_tool(
|
|
|
164
216
|
"content_length": len(content),
|
|
165
217
|
"message": "File write operation requires user approval",
|
|
166
218
|
}
|
|
219
|
+
if execution_result is not None:
|
|
220
|
+
response["execution_result"] = execution_result
|
|
221
|
+
response["status"] = "complete"
|
|
222
|
+
response["message"] = "File write executed with client-reported results"
|
|
223
|
+
return response
|
|
167
224
|
|
|
168
225
|
except ValueError as e:
|
|
169
226
|
return {
|
|
170
|
-
"tool": "
|
|
227
|
+
"tool": "write_file_tool",
|
|
228
|
+
"success": False,
|
|
229
|
+
"error": str(e),
|
|
230
|
+
"path": path,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@tool(args_schema=EditFileInput)
|
|
235
|
+
def edit_file_tool(
|
|
236
|
+
path: str,
|
|
237
|
+
old_string: str,
|
|
238
|
+
new_string: str,
|
|
239
|
+
replace_all: bool = False,
|
|
240
|
+
execution_result: Optional[Dict[str, Any]] = None,
|
|
241
|
+
workspace_root: str = ".",
|
|
242
|
+
) -> Dict[str, Any]:
|
|
243
|
+
"""
|
|
244
|
+
Edit a file by replacing a specific string with another.
|
|
245
|
+
|
|
246
|
+
This operation requires user approval before execution.
|
|
247
|
+
The old_string must be unique in the file unless replace_all=True.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
path: Relative path to the file
|
|
251
|
+
old_string: The exact string to find and replace
|
|
252
|
+
new_string: The replacement string
|
|
253
|
+
replace_all: Whether to replace all occurrences
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Dict with operation status and diff preview (pending approval)
|
|
257
|
+
"""
|
|
258
|
+
# Security validation
|
|
259
|
+
if os.path.isabs(path):
|
|
260
|
+
return {
|
|
261
|
+
"tool": "edit_file_tool",
|
|
262
|
+
"success": False,
|
|
263
|
+
"error": f"Absolute paths not allowed: {path}",
|
|
264
|
+
"path": path,
|
|
265
|
+
}
|
|
266
|
+
if ".." in path:
|
|
267
|
+
return {
|
|
268
|
+
"tool": "edit_file_tool",
|
|
269
|
+
"success": False,
|
|
270
|
+
"error": f"Parent directory traversal not allowed: {path}",
|
|
271
|
+
"path": path,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
resolved_path = _validate_path(path, workspace_root)
|
|
276
|
+
|
|
277
|
+
# Build response with diff preview
|
|
278
|
+
# Note: actual file content will be read by client for diff generation
|
|
279
|
+
old_preview = old_string[:200] + "..." if len(old_string) > 200 else old_string
|
|
280
|
+
new_preview = new_string[:200] + "..." if len(new_string) > 200 else new_string
|
|
281
|
+
|
|
282
|
+
response: Dict[str, Any] = {
|
|
283
|
+
"tool": "edit_file_tool",
|
|
284
|
+
"parameters": {
|
|
285
|
+
"path": path,
|
|
286
|
+
"old_string": old_string,
|
|
287
|
+
"new_string": new_string,
|
|
288
|
+
"replace_all": replace_all,
|
|
289
|
+
},
|
|
290
|
+
"status": "pending_approval",
|
|
291
|
+
"path": path,
|
|
292
|
+
"resolved_path": resolved_path,
|
|
293
|
+
"old_string_preview": old_preview,
|
|
294
|
+
"new_string_preview": new_preview,
|
|
295
|
+
"replace_all": replace_all,
|
|
296
|
+
"message": "File edit operation requires user approval",
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if execution_result is not None:
|
|
300
|
+
response["execution_result"] = execution_result
|
|
301
|
+
response["status"] = "complete"
|
|
302
|
+
response["message"] = "File edit executed with client-reported results"
|
|
303
|
+
# Include diff if provided by client
|
|
304
|
+
if "diff" in execution_result:
|
|
305
|
+
response["diff"] = execution_result["diff"]
|
|
306
|
+
|
|
307
|
+
return response
|
|
308
|
+
|
|
309
|
+
except ValueError as e:
|
|
310
|
+
return {
|
|
311
|
+
"tool": "edit_file_tool",
|
|
171
312
|
"success": False,
|
|
172
313
|
"error": str(e),
|
|
173
314
|
"path": path,
|
|
@@ -179,94 +320,191 @@ def list_files_tool(
|
|
|
179
320
|
path: str = ".",
|
|
180
321
|
recursive: bool = False,
|
|
181
322
|
pattern: Optional[str] = None,
|
|
182
|
-
|
|
323
|
+
execution_result: Optional[Dict[str, Any]] = None,
|
|
324
|
+
workspace_root: str = ".",
|
|
183
325
|
) -> Dict[str, Any]:
|
|
184
326
|
"""
|
|
185
327
|
List files and directories.
|
|
186
|
-
|
|
328
|
+
|
|
187
329
|
Args:
|
|
188
330
|
path: Directory path to list (default: current directory)
|
|
189
331
|
recursive: Whether to list recursively
|
|
190
332
|
pattern: Optional glob pattern to filter (e.g., '*.py')
|
|
191
|
-
|
|
333
|
+
|
|
192
334
|
Returns:
|
|
193
335
|
Dict with list of files and directories
|
|
194
336
|
"""
|
|
195
|
-
|
|
337
|
+
response: Dict[str, Any] = {
|
|
338
|
+
"tool": "list_files_tool",
|
|
339
|
+
"parameters": {
|
|
340
|
+
"path": path,
|
|
341
|
+
"recursive": recursive,
|
|
342
|
+
"pattern": pattern,
|
|
343
|
+
},
|
|
344
|
+
"status": "pending_execution",
|
|
345
|
+
"message": "File listing queued for execution by client",
|
|
346
|
+
}
|
|
347
|
+
if execution_result is not None:
|
|
348
|
+
response["execution_result"] = execution_result
|
|
349
|
+
response["status"] = "complete"
|
|
350
|
+
response["message"] = "File listing executed with client-reported results"
|
|
351
|
+
return response
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class EditOperation(BaseModel):
|
|
355
|
+
"""Single edit operation for multiedit_file tool"""
|
|
356
|
+
|
|
357
|
+
old_string: str = Field(description="The exact string to find and replace")
|
|
358
|
+
new_string: str = Field(description="The replacement string")
|
|
359
|
+
replace_all: bool = Field(
|
|
360
|
+
default=False,
|
|
361
|
+
description="Whether to replace all occurrences (default: false)"
|
|
362
|
+
)
|
|
196
363
|
|
|
197
|
-
try:
|
|
198
|
-
resolved_path = _validate_path(path, workspace_root)
|
|
199
364
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"tool": "list_files",
|
|
203
|
-
"success": False,
|
|
204
|
-
"error": f"Directory not found: {path}",
|
|
205
|
-
"path": path,
|
|
206
|
-
}
|
|
365
|
+
class MultiEditInput(BaseModel):
|
|
366
|
+
"""Input schema for multiedit_file tool"""
|
|
207
367
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
files: List[Dict[str, Any]] = []
|
|
217
|
-
dirs: List[str] = []
|
|
218
|
-
|
|
219
|
-
if recursive:
|
|
220
|
-
for root, dirnames, filenames in os.walk(resolved_path):
|
|
221
|
-
rel_root = os.path.relpath(root, resolved_path)
|
|
222
|
-
for dirname in dirnames:
|
|
223
|
-
dir_path = os.path.join(rel_root, dirname) if rel_root != "." else dirname
|
|
224
|
-
dirs.append(dir_path)
|
|
225
|
-
for filename in filenames:
|
|
226
|
-
if pattern and not fnmatch.fnmatch(filename, pattern):
|
|
227
|
-
continue
|
|
228
|
-
file_path = os.path.join(rel_root, filename) if rel_root != "." else filename
|
|
229
|
-
full_path = os.path.join(root, filename)
|
|
230
|
-
files.append({
|
|
231
|
-
"name": filename,
|
|
232
|
-
"path": file_path,
|
|
233
|
-
"size": os.path.getsize(full_path),
|
|
234
|
-
})
|
|
235
|
-
else:
|
|
236
|
-
for entry in os.scandir(resolved_path):
|
|
237
|
-
if entry.is_dir():
|
|
238
|
-
dirs.append(entry.name)
|
|
239
|
-
elif entry.is_file():
|
|
240
|
-
if pattern and not fnmatch.fnmatch(entry.name, pattern):
|
|
241
|
-
continue
|
|
242
|
-
files.append({
|
|
243
|
-
"name": entry.name,
|
|
244
|
-
"path": entry.name,
|
|
245
|
-
"size": entry.stat().st_size,
|
|
246
|
-
})
|
|
368
|
+
path: str = Field(description="Relative path to the file to edit")
|
|
369
|
+
edits: List[EditOperation] = Field(
|
|
370
|
+
description="List of edit operations to apply sequentially"
|
|
371
|
+
)
|
|
372
|
+
execution_result: Optional[Dict[str, Any]] = Field(
|
|
373
|
+
default=None,
|
|
374
|
+
description="Optional execution result payload from the client",
|
|
375
|
+
)
|
|
247
376
|
|
|
377
|
+
|
|
378
|
+
@tool(args_schema=MultiEditInput)
|
|
379
|
+
def multiedit_file_tool(
|
|
380
|
+
path: str,
|
|
381
|
+
edits: List[EditOperation],
|
|
382
|
+
execution_result: Optional[Dict[str, Any]] = None,
|
|
383
|
+
workspace_root: str = ".",
|
|
384
|
+
) -> Dict[str, Any]:
|
|
385
|
+
"""
|
|
386
|
+
Apply multiple sequential edits to a single file atomically.
|
|
387
|
+
|
|
388
|
+
This is more efficient than multiple edit_file_tool calls when you need
|
|
389
|
+
to make several changes to the same file. All edits are validated before
|
|
390
|
+
any are applied - if one fails, none are applied.
|
|
391
|
+
|
|
392
|
+
Use this tool when:
|
|
393
|
+
- Making multiple related changes to a file
|
|
394
|
+
- Updating several config values at once
|
|
395
|
+
- Refactoring multiple sections of code
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
path: Relative path to the file
|
|
399
|
+
edits: List of edit operations, each containing:
|
|
400
|
+
- old_string: The exact string to find and replace
|
|
401
|
+
- new_string: The replacement string
|
|
402
|
+
- replace_all: (optional) Whether to replace all occurrences
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Dict with operation status, edits_applied count, and diff preview
|
|
406
|
+
|
|
407
|
+
Example:
|
|
408
|
+
multiedit_file_tool(
|
|
409
|
+
path="config.py",
|
|
410
|
+
edits=[
|
|
411
|
+
{"old_string": "DEBUG = True", "new_string": "DEBUG = False"},
|
|
412
|
+
{"old_string": "LOG_LEVEL = 'INFO'", "new_string": "LOG_LEVEL = 'WARNING'"}
|
|
413
|
+
]
|
|
414
|
+
)
|
|
415
|
+
"""
|
|
416
|
+
# Security validation
|
|
417
|
+
if os.path.isabs(path):
|
|
418
|
+
return {
|
|
419
|
+
"tool": "multiedit_file_tool",
|
|
420
|
+
"success": False,
|
|
421
|
+
"error": f"Absolute paths not allowed: {path}",
|
|
422
|
+
"path": path,
|
|
423
|
+
}
|
|
424
|
+
if ".." in path:
|
|
248
425
|
return {
|
|
249
|
-
"tool": "
|
|
250
|
-
"success":
|
|
426
|
+
"tool": "multiedit_file_tool",
|
|
427
|
+
"success": False,
|
|
428
|
+
"error": f"Parent directory traversal not allowed: {path}",
|
|
251
429
|
"path": path,
|
|
252
|
-
"directories": sorted(dirs),
|
|
253
|
-
"files": sorted(files, key=lambda x: x["name"]),
|
|
254
|
-
"total_dirs": len(dirs),
|
|
255
|
-
"total_files": len(files),
|
|
256
430
|
}
|
|
257
431
|
|
|
258
|
-
|
|
432
|
+
if not edits or len(edits) == 0:
|
|
259
433
|
return {
|
|
260
|
-
"tool": "
|
|
434
|
+
"tool": "multiedit_file_tool",
|
|
261
435
|
"success": False,
|
|
262
|
-
"error":
|
|
436
|
+
"error": "At least one edit is required",
|
|
437
|
+
"path": path,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
resolved_path = _validate_path(path, workspace_root)
|
|
442
|
+
|
|
443
|
+
# Build edits preview (handle both EditOperation objects and dicts)
|
|
444
|
+
edits_preview = []
|
|
445
|
+
edits_as_dicts = []
|
|
446
|
+
for i, edit in enumerate(edits[:5]): # Preview first 5
|
|
447
|
+
# Support both Pydantic model and dict access
|
|
448
|
+
if hasattr(edit, "old_string"):
|
|
449
|
+
old_str = edit.old_string
|
|
450
|
+
new_str = edit.new_string
|
|
451
|
+
replace_all_val = edit.replace_all
|
|
452
|
+
else:
|
|
453
|
+
old_str = edit.get("old_string", "")
|
|
454
|
+
new_str = edit.get("new_string", "")
|
|
455
|
+
replace_all_val = edit.get("replace_all", False)
|
|
456
|
+
|
|
457
|
+
old_preview = (old_str[:50] + "...") if len(old_str) > 50 else old_str
|
|
458
|
+
new_preview = (new_str[:50] + "...") if len(new_str) > 50 else new_str
|
|
459
|
+
edits_preview.append({
|
|
460
|
+
"index": i,
|
|
461
|
+
"old_preview": old_preview,
|
|
462
|
+
"new_preview": new_preview,
|
|
463
|
+
"replace_all": replace_all_val
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
# Convert all edits to dicts for serialization
|
|
467
|
+
for edit in edits:
|
|
468
|
+
if hasattr(edit, "model_dump"):
|
|
469
|
+
edits_as_dicts.append(edit.model_dump())
|
|
470
|
+
elif hasattr(edit, "dict"):
|
|
471
|
+
edits_as_dicts.append(edit.dict())
|
|
472
|
+
else:
|
|
473
|
+
edits_as_dicts.append(edit)
|
|
474
|
+
|
|
475
|
+
response: Dict[str, Any] = {
|
|
476
|
+
"tool": "multiedit_file_tool",
|
|
477
|
+
"parameters": {
|
|
478
|
+
"path": path,
|
|
479
|
+
"edits_count": len(edits),
|
|
480
|
+
"edits": edits_as_dicts,
|
|
481
|
+
},
|
|
482
|
+
"status": "pending_approval",
|
|
263
483
|
"path": path,
|
|
484
|
+
"resolved_path": resolved_path,
|
|
485
|
+
"edits_preview": edits_preview,
|
|
486
|
+
"total_edits": len(edits),
|
|
487
|
+
"message": f"Multi-edit operation ({len(edits)} edits) requires user approval",
|
|
264
488
|
}
|
|
265
|
-
|
|
489
|
+
|
|
490
|
+
if execution_result is not None:
|
|
491
|
+
response["execution_result"] = execution_result
|
|
492
|
+
response["status"] = "complete"
|
|
493
|
+
response["message"] = "Multi-edit executed with client-reported results"
|
|
494
|
+
if "diff" in execution_result:
|
|
495
|
+
response["diff"] = execution_result["diff"]
|
|
496
|
+
if "edits_applied" in execution_result:
|
|
497
|
+
response["edits_applied"] = execution_result["edits_applied"]
|
|
498
|
+
if "edits_failed" in execution_result:
|
|
499
|
+
response["edits_failed"] = execution_result["edits_failed"]
|
|
500
|
+
|
|
501
|
+
return response
|
|
502
|
+
|
|
503
|
+
except ValueError as e:
|
|
266
504
|
return {
|
|
267
|
-
"tool": "
|
|
505
|
+
"tool": "multiedit_file_tool",
|
|
268
506
|
"success": False,
|
|
269
|
-
"error":
|
|
507
|
+
"error": str(e),
|
|
270
508
|
"path": path,
|
|
271
509
|
}
|
|
272
510
|
|
|
@@ -275,5 +513,7 @@ def list_files_tool(
|
|
|
275
513
|
FILE_TOOLS = [
|
|
276
514
|
read_file_tool,
|
|
277
515
|
write_file_tool,
|
|
516
|
+
edit_file_tool,
|
|
517
|
+
multiedit_file_tool,
|
|
278
518
|
list_files_tool,
|
|
279
519
|
]
|