toolplane-python-client 0.1.0__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.
- toolplane/__init__.py +106 -0
- toolplane/common/__init__.py +93 -0
- toolplane/common/base_config.py +129 -0
- toolplane/common/base_connection_manager.py +171 -0
- toolplane/common/base_session_manager.py +321 -0
- toolplane/common/base_tool_manager.py +347 -0
- toolplane/common/constants.py +47 -0
- toolplane/common/utils.py +310 -0
- toolplane/core/__init__.py +67 -0
- toolplane/core/config.py +107 -0
- toolplane/core/connection.py +285 -0
- toolplane/core/errors.py +298 -0
- toolplane/core/machine.py +480 -0
- toolplane/core/request.py +775 -0
- toolplane/core/session.py +332 -0
- toolplane/core/session_context.py +514 -0
- toolplane/core/task.py +130 -0
- toolplane/core/tool.py +329 -0
- toolplane/http_core/__init__.py +37 -0
- toolplane/http_core/http_config.py +97 -0
- toolplane/http_core/http_connection.py +409 -0
- toolplane/http_core/http_machine.py +298 -0
- toolplane/http_core/http_request.py +748 -0
- toolplane/http_core/http_session.py +348 -0
- toolplane/http_core/http_session_context.py +491 -0
- toolplane/http_core/http_task.py +101 -0
- toolplane/http_core/http_tool.py +400 -0
- toolplane/interfaces/__init__.py +27 -0
- toolplane/interfaces/client_interface.py +122 -0
- toolplane/interfaces/connection_interface.py +193 -0
- toolplane/interfaces/event_interface.py +290 -0
- toolplane/interfaces/request_interface.py +439 -0
- toolplane/interfaces/session_interface.py +288 -0
- toolplane/interfaces/tool_interface.py +441 -0
- toolplane/proto/__init__.py +0 -0
- toolplane/proto/service_pb2.py +315 -0
- toolplane/proto/service_pb2_grpc.py +2240 -0
- toolplane/provider_cli.py +268 -0
- toolplane/provider_registry.py +77 -0
- toolplane/provider_runtime.py +302 -0
- toolplane/toolkits/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/create_directory.py +94 -0
- toolplane/toolkits/standalone_tools/create_file.py +124 -0
- toolplane/toolkits/standalone_tools/file_search.py +229 -0
- toolplane/toolkits/standalone_tools/grep_search.py +372 -0
- toolplane/toolkits/standalone_tools/launcher.py +146 -0
- toolplane/toolkits/standalone_tools/list_dir.py +395 -0
- toolplane/toolkits/standalone_tools/read_file.py +346 -0
- toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
- toolplane/toolkits/standalone_tools/run_tests.py +66 -0
- toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
- toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
- toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
- toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
- toolplane/toolkits/swe/__init__.py +35 -0
- toolplane/toolkits/swe/create_directory.py +15 -0
- toolplane/toolkits/swe/create_file.py +15 -0
- toolplane/toolkits/swe/descriptions.py +273 -0
- toolplane/toolkits/swe/execute_bash.py +93 -0
- toolplane/toolkits/swe/file_editor.py +775 -0
- toolplane/toolkits/swe/file_search.py +16 -0
- toolplane/toolkits/swe/finish.py +50 -0
- toolplane/toolkits/swe/grep_search.py +19 -0
- toolplane/toolkits/swe/list_dir.py +407 -0
- toolplane/toolkits/swe/read_file.py +18 -0
- toolplane/toolkits/swe/replace_string_in_file.py +17 -0
- toolplane/toolkits/swe/search.py +260 -0
- toolplane/toolkits/swe/semantic_search.py +20 -0
- toolplane/toolkits/swe/str_replace_editor.py +647 -0
- toolplane/toolkits/swe/submit.py +29 -0
- toolplane/toolkits/swe/swe_toolkit.py +1296 -0
- toolplane/toolplane_client.py +686 -0
- toolplane/toolplane_http_client.py +681 -0
- toolplane/utils/__init__.py +3 -0
- toolplane/utils/schema.py +146 -0
- toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
- toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
- toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
- toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
- toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1296 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Type, Union
|
|
9
|
+
|
|
10
|
+
from langchain.tools import BaseTool
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
# Import descriptions from descriptions.py
|
|
14
|
+
try:
|
|
15
|
+
from toolplane.toolkits.swe.descriptions import (
|
|
16
|
+
_BASH_DESCRIPTION,
|
|
17
|
+
_FILE_EDITOR_DESCRIPTION,
|
|
18
|
+
_FINISH_DESCRIPTION,
|
|
19
|
+
_SEARCH_DESCRIPTION,
|
|
20
|
+
_SUBMIT_DESCRIPTION,
|
|
21
|
+
)
|
|
22
|
+
except ImportError:
|
|
23
|
+
# Fallback for when running as standalone
|
|
24
|
+
from descriptions import (
|
|
25
|
+
_BASH_DESCRIPTION,
|
|
26
|
+
_FILE_EDITOR_DESCRIPTION,
|
|
27
|
+
_FINISH_DESCRIPTION,
|
|
28
|
+
_SEARCH_DESCRIPTION,
|
|
29
|
+
_SUBMIT_DESCRIPTION,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Import functions from individual tool files
|
|
33
|
+
try:
|
|
34
|
+
from toolplane.toolkits.swe.execute_bash import run_command
|
|
35
|
+
from toolplane.toolkits.swe.finish import submit as finish_submit
|
|
36
|
+
from toolplane.toolkits.swe.read_file import read_file
|
|
37
|
+
from toolplane.toolkits.swe.search import search_in_directory, search_in_file
|
|
38
|
+
from toolplane.toolkits.swe.str_replace_editor import (
|
|
39
|
+
StrReplaceEditor,
|
|
40
|
+
load_history,
|
|
41
|
+
save_history,
|
|
42
|
+
)
|
|
43
|
+
from toolplane.toolkits.swe.submit import submit as simple_submit
|
|
44
|
+
except ImportError:
|
|
45
|
+
# Fallback for when running as standalone
|
|
46
|
+
from execute_bash import run_command
|
|
47
|
+
from finish import submit as finish_submit
|
|
48
|
+
from read_file import read_file
|
|
49
|
+
from search import search_in_directory, search_in_file
|
|
50
|
+
from str_replace_editor import StrReplaceEditor, load_history, save_history
|
|
51
|
+
from submit import submit as simple_submit
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ToolExecutionError(RuntimeError):
|
|
55
|
+
"""A tool failed while executing.
|
|
56
|
+
|
|
57
|
+
Raised — never returned as text — so the provider submits a rejection
|
|
58
|
+
and the request records FAILED: models, eval scoring, and retry
|
|
59
|
+
heuristics see the failure instead of parsing output strings.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
#!/usr/bin/env python3
|
|
64
|
+
"""
|
|
65
|
+
Standalone Toolkit for LangChain Integration
|
|
66
|
+
|
|
67
|
+
This module provides a comprehensive set of standalone development tools
|
|
68
|
+
wrapped as LangChain tools for AI agent integration.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
import os
|
|
72
|
+
import sys
|
|
73
|
+
from pathlib import Path
|
|
74
|
+
from typing import Any, Dict, List, Optional, Type, Union
|
|
75
|
+
|
|
76
|
+
from langchain.tools import BaseTool
|
|
77
|
+
from langchain_core.callbacks.manager import (
|
|
78
|
+
AsyncCallbackManagerForToolRun,
|
|
79
|
+
CallbackManagerForToolRun,
|
|
80
|
+
)
|
|
81
|
+
from pydantic import BaseModel, Field
|
|
82
|
+
|
|
83
|
+
# Import functions from standalone tools
|
|
84
|
+
try:
|
|
85
|
+
from toolplane.toolkits.swe.create_directory import create_directory
|
|
86
|
+
from toolplane.toolkits.swe.create_file import create_file
|
|
87
|
+
from toolplane.toolkits.swe.file_search import file_search
|
|
88
|
+
from toolplane.toolkits.swe.grep_search import grep_search
|
|
89
|
+
from toolplane.toolkits.swe.list_dir import list_dir
|
|
90
|
+
from toolplane.toolkits.swe.replace_string_in_file import replace_string_in_file
|
|
91
|
+
from toolplane.toolkits.swe.semantic_search import semantic_search
|
|
92
|
+
except ImportError:
|
|
93
|
+
# Fallback for when running as standalone
|
|
94
|
+
from create_directory import create_directory
|
|
95
|
+
from create_file import create_file
|
|
96
|
+
from file_search import file_search
|
|
97
|
+
from grep_search import grep_search
|
|
98
|
+
from list_dir import list_dir
|
|
99
|
+
from replace_string_in_file import replace_string_in_file
|
|
100
|
+
from semantic_search import semantic_search
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Input Models for each tool
|
|
104
|
+
class CreateDirectoryInput(BaseModel):
|
|
105
|
+
dir_path: str = Field(description="The absolute path to the directory to create")
|
|
106
|
+
mode: Optional[int] = Field(
|
|
107
|
+
default=0o755, description="Permission mode for the directory"
|
|
108
|
+
)
|
|
109
|
+
parents: Optional[bool] = Field(
|
|
110
|
+
default=True, description="Create parent directories if they don't exist"
|
|
111
|
+
)
|
|
112
|
+
exist_ok: Optional[bool] = Field(
|
|
113
|
+
default=True, description="Don't raise error if directory already exists"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class WriteFileInput(BaseModel):
|
|
118
|
+
file_path: str = Field(description="The absolute path to the file to create")
|
|
119
|
+
content: str = Field(description="The content to write to the file")
|
|
120
|
+
encoding: Optional[str] = Field(
|
|
121
|
+
default="utf-8", description="The encoding to use for the file"
|
|
122
|
+
)
|
|
123
|
+
mode: Optional[str] = Field(default="w", description="The file creation mode")
|
|
124
|
+
auto_create_dirs: Optional[bool] = Field(
|
|
125
|
+
default=True, description="Create parent directories if they don't exist"
|
|
126
|
+
)
|
|
127
|
+
overwrite: Optional[bool] = Field(
|
|
128
|
+
default=False, description="Overwrite the file if it already exists"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class FetchWebpageInput(BaseModel):
|
|
133
|
+
urls: List[str] = Field(description="List of URLs to fetch content from")
|
|
134
|
+
query: str = Field(description="The query to search for in the web page's content")
|
|
135
|
+
timeout: Optional[int] = Field(default=30, description="Request timeout in seconds")
|
|
136
|
+
max_content_length: Optional[int] = Field(
|
|
137
|
+
default=50000, description="Maximum content length to process"
|
|
138
|
+
)
|
|
139
|
+
user_agent: Optional[str] = Field(
|
|
140
|
+
default=None, description="Custom user agent string"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class FileSearchInput(BaseModel):
|
|
145
|
+
query: str = Field(description="Glob pattern to search for files")
|
|
146
|
+
max_results: Optional[int] = Field(
|
|
147
|
+
default=None, description="Maximum number of results to return"
|
|
148
|
+
)
|
|
149
|
+
include_hidden: Optional[bool] = Field(
|
|
150
|
+
default=False, description="Include hidden files in results"
|
|
151
|
+
)
|
|
152
|
+
sort_by: Optional[str] = Field(
|
|
153
|
+
default="name", description="Sort results by 'name', 'size', or 'modified'"
|
|
154
|
+
)
|
|
155
|
+
reverse_sort: Optional[bool] = Field(
|
|
156
|
+
default=False, description="Reverse the sort order"
|
|
157
|
+
)
|
|
158
|
+
show_details: Optional[bool] = Field(
|
|
159
|
+
default=False, description="Show file details like size and modification time"
|
|
160
|
+
)
|
|
161
|
+
base_path: Optional[str] = Field(
|
|
162
|
+
default=None, description="Base directory to search from"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class GrepSearchInput(BaseModel):
|
|
167
|
+
query: str = Field(description="The pattern to search for in files")
|
|
168
|
+
is_regexp: bool = Field(description="Whether the pattern is a regex")
|
|
169
|
+
include_pattern: Optional[str] = Field(
|
|
170
|
+
default=None, description="Search files matching this glob pattern"
|
|
171
|
+
)
|
|
172
|
+
max_results: Optional[int] = Field(
|
|
173
|
+
default=None, description="Maximum number of results to return"
|
|
174
|
+
)
|
|
175
|
+
context_lines: Optional[int] = Field(
|
|
176
|
+
default=0, description="Number of context lines to show around matches"
|
|
177
|
+
)
|
|
178
|
+
ignore_case: Optional[bool] = Field(
|
|
179
|
+
default=False, description="Perform case-insensitive search"
|
|
180
|
+
)
|
|
181
|
+
whole_word: Optional[bool] = Field(
|
|
182
|
+
default=False, description="Match whole words only"
|
|
183
|
+
)
|
|
184
|
+
invert_match: Optional[bool] = Field(
|
|
185
|
+
default=False, description="Show lines that don't match the pattern"
|
|
186
|
+
)
|
|
187
|
+
base_path: Optional[str] = Field(
|
|
188
|
+
default=None, description="Base directory to search from"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ListDirInput(BaseModel):
|
|
193
|
+
path: Optional[str] = Field(
|
|
194
|
+
default=".",
|
|
195
|
+
description="Directory to list. Use '.' for current working directory. Accepts absolute or relative paths.",
|
|
196
|
+
)
|
|
197
|
+
show_hidden: Optional[bool] = Field(
|
|
198
|
+
default=False, description="Show hidden files and directories"
|
|
199
|
+
)
|
|
200
|
+
show_details: Optional[bool] = Field(
|
|
201
|
+
default=False, description="Show detailed information like size and permissions"
|
|
202
|
+
)
|
|
203
|
+
sort_by: Optional[str] = Field(
|
|
204
|
+
default="name", description="Sort by 'name', 'size', 'modified', or 'type'"
|
|
205
|
+
)
|
|
206
|
+
reverse_sort: Optional[bool] = Field(
|
|
207
|
+
default=False, description="Reverse the sort order"
|
|
208
|
+
)
|
|
209
|
+
recursive: Optional[bool] = Field(
|
|
210
|
+
default=False, description="List contents recursively"
|
|
211
|
+
)
|
|
212
|
+
max_depth: Optional[int] = Field(
|
|
213
|
+
default=3, description="Maximum depth for recursive listing"
|
|
214
|
+
)
|
|
215
|
+
file_filter: Optional[str] = Field(
|
|
216
|
+
default=None, description="Filter files by extension"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class ReadFileInput(BaseModel):
|
|
221
|
+
file_path: str = Field(description="The absolute path of the file to read")
|
|
222
|
+
start_line: int = Field(
|
|
223
|
+
description="The line number to start reading from (1-based)"
|
|
224
|
+
)
|
|
225
|
+
end_line: int = Field(
|
|
226
|
+
description="The inclusive line number to end reading at (1-based, -1 for end)"
|
|
227
|
+
)
|
|
228
|
+
encoding: Optional[str] = Field(
|
|
229
|
+
default=None, description="The encoding to use for reading the file"
|
|
230
|
+
)
|
|
231
|
+
show_line_numbers: Optional[bool] = Field(
|
|
232
|
+
default=True, description="Show line numbers in output"
|
|
233
|
+
)
|
|
234
|
+
highlight_syntax: Optional[bool] = Field(
|
|
235
|
+
default=False, description="Attempt to highlight syntax"
|
|
236
|
+
)
|
|
237
|
+
max_line_length: Optional[int] = Field(
|
|
238
|
+
default=1000, description="Maximum line length before truncation"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class ReplaceStringInput(BaseModel):
|
|
243
|
+
file_path: str = Field(description="The absolute path to the file to edit")
|
|
244
|
+
old_string: str = Field(description="The string to be replaced")
|
|
245
|
+
new_string: str = Field(description="The replacement string")
|
|
246
|
+
create_backup: Optional[bool] = Field(
|
|
247
|
+
default=True, description="Create a backup before editing"
|
|
248
|
+
)
|
|
249
|
+
dry_run: Optional[bool] = Field(
|
|
250
|
+
default=False, description="Show what would be changed without making changes"
|
|
251
|
+
)
|
|
252
|
+
whole_word: Optional[bool] = Field(
|
|
253
|
+
default=False, description="Only replace whole words"
|
|
254
|
+
)
|
|
255
|
+
ignore_case: Optional[bool] = Field(
|
|
256
|
+
default=False, description="Perform case-insensitive matching"
|
|
257
|
+
)
|
|
258
|
+
max_replacements: Optional[int] = Field(
|
|
259
|
+
default=None, description="Maximum number of replacements to make"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class SemanticSearchInput(BaseModel):
|
|
264
|
+
query: str = Field(description="The search query in natural language")
|
|
265
|
+
max_results: Optional[int] = Field(
|
|
266
|
+
default=10, description="Maximum number of results to return"
|
|
267
|
+
)
|
|
268
|
+
file_types: Optional[List[str]] = Field(
|
|
269
|
+
default=None, description="File types to search in"
|
|
270
|
+
)
|
|
271
|
+
similarity_threshold: Optional[float] = Field(
|
|
272
|
+
default=0.1, description="Minimum similarity score to include"
|
|
273
|
+
)
|
|
274
|
+
context_size: Optional[int] = Field(
|
|
275
|
+
default=3, description="Number of lines of context around matches"
|
|
276
|
+
)
|
|
277
|
+
search_path: Optional[str] = Field(
|
|
278
|
+
default=None, description="Directory to search in"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class WebSearchInput(BaseModel):
|
|
283
|
+
query: str = Field(description="The search query")
|
|
284
|
+
max_results: Optional[int] = Field(
|
|
285
|
+
default=10, description="Maximum number of results to return"
|
|
286
|
+
)
|
|
287
|
+
search_engine: Optional[str] = Field(
|
|
288
|
+
default="duckduckgo", description="Search engine to use"
|
|
289
|
+
)
|
|
290
|
+
include_content: Optional[bool] = Field(
|
|
291
|
+
default=False, description="Include page content in results"
|
|
292
|
+
)
|
|
293
|
+
content_length: Optional[int] = Field(
|
|
294
|
+
default=1000, description="Maximum content length to extract"
|
|
295
|
+
)
|
|
296
|
+
filter_domain: Optional[str] = Field(
|
|
297
|
+
default=None, description="Only include results from this domain"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class TestFailureAnalysisInput(BaseModel):
|
|
302
|
+
test_output: Optional[str] = Field(
|
|
303
|
+
default=None, description="Path to test output file or direct test output"
|
|
304
|
+
)
|
|
305
|
+
test_framework: Optional[str] = Field(
|
|
306
|
+
default=None, description="Test framework used"
|
|
307
|
+
)
|
|
308
|
+
verbose: Optional[bool] = Field(default=False, description="Show detailed analysis")
|
|
309
|
+
suggest_fixes: Optional[bool] = Field(
|
|
310
|
+
default=True, description="Suggest potential fixes"
|
|
311
|
+
)
|
|
312
|
+
group_by_type: Optional[bool] = Field(
|
|
313
|
+
default=True, description="Group failures by error type"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# Tool Classes
|
|
318
|
+
class CreateDirectoryTool(BaseTool):
|
|
319
|
+
"""📁 Create directories with enhanced features and permission handling."""
|
|
320
|
+
|
|
321
|
+
name: str = "create_directory"
|
|
322
|
+
description: str = """Create a directory structure with enhanced features.
|
|
323
|
+
|
|
324
|
+
This tool creates directories recursively (like mkdir -p) and provides
|
|
325
|
+
additional features like permission handling and validation.
|
|
326
|
+
|
|
327
|
+
Best for:
|
|
328
|
+
- Creating project directory structures
|
|
329
|
+
- Setting up development environments
|
|
330
|
+
- Organizing file systems
|
|
331
|
+
- Batch directory creation"""
|
|
332
|
+
|
|
333
|
+
args_schema: Type[BaseModel] = CreateDirectoryInput
|
|
334
|
+
|
|
335
|
+
def _run(
|
|
336
|
+
self,
|
|
337
|
+
dir_path: str,
|
|
338
|
+
mode: Optional[int] = 0o755,
|
|
339
|
+
parents: Optional[bool] = True,
|
|
340
|
+
exist_ok: Optional[bool] = True,
|
|
341
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
342
|
+
) -> str:
|
|
343
|
+
try:
|
|
344
|
+
success = create_directory(
|
|
345
|
+
dir_path=dir_path,
|
|
346
|
+
mode=mode or 0o755,
|
|
347
|
+
parents=parents if parents is not None else True,
|
|
348
|
+
exist_ok=exist_ok if exist_ok is not None else True,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if success:
|
|
352
|
+
return f"Directory created successfully: {dir_path}"
|
|
353
|
+
else:
|
|
354
|
+
raise ToolExecutionError(f"Failed to create directory: {dir_path}")
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
raise ToolExecutionError(f"Error creating directory: {str(e)}")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class WriteFileTool(BaseTool):
|
|
361
|
+
"""📄 Create files with specified content and automatic directory creation."""
|
|
362
|
+
|
|
363
|
+
name: str = "write_file"
|
|
364
|
+
description: str = """Write to a file or create a new file with the given content and automatically creates
|
|
365
|
+
parent directories if they don't exist. It includes validation and error handling.
|
|
366
|
+
|
|
367
|
+
Best for:
|
|
368
|
+
- Creating new source files
|
|
369
|
+
- Generating configuration files
|
|
370
|
+
- Setting up project templates
|
|
371
|
+
- Batch file creation"""
|
|
372
|
+
|
|
373
|
+
args_schema: Type[BaseModel] = WriteFileInput
|
|
374
|
+
|
|
375
|
+
def _run(
|
|
376
|
+
self,
|
|
377
|
+
file_path: str,
|
|
378
|
+
content: str,
|
|
379
|
+
encoding: Optional[str] = "utf-8",
|
|
380
|
+
mode: Optional[str] = "w",
|
|
381
|
+
auto_create_dirs: Optional[bool] = True,
|
|
382
|
+
overwrite: Optional[bool] = False,
|
|
383
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
384
|
+
) -> str:
|
|
385
|
+
try:
|
|
386
|
+
success = create_file(
|
|
387
|
+
file_path=file_path,
|
|
388
|
+
content=content,
|
|
389
|
+
encoding=encoding or "utf-8",
|
|
390
|
+
mode=mode or "w",
|
|
391
|
+
auto_create_dirs=(
|
|
392
|
+
auto_create_dirs if auto_create_dirs is not None else True
|
|
393
|
+
),
|
|
394
|
+
overwrite=overwrite if overwrite is not None else False,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if success:
|
|
398
|
+
return f"File created successfully: {file_path}"
|
|
399
|
+
else:
|
|
400
|
+
raise ToolExecutionError(f"Failed to create file: {file_path}")
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
raise ToolExecutionError(f"Error creating file: {str(e)}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class FileSearchTool(BaseTool):
|
|
407
|
+
"""🔍 Search for files using glob patterns with advanced features."""
|
|
408
|
+
|
|
409
|
+
name: str = "file_search"
|
|
410
|
+
description: str = """Search for files using glob patterns and provides additional
|
|
411
|
+
filtering and sorting options. It returns file paths matching the pattern.
|
|
412
|
+
|
|
413
|
+
Best for:
|
|
414
|
+
- Finding files by pattern
|
|
415
|
+
- Project file discovery
|
|
416
|
+
- Build system file location
|
|
417
|
+
- Code organization analysis"""
|
|
418
|
+
|
|
419
|
+
args_schema: Type[BaseModel] = FileSearchInput
|
|
420
|
+
|
|
421
|
+
def _run(
|
|
422
|
+
self,
|
|
423
|
+
query: str,
|
|
424
|
+
max_results: Optional[int] = None,
|
|
425
|
+
include_hidden: Optional[bool] = False,
|
|
426
|
+
sort_by: Optional[str] = "name",
|
|
427
|
+
reverse_sort: Optional[bool] = False,
|
|
428
|
+
show_details: Optional[bool] = False,
|
|
429
|
+
base_path: Optional[str] = None,
|
|
430
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
431
|
+
) -> str:
|
|
432
|
+
try:
|
|
433
|
+
results = file_search(
|
|
434
|
+
query=query,
|
|
435
|
+
max_results=max_results,
|
|
436
|
+
include_hidden=include_hidden or False,
|
|
437
|
+
sort_by=sort_by or "name",
|
|
438
|
+
reverse_sort=reverse_sort or False,
|
|
439
|
+
show_details=show_details or False,
|
|
440
|
+
base_path=base_path,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if not results:
|
|
444
|
+
return f"No files found matching pattern: {query}"
|
|
445
|
+
|
|
446
|
+
# Format results for display
|
|
447
|
+
output = [f"Found {len(results)} files matching pattern: {query}"]
|
|
448
|
+
|
|
449
|
+
if show_details:
|
|
450
|
+
output.append("-" * 80)
|
|
451
|
+
for result in results:
|
|
452
|
+
file_type = "DIR" if result["is_dir"] else "FILE"
|
|
453
|
+
size_str = f"{result['size']} bytes" if result["is_file"] else "-"
|
|
454
|
+
output.append(f"{result['name']:<40} {file_type:<5} {size_str}")
|
|
455
|
+
else:
|
|
456
|
+
for result in results:
|
|
457
|
+
suffix = "/" if result["is_dir"] else ""
|
|
458
|
+
output.append(f"{result['path']}{suffix}")
|
|
459
|
+
|
|
460
|
+
return "\n".join(output)
|
|
461
|
+
|
|
462
|
+
except Exception as e:
|
|
463
|
+
raise ToolExecutionError(f"Error searching files: {str(e)}")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class GrepSearchTool(BaseTool):
|
|
467
|
+
"""🔎 Fast text search in files using grep-like functionality."""
|
|
468
|
+
|
|
469
|
+
name: str = "grep_search"
|
|
470
|
+
description: str = """Perform fast text search in files using exact strings or regex patterns.
|
|
471
|
+
It provides context lines, file filtering, and other advanced search options.
|
|
472
|
+
|
|
473
|
+
Best for:
|
|
474
|
+
- Code pattern searching
|
|
475
|
+
- Log file analysis
|
|
476
|
+
- Documentation searching
|
|
477
|
+
- Multi-file text analysis"""
|
|
478
|
+
|
|
479
|
+
args_schema: Type[BaseModel] = GrepSearchInput
|
|
480
|
+
|
|
481
|
+
def _run(
|
|
482
|
+
self,
|
|
483
|
+
query: str,
|
|
484
|
+
is_regexp: bool,
|
|
485
|
+
include_pattern: Optional[str] = None,
|
|
486
|
+
max_results: Optional[int] = None,
|
|
487
|
+
context_lines: Optional[int] = 0,
|
|
488
|
+
ignore_case: Optional[bool] = False,
|
|
489
|
+
whole_word: Optional[bool] = False,
|
|
490
|
+
invert_match: Optional[bool] = False,
|
|
491
|
+
base_path: Optional[str] = None,
|
|
492
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
493
|
+
) -> str:
|
|
494
|
+
try:
|
|
495
|
+
results = grep_search(
|
|
496
|
+
query=query,
|
|
497
|
+
is_regexp=is_regexp,
|
|
498
|
+
include_pattern=include_pattern,
|
|
499
|
+
max_results=max_results,
|
|
500
|
+
context_lines=context_lines or 0,
|
|
501
|
+
ignore_case=ignore_case or False,
|
|
502
|
+
whole_word=whole_word or False,
|
|
503
|
+
invert_match=invert_match or False,
|
|
504
|
+
base_path=base_path,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
if not results:
|
|
508
|
+
return f"No matches found for pattern: {query}"
|
|
509
|
+
|
|
510
|
+
# Format results for display
|
|
511
|
+
total_matches = sum(r["total_matches"] for r in results)
|
|
512
|
+
output = [f"Found {total_matches} matches in {len(results)} files"]
|
|
513
|
+
output.append("=" * 60)
|
|
514
|
+
|
|
515
|
+
for result in results:
|
|
516
|
+
if "error" in result:
|
|
517
|
+
output.append(f"Error in {result['file']}: {result['error']}")
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
output.append(f"\nFile: {result['file']}")
|
|
521
|
+
output.append(f"Matches: {result['total_matches']}")
|
|
522
|
+
output.append("-" * 40)
|
|
523
|
+
|
|
524
|
+
for match in result["matches"][:5]: # Show first 5 matches
|
|
525
|
+
if context_lines and match.get("context"):
|
|
526
|
+
for ctx in match["context"]:
|
|
527
|
+
prefix = ">" if ctx["is_match"] else " "
|
|
528
|
+
output.append(
|
|
529
|
+
f"{prefix}{ctx['line_number']:4d}: {ctx['content']}"
|
|
530
|
+
)
|
|
531
|
+
else:
|
|
532
|
+
output.append(f"{match['line_number']:4d}: {match['content']}")
|
|
533
|
+
|
|
534
|
+
return "\n".join(output)
|
|
535
|
+
|
|
536
|
+
except Exception as e:
|
|
537
|
+
raise ToolExecutionError(f"Error searching with grep: {str(e)}")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class ListDirTool(BaseTool):
|
|
541
|
+
"""📂 List directory contents with enhanced features."""
|
|
542
|
+
|
|
543
|
+
name: str = "list_dir"
|
|
544
|
+
description: str = """List the contents of a directory with various display options,
|
|
545
|
+
sorting capabilities, and filtering features.
|
|
546
|
+
|
|
547
|
+
Best for:
|
|
548
|
+
- Directory exploration
|
|
549
|
+
- Project structure analysis
|
|
550
|
+
- File system navigation
|
|
551
|
+
- Content organization"""
|
|
552
|
+
|
|
553
|
+
args_schema: Type[BaseModel] = ListDirInput
|
|
554
|
+
|
|
555
|
+
def _run(
|
|
556
|
+
self,
|
|
557
|
+
path: Optional[str] = ".",
|
|
558
|
+
show_hidden: Optional[bool] = False,
|
|
559
|
+
show_details: Optional[bool] = False,
|
|
560
|
+
sort_by: Optional[str] = "name",
|
|
561
|
+
reverse_sort: Optional[bool] = False,
|
|
562
|
+
recursive: Optional[bool] = False,
|
|
563
|
+
max_depth: Optional[int] = 3,
|
|
564
|
+
file_filter: Optional[str] = None,
|
|
565
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
566
|
+
) -> str:
|
|
567
|
+
try:
|
|
568
|
+
path = path or "."
|
|
569
|
+
normalized_path = os.path.abspath(path)
|
|
570
|
+
results = list_dir(
|
|
571
|
+
path=normalized_path,
|
|
572
|
+
show_hidden=show_hidden or False,
|
|
573
|
+
show_details=show_details or False,
|
|
574
|
+
sort_by=sort_by or "name",
|
|
575
|
+
reverse_sort=reverse_sort or False,
|
|
576
|
+
recursive=recursive or False,
|
|
577
|
+
max_depth=max_depth or 3,
|
|
578
|
+
file_filter=file_filter,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if not results:
|
|
582
|
+
return f"No contents found in directory: {path}"
|
|
583
|
+
|
|
584
|
+
# Format results for display
|
|
585
|
+
output = [f"Contents of {path}:"]
|
|
586
|
+
|
|
587
|
+
if show_details:
|
|
588
|
+
output.append(f"{'Name':<40} {'Type':<5} {'Size':<10}")
|
|
589
|
+
output.append("-" * 60)
|
|
590
|
+
|
|
591
|
+
for result in results:
|
|
592
|
+
file_type = "DIR" if result["is_dir"] else "FILE"
|
|
593
|
+
if result["is_symlink"]:
|
|
594
|
+
file_type = "LINK"
|
|
595
|
+
|
|
596
|
+
size_str = f"{result['size']} bytes" if result["is_file"] else "-"
|
|
597
|
+
indent = " " * result.get("depth", 0)
|
|
598
|
+
|
|
599
|
+
output.append(
|
|
600
|
+
f"{indent}{result['name']:<40} {file_type:<5} {size_str}"
|
|
601
|
+
)
|
|
602
|
+
else:
|
|
603
|
+
for result in results:
|
|
604
|
+
suffix = "/" if result["is_dir"] else ""
|
|
605
|
+
indent = " " * result.get("depth", 0)
|
|
606
|
+
output.append(f"{indent}{result['name']}{suffix}")
|
|
607
|
+
|
|
608
|
+
return "\n".join(output)
|
|
609
|
+
|
|
610
|
+
except Exception as e:
|
|
611
|
+
raise ToolExecutionError(f"Error listing directory: {str(e)}")
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class ReadFileTool(BaseTool):
|
|
615
|
+
"""📖 Read file contents with line range support and encoding detection."""
|
|
616
|
+
|
|
617
|
+
name: str = "read_file"
|
|
618
|
+
description: str = """Read the contents of a file with support for line ranges,
|
|
619
|
+
encoding detection, and various output formats.
|
|
620
|
+
|
|
621
|
+
Best for:
|
|
622
|
+
- Code review and analysis
|
|
623
|
+
- Configuration file inspection
|
|
624
|
+
- Log file examination
|
|
625
|
+
- Content verification"""
|
|
626
|
+
|
|
627
|
+
args_schema: Type[BaseModel] = ReadFileInput
|
|
628
|
+
|
|
629
|
+
def _run(
|
|
630
|
+
self,
|
|
631
|
+
file_path: str,
|
|
632
|
+
start_line: int,
|
|
633
|
+
end_line: int,
|
|
634
|
+
encoding: Optional[str] = None,
|
|
635
|
+
show_line_numbers: Optional[bool] = True,
|
|
636
|
+
highlight_syntax: Optional[bool] = False,
|
|
637
|
+
max_line_length: Optional[int] = 1000,
|
|
638
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
639
|
+
) -> str:
|
|
640
|
+
try:
|
|
641
|
+
result = read_file(
|
|
642
|
+
file_path=file_path,
|
|
643
|
+
start_line=start_line,
|
|
644
|
+
end_line=end_line,
|
|
645
|
+
encoding=encoding,
|
|
646
|
+
show_line_numbers=(
|
|
647
|
+
show_line_numbers if show_line_numbers is not None else True
|
|
648
|
+
),
|
|
649
|
+
highlight_syntax=highlight_syntax or False,
|
|
650
|
+
max_line_length=max_line_length or 1000,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if not result["success"]:
|
|
654
|
+
raise ToolExecutionError(f"Error: {result['error']}")
|
|
655
|
+
|
|
656
|
+
# Format output
|
|
657
|
+
output = []
|
|
658
|
+
output.append(f"File: {result['file_path']}")
|
|
659
|
+
output.append(
|
|
660
|
+
f"Lines: {result['start_line']}-{result['end_line']} (of {result['total_lines']})"
|
|
661
|
+
)
|
|
662
|
+
output.append(f"Encoding: {result['encoding']}")
|
|
663
|
+
if result["file_language"]:
|
|
664
|
+
output.append(f"Language: {result['file_language']}")
|
|
665
|
+
output.append("-" * 60)
|
|
666
|
+
|
|
667
|
+
# Content
|
|
668
|
+
for line_info in result["content"]:
|
|
669
|
+
if result["show_line_numbers"]:
|
|
670
|
+
line_num = f"{line_info['line_number']:4d}"
|
|
671
|
+
content = line_info["content"]
|
|
672
|
+
|
|
673
|
+
# Show truncation indicator
|
|
674
|
+
if line_info["original_length"] > len(content):
|
|
675
|
+
truncated = " [truncated]"
|
|
676
|
+
else:
|
|
677
|
+
truncated = ""
|
|
678
|
+
|
|
679
|
+
output.append(f"{line_num}: {content}{truncated}")
|
|
680
|
+
else:
|
|
681
|
+
output.append(line_info["content"])
|
|
682
|
+
|
|
683
|
+
return "\n".join(output)
|
|
684
|
+
|
|
685
|
+
except Exception as e:
|
|
686
|
+
raise ToolExecutionError(f"Error reading file: {str(e)}")
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class ReplaceStringTool(BaseTool):
|
|
690
|
+
"""🔄 Replace strings in files with safety checks and backup options."""
|
|
691
|
+
|
|
692
|
+
name: str = "replace_string"
|
|
693
|
+
description: str = """Replace strings in files with validation, backup options,
|
|
694
|
+
and various safety features to prevent accidental data loss.
|
|
695
|
+
|
|
696
|
+
Best for:
|
|
697
|
+
- Code refactoring
|
|
698
|
+
- Configuration updates
|
|
699
|
+
- Batch text replacements
|
|
700
|
+
- Safe file modifications"""
|
|
701
|
+
|
|
702
|
+
args_schema: Type[BaseModel] = ReplaceStringInput
|
|
703
|
+
|
|
704
|
+
def _run(
|
|
705
|
+
self,
|
|
706
|
+
file_path: str,
|
|
707
|
+
old_string: str,
|
|
708
|
+
new_string: str,
|
|
709
|
+
create_backup: Optional[bool] = True,
|
|
710
|
+
dry_run: Optional[bool] = False,
|
|
711
|
+
whole_word: Optional[bool] = False,
|
|
712
|
+
ignore_case: Optional[bool] = False,
|
|
713
|
+
max_replacements: Optional[int] = None,
|
|
714
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
715
|
+
) -> str:
|
|
716
|
+
try:
|
|
717
|
+
result = replace_string_in_file(
|
|
718
|
+
file_path=file_path,
|
|
719
|
+
old_string=old_string,
|
|
720
|
+
new_string=new_string,
|
|
721
|
+
create_backup=create_backup if create_backup is not None else True,
|
|
722
|
+
dry_run=dry_run or False,
|
|
723
|
+
whole_word=whole_word or False,
|
|
724
|
+
ignore_case=ignore_case or False,
|
|
725
|
+
max_replacements=max_replacements,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
if not result["success"]:
|
|
729
|
+
raise ToolExecutionError(f"Error: {result['error']}")
|
|
730
|
+
|
|
731
|
+
output = [result["message"]]
|
|
732
|
+
|
|
733
|
+
if result.get("dry_run") and "changes_preview" in result:
|
|
734
|
+
output.append("\nChanges preview:")
|
|
735
|
+
for change in result["changes_preview"]:
|
|
736
|
+
output.append(
|
|
737
|
+
f"\nMatch {change['match_number']} at line {change['line_number']}:"
|
|
738
|
+
)
|
|
739
|
+
output.append(f"Before: {change['before']}")
|
|
740
|
+
output.append(f"After: {change['after']}")
|
|
741
|
+
|
|
742
|
+
if result.get("backup_created"):
|
|
743
|
+
output.append(f"Backup created: {result['backup_path']}")
|
|
744
|
+
|
|
745
|
+
return "\n".join(output)
|
|
746
|
+
|
|
747
|
+
except Exception as e:
|
|
748
|
+
raise ToolExecutionError(f"Error replacing string: {str(e)}")
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
class SemanticSearchTool(BaseTool):
|
|
752
|
+
"""🧠 Semantic search for relevant code using text similarity."""
|
|
753
|
+
|
|
754
|
+
name: str = "semantic_search"
|
|
755
|
+
description: str = """Perform semantic search across code and documentation files using
|
|
756
|
+
text similarity algorithms to find relevant content based on natural language queries.
|
|
757
|
+
|
|
758
|
+
Best for:
|
|
759
|
+
- Finding relevant code snippets
|
|
760
|
+
- Documentation discovery
|
|
761
|
+
- Code understanding and navigation
|
|
762
|
+
- Knowledge base search"""
|
|
763
|
+
|
|
764
|
+
args_schema: Type[BaseModel] = SemanticSearchInput
|
|
765
|
+
|
|
766
|
+
def _run(
|
|
767
|
+
self,
|
|
768
|
+
query: str,
|
|
769
|
+
max_results: Optional[int] = 10,
|
|
770
|
+
file_types: Optional[List[str]] = None,
|
|
771
|
+
similarity_threshold: Optional[float] = 0.1,
|
|
772
|
+
context_size: Optional[int] = 3,
|
|
773
|
+
search_path: Optional[str] = None,
|
|
774
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
775
|
+
) -> str:
|
|
776
|
+
try:
|
|
777
|
+
results = semantic_search(
|
|
778
|
+
query=query,
|
|
779
|
+
max_results=max_results or 10,
|
|
780
|
+
file_types=file_types,
|
|
781
|
+
similarity_threshold=similarity_threshold or 0.1,
|
|
782
|
+
context_size=context_size or 3,
|
|
783
|
+
search_path=search_path,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
if not results:
|
|
787
|
+
return f"No results found for query: {query}"
|
|
788
|
+
|
|
789
|
+
# Format results for display
|
|
790
|
+
output = [f"Semantic search results for: {query}"]
|
|
791
|
+
output.append(f"Found {len(results)} results")
|
|
792
|
+
output.append("=" * 80)
|
|
793
|
+
|
|
794
|
+
for i, result in enumerate(results, 1):
|
|
795
|
+
output.append(
|
|
796
|
+
f"\nResult {i}: {result['file_path']}:{result['line_number']}"
|
|
797
|
+
)
|
|
798
|
+
output.append(f"Similarity Score: {result['similarity_score']:.3f}")
|
|
799
|
+
output.append(f"File Type: {result['file_type']}")
|
|
800
|
+
|
|
801
|
+
if result.get("match_type") == "function_definition":
|
|
802
|
+
output.append(f"Function: {result.get('function_name', 'unknown')}")
|
|
803
|
+
|
|
804
|
+
output.append("Context:")
|
|
805
|
+
for context_line in result["context"]:
|
|
806
|
+
marker = ">>>" if context_line["is_match"] else " "
|
|
807
|
+
output.append(
|
|
808
|
+
f"{marker} {context_line['line_number']:4d}: {context_line['content']}"
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
output.append("-" * 80)
|
|
812
|
+
|
|
813
|
+
return "\n".join(output)
|
|
814
|
+
|
|
815
|
+
except Exception as e:
|
|
816
|
+
raise ToolExecutionError(f"Error performing semantic search: {str(e)}")
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def search_directory_for_term(search_term: str, directory: str = "."):
|
|
820
|
+
"""Wrapper function for search_dir functionality"""
|
|
821
|
+
import os
|
|
822
|
+
|
|
823
|
+
if not os.path.isdir(directory):
|
|
824
|
+
raise ToolExecutionError(f"Directory {directory} not found")
|
|
825
|
+
|
|
826
|
+
directory = os.path.realpath(directory)
|
|
827
|
+
matches = {}
|
|
828
|
+
num_files_matched = 0
|
|
829
|
+
|
|
830
|
+
for root, dirs, files in os.walk(directory):
|
|
831
|
+
# Exclude hidden directories
|
|
832
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
|
833
|
+
for file in files:
|
|
834
|
+
if file.startswith("."):
|
|
835
|
+
continue # Skip hidden files
|
|
836
|
+
filepath = os.path.join(root, file)
|
|
837
|
+
try:
|
|
838
|
+
with open(filepath, "r", errors="ignore") as f:
|
|
839
|
+
file_matches = 0
|
|
840
|
+
for line_num, line in enumerate(f, 1):
|
|
841
|
+
if search_term in line:
|
|
842
|
+
file_matches += 1
|
|
843
|
+
if file_matches > 0:
|
|
844
|
+
matches[filepath] = file_matches
|
|
845
|
+
num_files_matched += 1
|
|
846
|
+
except (UnicodeDecodeError, PermissionError):
|
|
847
|
+
continue # Skip files that can't be read
|
|
848
|
+
|
|
849
|
+
if not matches:
|
|
850
|
+
return f'No matches found for "{search_term}" in {directory}'
|
|
851
|
+
|
|
852
|
+
num_matches = sum(matches.values())
|
|
853
|
+
|
|
854
|
+
if num_files_matched > 100:
|
|
855
|
+
return f'More than {num_files_matched} files matched for "{search_term}" in {directory}. Please narrow your search.'
|
|
856
|
+
|
|
857
|
+
result = f'Found {num_matches} matches for "{search_term}" in {directory}:\n'
|
|
858
|
+
|
|
859
|
+
for filepath, count in matches.items():
|
|
860
|
+
# Replace leading path with './' for consistency
|
|
861
|
+
relative_path = os.path.relpath(filepath, start=os.getcwd())
|
|
862
|
+
if not relative_path.startswith("./"):
|
|
863
|
+
relative_path = "./" + relative_path
|
|
864
|
+
result += f"{relative_path} ({count} matches)\n"
|
|
865
|
+
|
|
866
|
+
result += f'End of matches for "{search_term}" in {directory}'
|
|
867
|
+
return result
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
class FileEditorInput(BaseModel):
|
|
871
|
+
command: str = Field(
|
|
872
|
+
description="The command to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`."
|
|
873
|
+
)
|
|
874
|
+
path: str = Field(
|
|
875
|
+
description="Absolute path to file or directory, e.g. `/testbed/file.py` or `/testbed`."
|
|
876
|
+
)
|
|
877
|
+
file_text: Optional[str] = Field(
|
|
878
|
+
default=None,
|
|
879
|
+
description="Required for the `create` command, contains the content of the file to be created.",
|
|
880
|
+
)
|
|
881
|
+
old_str: Optional[str] = Field(
|
|
882
|
+
default=None,
|
|
883
|
+
description="Required for the `str_replace` command, specifies the string in `path` to replace.",
|
|
884
|
+
)
|
|
885
|
+
new_str: Optional[str] = Field(
|
|
886
|
+
default=None,
|
|
887
|
+
description="Optional for the `str_replace` command to specify the replacement string. Required for the `insert` command to specify the string to insert.",
|
|
888
|
+
)
|
|
889
|
+
insert_line: Optional[int] = Field(
|
|
890
|
+
default=None,
|
|
891
|
+
description="Required for the `insert` command. The `new_str` will be inserted AFTER the line specified.",
|
|
892
|
+
)
|
|
893
|
+
view_range: Optional[List[int]] = Field(
|
|
894
|
+
default=None,
|
|
895
|
+
description="Optional for the `view` command when `path` points to a file. Specifies the line range to view. E.g., [11, 12] shows lines 11 and 12. Indexing starts at 1. Use [start_line, -1] to show all lines from `start_line` to the end.",
|
|
896
|
+
)
|
|
897
|
+
enable_linting: bool = Field(
|
|
898
|
+
default=False, description="Enable Python linting checks before saving changes"
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
class SearchInput(BaseModel):
|
|
903
|
+
search_term: str = Field(description="The term to search for in files.")
|
|
904
|
+
path: Optional[str] = Field(
|
|
905
|
+
default=".",
|
|
906
|
+
description="The file or directory to search in. Defaults to `.` if not specified.",
|
|
907
|
+
)
|
|
908
|
+
python_only: bool = Field(default=False, description="Only search in Python files")
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
class SearchDirInput(BaseModel):
|
|
912
|
+
# Keep existing descriptions as there's no direct match in descriptions.py
|
|
913
|
+
search_term: str = Field(description="The term to search for")
|
|
914
|
+
directory: Optional[str] = Field(
|
|
915
|
+
default=".", description="The directory to search in"
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
class FinishInput(BaseModel):
|
|
920
|
+
result: str = Field(
|
|
921
|
+
default="",
|
|
922
|
+
description="Optional. The result text to submit. Defaults to an empty string if not provided.",
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
class FileEditorTool(BaseTool):
|
|
927
|
+
"""📝 Advanced file editor with view, create, edit, and undo capabilities."""
|
|
928
|
+
|
|
929
|
+
name: str = "file_editor"
|
|
930
|
+
description: str = _FILE_EDITOR_DESCRIPTION
|
|
931
|
+
|
|
932
|
+
args_schema: Type[BaseModel] = FileEditorInput
|
|
933
|
+
|
|
934
|
+
def __init__(self, **kwargs):
|
|
935
|
+
super().__init__(**kwargs)
|
|
936
|
+
|
|
937
|
+
def _run(
|
|
938
|
+
self,
|
|
939
|
+
command: str,
|
|
940
|
+
path: str,
|
|
941
|
+
file_text: Optional[str] = None,
|
|
942
|
+
old_str: Optional[str] = None,
|
|
943
|
+
new_str: Optional[str] = None,
|
|
944
|
+
insert_line: Optional[int] = None,
|
|
945
|
+
view_range: Optional[List[int]] = None,
|
|
946
|
+
enable_linting: bool = False,
|
|
947
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
948
|
+
) -> str:
|
|
949
|
+
try:
|
|
950
|
+
# Load edit history and create editor instance
|
|
951
|
+
history = load_history()
|
|
952
|
+
editor = StrReplaceEditor(history, enable_linting)
|
|
953
|
+
|
|
954
|
+
# Capture stdout to get the result
|
|
955
|
+
old_stdout = sys.stdout
|
|
956
|
+
sys.stdout = captured_output = io.StringIO()
|
|
957
|
+
|
|
958
|
+
try:
|
|
959
|
+
# Run the editor command
|
|
960
|
+
result = editor.run(
|
|
961
|
+
command=command,
|
|
962
|
+
path_str=path,
|
|
963
|
+
file_text=file_text,
|
|
964
|
+
view_range=view_range,
|
|
965
|
+
old_str=old_str,
|
|
966
|
+
new_str=new_str,
|
|
967
|
+
insert_line=insert_line,
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
# Save updated history
|
|
971
|
+
save_history(editor.file_history)
|
|
972
|
+
|
|
973
|
+
# Return the result
|
|
974
|
+
return str(result)
|
|
975
|
+
|
|
976
|
+
finally:
|
|
977
|
+
sys.stdout = old_stdout
|
|
978
|
+
|
|
979
|
+
except Exception as e:
|
|
980
|
+
raise ToolExecutionError(f"Error running file editor: {str(e)}")
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
class SearchTool(BaseTool):
|
|
984
|
+
"""🔍 Search for text within files and directories."""
|
|
985
|
+
|
|
986
|
+
name: str = "search"
|
|
987
|
+
description: str = _SEARCH_DESCRIPTION
|
|
988
|
+
|
|
989
|
+
args_schema: Type[BaseModel] = SearchInput
|
|
990
|
+
|
|
991
|
+
def __init__(self, **kwargs):
|
|
992
|
+
super().__init__(**kwargs)
|
|
993
|
+
|
|
994
|
+
def _run(
|
|
995
|
+
self,
|
|
996
|
+
search_term: str,
|
|
997
|
+
path: str = ".",
|
|
998
|
+
python_only: bool = False,
|
|
999
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
1000
|
+
) -> str:
|
|
1001
|
+
try:
|
|
1002
|
+
# Capture stdout to get the result
|
|
1003
|
+
old_stdout = sys.stdout
|
|
1004
|
+
sys.stdout = captured_output = io.StringIO()
|
|
1005
|
+
|
|
1006
|
+
try:
|
|
1007
|
+
# Check if path is a file or directory
|
|
1008
|
+
if os.path.isfile(path):
|
|
1009
|
+
search_in_file(search_term, path)
|
|
1010
|
+
else:
|
|
1011
|
+
search_in_directory(search_term, path, python_only)
|
|
1012
|
+
|
|
1013
|
+
# Get the captured output
|
|
1014
|
+
result = captured_output.getvalue()
|
|
1015
|
+
return result
|
|
1016
|
+
|
|
1017
|
+
finally:
|
|
1018
|
+
sys.stdout = old_stdout
|
|
1019
|
+
|
|
1020
|
+
except Exception as e:
|
|
1021
|
+
raise ToolExecutionError(f"Error running search: {str(e)}")
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
class SearchDirTool(BaseTool):
|
|
1025
|
+
"""📁 Search for text within all files in a directory."""
|
|
1026
|
+
|
|
1027
|
+
name: str = "search_dir"
|
|
1028
|
+
description: str = """Recursively search for text patterns in all files within a directory.
|
|
1029
|
+
|
|
1030
|
+
Features:
|
|
1031
|
+
- Recursive directory traversal
|
|
1032
|
+
- Match counting per file
|
|
1033
|
+
- Excludes hidden files and directories
|
|
1034
|
+
- Performance optimized for large directories
|
|
1035
|
+
|
|
1036
|
+
Best for:
|
|
1037
|
+
- Codebase-wide searches
|
|
1038
|
+
- Finding all occurrences of a pattern
|
|
1039
|
+
- Project-wide refactoring preparation
|
|
1040
|
+
- Code analysis and auditing"""
|
|
1041
|
+
|
|
1042
|
+
args_schema: Type[BaseModel] = SearchDirInput
|
|
1043
|
+
|
|
1044
|
+
def __init__(self, **kwargs):
|
|
1045
|
+
super().__init__(**kwargs)
|
|
1046
|
+
|
|
1047
|
+
def _run(
|
|
1048
|
+
self,
|
|
1049
|
+
search_term: str,
|
|
1050
|
+
directory: str = ".",
|
|
1051
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
1052
|
+
) -> str:
|
|
1053
|
+
try:
|
|
1054
|
+
return search_directory_for_term(search_term, directory)
|
|
1055
|
+
except Exception as e:
|
|
1056
|
+
raise ToolExecutionError(f"Error running search_dir: {str(e)}")
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
class BashInput(BaseModel):
|
|
1060
|
+
command: str = Field(
|
|
1061
|
+
description="The command (and optional arguments) to execute. For example: 'python my_script.py'"
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
class BashTool(BaseTool):
|
|
1066
|
+
"""⚡ Execute bash commands. UNSANDBOXED: only register on providers that
|
|
1067
|
+
are themselves isolated (container/VM). Commands are killed after the
|
|
1068
|
+
configured timeout; there is no command filtering."""
|
|
1069
|
+
|
|
1070
|
+
name: str = "execute_bash"
|
|
1071
|
+
description: str = _BASH_DESCRIPTION.format(PWD=os.getcwd())
|
|
1072
|
+
args_schema: Type[BaseModel] = BashInput
|
|
1073
|
+
|
|
1074
|
+
def __init__(self, **kwargs):
|
|
1075
|
+
super().__init__(**kwargs)
|
|
1076
|
+
|
|
1077
|
+
def _run(
|
|
1078
|
+
self,
|
|
1079
|
+
command: str,
|
|
1080
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
1081
|
+
) -> str:
|
|
1082
|
+
try:
|
|
1083
|
+
# Run the command using the imported function (hard timeout and
|
|
1084
|
+
# optional workspace cwd are applied inside run_command).
|
|
1085
|
+
result = run_command(command)
|
|
1086
|
+
|
|
1087
|
+
if result.returncode != 0:
|
|
1088
|
+
# A nonzero exit is a tool failure: raise so the provider
|
|
1089
|
+
# submits a rejection and the durable record says FAILED,
|
|
1090
|
+
# keeping the captured output in the message for the model.
|
|
1091
|
+
output = "Error executing command:\n"
|
|
1092
|
+
output += "[STDOUT]\n"
|
|
1093
|
+
output += result.stdout.strip() + "\n"
|
|
1094
|
+
output += "[STDERR]\n"
|
|
1095
|
+
output += result.stderr.strip()
|
|
1096
|
+
raise ToolExecutionError(output)
|
|
1097
|
+
|
|
1098
|
+
output = "[STDOUT]\n"
|
|
1099
|
+
output += result.stdout.strip() + "\n"
|
|
1100
|
+
output += "[STDERR]\n"
|
|
1101
|
+
output += result.stderr.strip()
|
|
1102
|
+
return output
|
|
1103
|
+
|
|
1104
|
+
except ToolExecutionError:
|
|
1105
|
+
raise
|
|
1106
|
+
except Exception as e:
|
|
1107
|
+
raise ToolExecutionError(f"Error running bash command: {e}")
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
class FinishTool(BaseTool):
|
|
1111
|
+
"""✅ Submit results and finish tasks."""
|
|
1112
|
+
|
|
1113
|
+
name: str = "finish"
|
|
1114
|
+
description: str = _FINISH_DESCRIPTION
|
|
1115
|
+
|
|
1116
|
+
args_schema: Type[BaseModel] = FinishInput
|
|
1117
|
+
|
|
1118
|
+
def __init__(self, **kwargs):
|
|
1119
|
+
super().__init__(**kwargs)
|
|
1120
|
+
|
|
1121
|
+
def _run(
|
|
1122
|
+
self,
|
|
1123
|
+
result: str = "",
|
|
1124
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
1125
|
+
) -> str:
|
|
1126
|
+
try:
|
|
1127
|
+
# Capture stdout to get the result
|
|
1128
|
+
old_stdout = sys.stdout
|
|
1129
|
+
sys.stdout = captured_output = io.StringIO()
|
|
1130
|
+
|
|
1131
|
+
try:
|
|
1132
|
+
# Call the finish submit function
|
|
1133
|
+
finish_submit(result)
|
|
1134
|
+
|
|
1135
|
+
# Get the captured output
|
|
1136
|
+
output = captured_output.getvalue()
|
|
1137
|
+
return output
|
|
1138
|
+
|
|
1139
|
+
finally:
|
|
1140
|
+
sys.stdout = old_stdout
|
|
1141
|
+
|
|
1142
|
+
except Exception as e:
|
|
1143
|
+
raise ToolExecutionError(f"Error running finish: {str(e)}")
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
class SubmitTool(BaseTool):
|
|
1147
|
+
"""🎯 Simple task submission tool."""
|
|
1148
|
+
|
|
1149
|
+
name: str = "submit"
|
|
1150
|
+
description: str = _SUBMIT_DESCRIPTION
|
|
1151
|
+
|
|
1152
|
+
args_schema: Type[BaseModel] = BaseModel
|
|
1153
|
+
|
|
1154
|
+
def __init__(self, **kwargs):
|
|
1155
|
+
super().__init__(**kwargs)
|
|
1156
|
+
|
|
1157
|
+
def _run(
|
|
1158
|
+
self,
|
|
1159
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
1160
|
+
) -> str:
|
|
1161
|
+
try:
|
|
1162
|
+
# Capture stdout to get the result
|
|
1163
|
+
old_stdout = sys.stdout
|
|
1164
|
+
sys.stdout = captured_output = io.StringIO()
|
|
1165
|
+
|
|
1166
|
+
try:
|
|
1167
|
+
# Call the simple submit function
|
|
1168
|
+
simple_submit()
|
|
1169
|
+
|
|
1170
|
+
# Get the captured output
|
|
1171
|
+
output = captured_output.getvalue()
|
|
1172
|
+
return output
|
|
1173
|
+
|
|
1174
|
+
finally:
|
|
1175
|
+
sys.stdout = old_stdout
|
|
1176
|
+
|
|
1177
|
+
except Exception as e:
|
|
1178
|
+
raise ToolExecutionError(f"Error running submit: {str(e)}")
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def get_swe_toolkit() -> List[BaseTool]:
|
|
1182
|
+
"""Get all SWE (Software Engineering) tools with proper initialization.
|
|
1183
|
+
|
|
1184
|
+
Args:
|
|
1185
|
+
tools_dir: Path to the tools directory. If None, uses the current file's directory.
|
|
1186
|
+
Note: This parameter is maintained for compatibility but is no longer used
|
|
1187
|
+
since tools now import functions directly.
|
|
1188
|
+
|
|
1189
|
+
Returns:
|
|
1190
|
+
List of initialized SWE tools ready for LangChain integration.
|
|
1191
|
+
"""
|
|
1192
|
+
|
|
1193
|
+
return [
|
|
1194
|
+
# FileEditorTool(),
|
|
1195
|
+
# SearchTool(),
|
|
1196
|
+
# SearchDirTool(),
|
|
1197
|
+
BashTool(),
|
|
1198
|
+
CreateDirectoryTool(),
|
|
1199
|
+
WriteFileTool(),
|
|
1200
|
+
FileSearchTool(),
|
|
1201
|
+
GrepSearchTool(),
|
|
1202
|
+
# ListDirTool(),
|
|
1203
|
+
ReadFileTool(),
|
|
1204
|
+
ReplaceStringTool(),
|
|
1205
|
+
SemanticSearchTool(),
|
|
1206
|
+
]
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def get_file_editor_only(tools_dir: Optional[str] = None) -> FileEditorTool:
|
|
1210
|
+
"""Get only the file editor tool for focused file operations."""
|
|
1211
|
+
if tools_dir is None:
|
|
1212
|
+
tools_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1213
|
+
return FileEditorTool(tools_dir=tools_dir)
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def get_search_tools_only(tools_dir: Optional[str] = None) -> List[BaseTool]:
|
|
1217
|
+
"""Get only the search-related tools."""
|
|
1218
|
+
if tools_dir is None:
|
|
1219
|
+
tools_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1220
|
+
return [
|
|
1221
|
+
SearchTool(tools_dir=tools_dir),
|
|
1222
|
+
SearchDirTool(tools_dir=tools_dir),
|
|
1223
|
+
]
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
# Tool selection guide for LLMs
|
|
1227
|
+
SWE_TOOL_SELECTION_GUIDE = """
|
|
1228
|
+
🛠️ SWE TOOLKIT SELECTION GUIDE FOR AI AGENTS:
|
|
1229
|
+
|
|
1230
|
+
1. **file_editor** 📝
|
|
1231
|
+
- Default choice for all file operations
|
|
1232
|
+
- View, create, edit, and undo file changes
|
|
1233
|
+
- Supports Python linting and syntax checking
|
|
1234
|
+
- Persistent edit history
|
|
1235
|
+
|
|
1236
|
+
2. **search** 🔍
|
|
1237
|
+
- Search within specific files or directories
|
|
1238
|
+
- Line number reporting for matches
|
|
1239
|
+
- Cross-platform grep/Python fallback
|
|
1240
|
+
- Use when you need to find specific patterns
|
|
1241
|
+
|
|
1242
|
+
3. **search_dir** 📁
|
|
1243
|
+
- Recursive directory-wide searches
|
|
1244
|
+
- Match counting per file
|
|
1245
|
+
- Use for codebase-wide pattern finding
|
|
1246
|
+
- Great for refactoring preparation
|
|
1247
|
+
|
|
1248
|
+
4. **execute_bash** ⚡
|
|
1249
|
+
- Run system commands and scripts
|
|
1250
|
+
- Security restrictions for safety
|
|
1251
|
+
- Cross-platform compatibility
|
|
1252
|
+
- NOT for interactive or long-running commands
|
|
1253
|
+
|
|
1254
|
+
5. **finish** ✅
|
|
1255
|
+
- Submit results and complete tasks
|
|
1256
|
+
- Optional result text submission
|
|
1257
|
+
- Use when task objectives are met
|
|
1258
|
+
|
|
1259
|
+
6. **submit** 🎯
|
|
1260
|
+
- Simple task completion signal
|
|
1261
|
+
- No parameters needed
|
|
1262
|
+
- Quick completion indicator
|
|
1263
|
+
|
|
1264
|
+
⚠️ IMPORTANT WORKFLOW:
|
|
1265
|
+
1. Start with file_editor to view project structure
|
|
1266
|
+
2. Use search/search_dir to find relevant code
|
|
1267
|
+
3. Use file_editor to make changes
|
|
1268
|
+
4. Use execute_bash to test changes
|
|
1269
|
+
5. Use finish/submit to complete tasks
|
|
1270
|
+
|
|
1271
|
+
💡 BEST PRACTICES:
|
|
1272
|
+
- Always view files before editing
|
|
1273
|
+
- Use search to understand codebase first
|
|
1274
|
+
- Test changes with execute_bash
|
|
1275
|
+
- Use specific, unique strings for str_replace
|
|
1276
|
+
- Include context in old_str for uniqueness
|
|
1277
|
+
"""
|
|
1278
|
+
|
|
1279
|
+
# Export all tools and utilities
|
|
1280
|
+
__all__ = [
|
|
1281
|
+
"FileEditorInput",
|
|
1282
|
+
"SearchInput",
|
|
1283
|
+
"SearchDirInput",
|
|
1284
|
+
"BashInput",
|
|
1285
|
+
"FinishInput",
|
|
1286
|
+
"FileEditorTool",
|
|
1287
|
+
"SearchTool",
|
|
1288
|
+
"SearchDirTool",
|
|
1289
|
+
"BashTool",
|
|
1290
|
+
"FinishTool",
|
|
1291
|
+
"SubmitTool",
|
|
1292
|
+
"get_swe_toolkit",
|
|
1293
|
+
"get_file_editor_only",
|
|
1294
|
+
"get_search_tools_only",
|
|
1295
|
+
"SWE_TOOL_SELECTION_GUIDE",
|
|
1296
|
+
]
|