agentcrew-ai 0.8.12__py3-none-any.whl → 0.9.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.
- AgentCrew/__init__.py +1 -1
- AgentCrew/app.py +34 -633
- AgentCrew/main.py +55 -3
- AgentCrew/main_docker.py +1 -30
- AgentCrew/modules/agents/local_agent.py +26 -1
- AgentCrew/modules/chat/message/command_processor.py +33 -8
- AgentCrew/modules/chat/message/handler.py +5 -1
- AgentCrew/modules/code_analysis/__init__.py +8 -0
- AgentCrew/modules/code_analysis/parsers/__init__.py +67 -0
- AgentCrew/modules/code_analysis/parsers/base.py +93 -0
- AgentCrew/modules/code_analysis/parsers/cpp_parser.py +127 -0
- AgentCrew/modules/code_analysis/parsers/csharp_parser.py +162 -0
- AgentCrew/modules/code_analysis/parsers/generic_parser.py +63 -0
- AgentCrew/modules/code_analysis/parsers/go_parser.py +154 -0
- AgentCrew/modules/code_analysis/parsers/java_parser.py +103 -0
- AgentCrew/modules/code_analysis/parsers/javascript_parser.py +268 -0
- AgentCrew/modules/code_analysis/parsers/kotlin_parser.py +84 -0
- AgentCrew/modules/code_analysis/parsers/php_parser.py +107 -0
- AgentCrew/modules/code_analysis/parsers/python_parser.py +60 -0
- AgentCrew/modules/code_analysis/parsers/ruby_parser.py +46 -0
- AgentCrew/modules/code_analysis/parsers/rust_parser.py +72 -0
- AgentCrew/modules/code_analysis/service.py +231 -897
- AgentCrew/modules/command_execution/constants.py +2 -2
- AgentCrew/modules/console/completers.py +1 -1
- AgentCrew/modules/console/confirmation_handler.py +4 -4
- AgentCrew/modules/console/console_ui.py +17 -3
- AgentCrew/modules/console/conversation_browser/__init__.py +9 -0
- AgentCrew/modules/console/conversation_browser/browser.py +84 -0
- AgentCrew/modules/console/conversation_browser/browser_input_handler.py +279 -0
- AgentCrew/modules/console/conversation_browser/browser_ui.py +643 -0
- AgentCrew/modules/console/conversation_handler.py +34 -1
- AgentCrew/modules/console/diff_display.py +22 -51
- AgentCrew/modules/console/display_handlers.py +142 -26
- AgentCrew/modules/console/tool_display.py +4 -6
- AgentCrew/modules/file_editing/service.py +8 -8
- AgentCrew/modules/file_editing/tool.py +65 -67
- AgentCrew/modules/gui/components/command_handler.py +137 -29
- AgentCrew/modules/gui/components/tool_handlers.py +0 -2
- AgentCrew/modules/gui/themes/README.md +30 -14
- AgentCrew/modules/gui/themes/__init__.py +2 -1
- AgentCrew/modules/gui/themes/atom_light.yaml +1287 -0
- AgentCrew/modules/gui/themes/catppuccin.yaml +1276 -0
- AgentCrew/modules/gui/themes/dracula.yaml +1262 -0
- AgentCrew/modules/gui/themes/nord.yaml +1267 -0
- AgentCrew/modules/gui/themes/saigontech.yaml +1268 -0
- AgentCrew/modules/gui/themes/style_provider.py +76 -264
- AgentCrew/modules/gui/themes/theme_loader.py +379 -0
- AgentCrew/modules/gui/themes/unicorn.yaml +1276 -0
- AgentCrew/modules/gui/widgets/configs/global_settings.py +3 -4
- AgentCrew/modules/gui/widgets/diff_widget.py +30 -61
- AgentCrew/modules/llm/constants.py +18 -9
- AgentCrew/modules/memory/context_persistent.py +1 -0
- AgentCrew/modules/memory/tool.py +1 -1
- AgentCrew/setup.py +470 -0
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/METADATA +1 -1
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/RECORD +60 -41
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/WHEEL +1 -1
- AgentCrew/modules/gui/themes/atom_light.py +0 -1365
- AgentCrew/modules/gui/themes/catppuccin.py +0 -1404
- AgentCrew/modules/gui/themes/dracula.py +0 -1372
- AgentCrew/modules/gui/themes/nord.py +0 -1365
- AgentCrew/modules/gui/themes/saigontech.py +0 -1359
- AgentCrew/modules/gui/themes/unicorn.py +0 -1372
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/entry_points.txt +0 -0
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -4,6 +4,7 @@ Provides split view diff display with syntax highlighting.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import difflib
|
|
7
|
+
from typing import List, Dict
|
|
7
8
|
from rich.text import Text
|
|
8
9
|
from rich.table import Table
|
|
9
10
|
from rich.box import SIMPLE_HEAD
|
|
@@ -16,67 +17,37 @@ class DiffDisplay:
|
|
|
16
17
|
"""Helper class for creating split diff views."""
|
|
17
18
|
|
|
18
19
|
@staticmethod
|
|
19
|
-
def has_search_replace_blocks(
|
|
20
|
-
"""Check if
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
def has_search_replace_blocks(blocks: List[Dict]) -> bool:
|
|
21
|
+
"""Check if input is a valid list of search/replace blocks."""
|
|
22
|
+
if not isinstance(blocks, list):
|
|
23
|
+
return False
|
|
24
|
+
return len(blocks) > 0 and all(
|
|
25
|
+
isinstance(b, dict) and "search" in b and "replace" in b for b in blocks
|
|
23
26
|
)
|
|
24
27
|
|
|
25
28
|
@staticmethod
|
|
26
|
-
def parse_search_replace_blocks(
|
|
29
|
+
def parse_search_replace_blocks(blocks: List[Dict]) -> List[Dict]:
|
|
27
30
|
"""
|
|
28
|
-
Parse search/replace blocks from
|
|
31
|
+
Parse search/replace blocks from list format.
|
|
29
32
|
|
|
30
33
|
Args:
|
|
31
|
-
|
|
34
|
+
blocks: List of dicts with 'search' and 'replace' keys
|
|
32
35
|
|
|
33
36
|
Returns:
|
|
34
37
|
List of dicts with 'index', 'search', and 'replace' keys
|
|
35
38
|
"""
|
|
36
|
-
blocks
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
i += 1
|
|
49
|
-
|
|
50
|
-
if i >= len(lines):
|
|
51
|
-
break
|
|
52
|
-
|
|
53
|
-
i += 1
|
|
54
|
-
replace_lines = []
|
|
55
|
-
|
|
56
|
-
while (
|
|
57
|
-
i < len(lines)
|
|
58
|
-
and lines[i].strip() != ">>>>>>> REPLACE"
|
|
59
|
-
and lines[i].strip() != "======="
|
|
60
|
-
):
|
|
61
|
-
replace_lines.append(lines[i])
|
|
62
|
-
i += 1
|
|
63
|
-
|
|
64
|
-
if i >= len(lines):
|
|
65
|
-
break
|
|
66
|
-
|
|
67
|
-
blocks.append(
|
|
68
|
-
{
|
|
69
|
-
"index": block_index,
|
|
70
|
-
"search": "\n".join(search_lines),
|
|
71
|
-
"replace": "\n".join(replace_lines),
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
block_index += 1
|
|
75
|
-
i += 1
|
|
76
|
-
else:
|
|
77
|
-
i += 1
|
|
78
|
-
|
|
79
|
-
return blocks
|
|
39
|
+
if not isinstance(blocks, list):
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
"index": i,
|
|
45
|
+
"search": block.get("search", ""),
|
|
46
|
+
"replace": block.get("replace", ""),
|
|
47
|
+
}
|
|
48
|
+
for i, block in enumerate(blocks)
|
|
49
|
+
if isinstance(block, dict)
|
|
50
|
+
]
|
|
80
51
|
|
|
81
52
|
@staticmethod
|
|
82
53
|
def create_split_diff_table(
|
|
@@ -122,12 +122,125 @@ class DisplayHandlers:
|
|
|
122
122
|
)
|
|
123
123
|
|
|
124
124
|
def display_debug_info(self, debug_info):
|
|
125
|
-
"""Display debug information.
|
|
126
|
-
|
|
125
|
+
"""Display debug information with formatting and truncation.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
debug_info: Either a dict with 'type' and 'messages' keys (new format)
|
|
129
|
+
or a raw list of messages (legacy format)
|
|
130
|
+
"""
|
|
131
|
+
if (
|
|
132
|
+
isinstance(debug_info, dict)
|
|
133
|
+
and "type" in debug_info
|
|
134
|
+
and "messages" in debug_info
|
|
135
|
+
):
|
|
136
|
+
# New format with type and messages
|
|
137
|
+
msg_type = debug_info["type"]
|
|
138
|
+
messages = debug_info["messages"]
|
|
139
|
+
title = "Agent Messages" if msg_type == "agent" else "Chat Messages"
|
|
140
|
+
else:
|
|
141
|
+
# Legacy format - just raw messages
|
|
142
|
+
title = "Messages"
|
|
143
|
+
messages = debug_info
|
|
144
|
+
|
|
145
|
+
self.console.print(
|
|
146
|
+
Text(f"\n{title} ({len(messages)} messages):", style=RICH_STYLE_YELLOW)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
formatted = self._format_messages_for_debug(messages)
|
|
127
150
|
try:
|
|
128
|
-
self.console.print(json.dumps(
|
|
151
|
+
self.console.print(json.dumps(formatted, indent=2))
|
|
129
152
|
except Exception:
|
|
130
|
-
self.console.print(
|
|
153
|
+
self.console.print(str(formatted))
|
|
154
|
+
|
|
155
|
+
def _format_messages_for_debug(
|
|
156
|
+
self, messages, max_content_length: int = 200
|
|
157
|
+
) -> list:
|
|
158
|
+
"""Format messages for debug display with truncated content.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
messages: List of message dictionaries
|
|
162
|
+
max_content_length: Maximum length for message content
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of formatted message dictionaries
|
|
166
|
+
"""
|
|
167
|
+
formatted = []
|
|
168
|
+
|
|
169
|
+
for i, msg in enumerate(messages):
|
|
170
|
+
formatted_msg = {"#": i, "content": ""}
|
|
171
|
+
|
|
172
|
+
# Copy basic fields
|
|
173
|
+
if "role" in msg:
|
|
174
|
+
formatted_msg["role"] = msg["role"]
|
|
175
|
+
if "agent" in msg:
|
|
176
|
+
formatted_msg["agent"] = msg["agent"]
|
|
177
|
+
|
|
178
|
+
# Truncate content
|
|
179
|
+
content = msg.get("content", "")
|
|
180
|
+
formatted_msg["content"] = self._truncate_content(
|
|
181
|
+
content, max_content_length
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Include tool_use/tool_result indicators if present
|
|
185
|
+
if isinstance(content, list):
|
|
186
|
+
content_types = []
|
|
187
|
+
for item in content:
|
|
188
|
+
if isinstance(item, dict):
|
|
189
|
+
item_type = item.get("type", "unknown")
|
|
190
|
+
if item_type == "tool_use":
|
|
191
|
+
tool_name = item.get("name", "unknown")
|
|
192
|
+
content_types.append(f"tool_use:{tool_name}")
|
|
193
|
+
elif item_type == "tool_result":
|
|
194
|
+
content_types.append("tool_result")
|
|
195
|
+
elif item_type not in ("text",):
|
|
196
|
+
content_types.append(item_type)
|
|
197
|
+
if content_types:
|
|
198
|
+
formatted_msg["content_types"] = content_types
|
|
199
|
+
|
|
200
|
+
formatted.append(formatted_msg)
|
|
201
|
+
|
|
202
|
+
return formatted
|
|
203
|
+
|
|
204
|
+
def _truncate_content(self, content, max_length: int = 200) -> str:
|
|
205
|
+
"""Truncate content to max_length with ellipsis.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
content: Message content (can be string, list, or dict)
|
|
209
|
+
max_length: Maximum length for the output
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Truncated string representation
|
|
213
|
+
"""
|
|
214
|
+
if isinstance(content, str):
|
|
215
|
+
text = content
|
|
216
|
+
elif isinstance(content, list):
|
|
217
|
+
# Extract text from content blocks
|
|
218
|
+
text_parts = []
|
|
219
|
+
for item in content:
|
|
220
|
+
if isinstance(item, dict):
|
|
221
|
+
if item.get("type") == "text":
|
|
222
|
+
text_parts.append(item.get("text", ""))
|
|
223
|
+
elif item.get("type") == "tool_use":
|
|
224
|
+
text_parts.append(f"[tool:{item.get('name', 'unknown')}]")
|
|
225
|
+
elif item.get("type") == "tool_result":
|
|
226
|
+
result = item.get("content", "")
|
|
227
|
+
if isinstance(result, str):
|
|
228
|
+
text_parts.append(f"[result:{result[:50]}...]")
|
|
229
|
+
else:
|
|
230
|
+
text_parts.append("[result:...]")
|
|
231
|
+
elif isinstance(item, str):
|
|
232
|
+
text_parts.append(item)
|
|
233
|
+
text = " | ".join(text_parts)
|
|
234
|
+
else:
|
|
235
|
+
text = str(content)
|
|
236
|
+
|
|
237
|
+
# Clean up whitespace
|
|
238
|
+
text = " ".join(text.split())
|
|
239
|
+
|
|
240
|
+
if len(text) <= max_length:
|
|
241
|
+
return text
|
|
242
|
+
|
|
243
|
+
return text[: max_length - 3] + "..."
|
|
131
244
|
|
|
132
245
|
def display_models(self, models_by_provider: Dict):
|
|
133
246
|
"""Display available models grouped by provider."""
|
|
@@ -157,34 +270,37 @@ class DisplayHandlers:
|
|
|
157
270
|
f" - {agent_name}{current}: {agent_data['description']}"
|
|
158
271
|
)
|
|
159
272
|
|
|
160
|
-
def display_conversations(
|
|
161
|
-
|
|
273
|
+
def display_conversations(
|
|
274
|
+
self,
|
|
275
|
+
conversations: List[Dict[str, Any]],
|
|
276
|
+
get_history_callback=None,
|
|
277
|
+
delete_callback=None,
|
|
278
|
+
):
|
|
279
|
+
"""Display available conversations using interactive browser.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
conversations: List of conversation metadata
|
|
283
|
+
get_history_callback: Optional callback to fetch full conversation history
|
|
284
|
+
delete_callback: Optional callback to delete conversations by IDs
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Selected conversation ID or None if cancelled
|
|
288
|
+
"""
|
|
162
289
|
if not conversations:
|
|
163
290
|
self.console.print(
|
|
164
291
|
Text("No saved conversations found.", style=RICH_STYLE_YELLOW)
|
|
165
292
|
)
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
self.console.print(Text("Available conversations:", style=RICH_STYLE_YELLOW))
|
|
169
|
-
for i, convo in enumerate(conversations[:30], 1):
|
|
170
|
-
# Format timestamp for better readability
|
|
171
|
-
timestamp = convo.get("timestamp", "Unknown")
|
|
172
|
-
if isinstance(timestamp, (int, float)):
|
|
173
|
-
timestamp = datetime.fromtimestamp(timestamp).strftime(
|
|
174
|
-
"%Y-%m-%d %H:%M:%S"
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
title = convo.get("title", "Untitled")
|
|
178
|
-
convo_id = convo.get("id", "unknown")
|
|
293
|
+
return None
|
|
179
294
|
|
|
180
|
-
|
|
181
|
-
self.console.print(f" {i}. {title} [{convo_id}]")
|
|
182
|
-
self.console.print(f" Created: {timestamp}")
|
|
295
|
+
from .conversation_browser.browser import ConversationBrowser
|
|
183
296
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
297
|
+
browser = ConversationBrowser(
|
|
298
|
+
console=self.console,
|
|
299
|
+
get_conversation_history=get_history_callback,
|
|
300
|
+
on_delete=delete_callback,
|
|
301
|
+
)
|
|
302
|
+
browser.set_conversations(conversations)
|
|
303
|
+
return browser.show()
|
|
188
304
|
|
|
189
305
|
def display_consolidation_result(self, result: Dict[str, Any]):
|
|
190
306
|
"""Display information about a consolidation operation."""
|
|
@@ -109,9 +109,7 @@ class ToolDisplayHandlers:
|
|
|
109
109
|
)
|
|
110
110
|
)
|
|
111
111
|
|
|
112
|
-
def _display_write_or_edit_file_use(
|
|
113
|
-
self, tool_use: Dict, file_path: str, blocks_text: str
|
|
114
|
-
):
|
|
112
|
+
def _display_write_or_edit_file_use(self, tool_use: Dict, file_path: str, blocks):
|
|
115
113
|
"""Display write_or_edit_file tool with split diff view."""
|
|
116
114
|
tool_icon = self.get_tool_icon(tool_use["name"])
|
|
117
115
|
|
|
@@ -121,10 +119,10 @@ class ToolDisplayHandlers:
|
|
|
121
119
|
|
|
122
120
|
self.console.print(Panel(header, box=HORIZONTALS, title_align="left"))
|
|
123
121
|
|
|
124
|
-
|
|
122
|
+
parsed_blocks = DiffDisplay.parse_search_replace_blocks(blocks)
|
|
125
123
|
|
|
126
|
-
if
|
|
127
|
-
for block in
|
|
124
|
+
if parsed_blocks:
|
|
125
|
+
for block in parsed_blocks:
|
|
128
126
|
diff_table = DiffDisplay.create_split_diff_table(
|
|
129
127
|
block["search"], block["replace"], max_width=self.console.width - 4
|
|
130
128
|
)
|
|
@@ -41,21 +41,21 @@ class FileEditingService:
|
|
|
41
41
|
def write_or_edit_file(
|
|
42
42
|
self,
|
|
43
43
|
file_path: str,
|
|
44
|
-
percentage_to_change: float,
|
|
45
44
|
text_or_search_replace_blocks: str,
|
|
45
|
+
is_search_replace: bool = False,
|
|
46
46
|
agent_name: Optional[str] = None,
|
|
47
47
|
) -> Dict[str, Any]:
|
|
48
48
|
"""
|
|
49
49
|
Main entry point for file editing.
|
|
50
50
|
|
|
51
51
|
Decision logic:
|
|
52
|
-
-
|
|
53
|
-
-
|
|
52
|
+
- is_search_replace=False: Full file write
|
|
53
|
+
- is_search_replace=True: Search/replace blocks
|
|
54
54
|
|
|
55
55
|
Args:
|
|
56
56
|
file_path: Path to file (absolute or relative, ~ supported)
|
|
57
|
-
percentage_to_change: Percentage of lines changing (0-100)
|
|
58
57
|
text_or_search_replace_blocks: Full content or search/replace blocks
|
|
58
|
+
is_search_replace: True for search/replace mode, False for full content
|
|
59
59
|
agent_name: Optional agent name for permission checks
|
|
60
60
|
|
|
61
61
|
Returns:
|
|
@@ -94,12 +94,12 @@ class FileEditingService:
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
try:
|
|
97
|
-
if
|
|
98
|
-
result = self._write_full_file(file_path, text_or_search_replace_blocks)
|
|
99
|
-
else:
|
|
97
|
+
if is_search_replace:
|
|
100
98
|
result = self._apply_search_replace(
|
|
101
99
|
file_path, text_or_search_replace_blocks
|
|
102
100
|
)
|
|
101
|
+
else:
|
|
102
|
+
result = self._write_full_file(file_path, text_or_search_replace_blocks)
|
|
103
103
|
|
|
104
104
|
if result["status"] != "success":
|
|
105
105
|
return result
|
|
@@ -166,7 +166,7 @@ class FileEditingService:
|
|
|
166
166
|
return {
|
|
167
167
|
"status": "error",
|
|
168
168
|
"error": f"File not found: {file_path}",
|
|
169
|
-
"suggestion": "Use
|
|
169
|
+
"suggestion": "Use full content mode (string) to create new files",
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
try:
|
|
@@ -4,39 +4,42 @@ File editing tool definitions and handlers for AgentCrew.
|
|
|
4
4
|
Provides file_write_or_edit tool for intelligent file editing with search/replace blocks.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Dict, Any, Callable, Optional
|
|
7
|
+
from typing import Dict, Any, Callable, Optional, List
|
|
8
8
|
from .service import FileEditingService
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
def convert_blocks_to_string(blocks: List[Dict[str, str]]) -> str:
|
|
12
|
+
result_parts = []
|
|
13
|
+
for block in blocks:
|
|
14
|
+
search_text = block.get("search", "")
|
|
15
|
+
replace_text = block.get("replace", "")
|
|
16
|
+
block_str = (
|
|
17
|
+
f"<<<<<<< SEARCH\n{search_text}\n=======\n{replace_text}\n>>>>>>> REPLACE"
|
|
18
|
+
)
|
|
19
|
+
result_parts.append(block_str)
|
|
20
|
+
return "\n".join(result_parts)
|
|
21
|
+
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
def is_full_content_mode(blocks: List[Dict[str, str]]) -> bool:
|
|
24
|
+
if len(blocks) == 1:
|
|
25
|
+
block = blocks[0]
|
|
26
|
+
search_text = block.get("search", "")
|
|
27
|
+
return search_text == ""
|
|
28
|
+
return False
|
|
17
29
|
|
|
18
|
-
Returns:
|
|
19
|
-
Provider-specific tool definition
|
|
20
|
-
"""
|
|
21
|
-
tool_description = """Write/edit files via search/replace blocks or full content.
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
def get_file_write_or_edit_tool_definition(provider="claude") -> Dict[str, Any]:
|
|
32
|
+
tool_description = """Write/edit files via search/replace blocks.
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[replacement content]
|
|
30
|
-
>>>>>>> REPLACE
|
|
34
|
+
FORMAT: Array of {"search": "...", "replace": "..."} objects
|
|
35
|
+
- Empty search + replace with content = write full file content
|
|
36
|
+
- Non-empty search + replace = search/replace operation
|
|
37
|
+
- Non-empty search + empty replace = delete matched content
|
|
31
38
|
|
|
32
39
|
RULES:
|
|
33
40
|
1. SEARCH must match exactly (character-perfect)
|
|
34
41
|
2. Include changing lines +0-3 context
|
|
35
|
-
3.
|
|
36
|
-
4. Preserve whitespace/indentation
|
|
37
|
-
5. Empty REPLACE = delete
|
|
38
|
-
|
|
39
|
-
EXAMPLES: Add import (existing+new) | Delete (full→empty) | Modify (signature+changes)
|
|
42
|
+
3. Preserve whitespace/indentation
|
|
40
43
|
|
|
41
44
|
Auto syntax check (30+ langs) with rollback on error
|
|
42
45
|
"""
|
|
@@ -46,19 +49,28 @@ Auto syntax check (30+ langs) with rollback on error
|
|
|
46
49
|
"type": "string",
|
|
47
50
|
"description": "Path (absolute/relative). Use ~ for home. Ex: './src/main.py'",
|
|
48
51
|
},
|
|
49
|
-
"percentage_to_change": {
|
|
50
|
-
"type": "number",
|
|
51
|
-
"description": "% lines changing (0-100). >50=full, ≤50=blocks",
|
|
52
|
-
},
|
|
53
52
|
"text_or_search_replace_blocks": {
|
|
54
|
-
"type": "
|
|
55
|
-
"
|
|
53
|
+
"type": "array",
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"search": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Exact content to find. Empty string means full file write mode.",
|
|
60
|
+
},
|
|
61
|
+
"replace": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Replacement content (empty string to delete)",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
"required": ["search", "replace"],
|
|
67
|
+
},
|
|
68
|
+
"description": 'Array of search/replace blocks. For full file write: [{"search": "", "replace": "full content"}]. For edits: [{"search": "exact match", "replace": "replacement"}]',
|
|
56
69
|
},
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
tool_required = [
|
|
60
73
|
"file_path",
|
|
61
|
-
"percentage_to_change",
|
|
62
74
|
"text_or_search_replace_blocks",
|
|
63
75
|
]
|
|
64
76
|
|
|
@@ -72,7 +84,7 @@ Auto syntax check (30+ langs) with rollback on error
|
|
|
72
84
|
"required": tool_required,
|
|
73
85
|
},
|
|
74
86
|
}
|
|
75
|
-
else:
|
|
87
|
+
else:
|
|
76
88
|
return {
|
|
77
89
|
"type": "function",
|
|
78
90
|
"function": {
|
|
@@ -90,44 +102,37 @@ Auto syntax check (30+ langs) with rollback on error
|
|
|
90
102
|
def get_file_write_or_edit_tool_handler(
|
|
91
103
|
file_editing_service: FileEditingService,
|
|
92
104
|
) -> Callable:
|
|
93
|
-
"""
|
|
94
|
-
Get the handler function for the file editing tool.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
file_editing_service: FileEditingService instance
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
Handler function
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
105
|
def handle_file_write_or_edit(**params) -> str:
|
|
104
|
-
"""
|
|
105
|
-
Tool execution handler.
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
**params: Tool parameters (file_path, percentage_to_change, text_or_search_replace_blocks)
|
|
109
|
-
|
|
110
|
-
Returns:
|
|
111
|
-
Success or error message
|
|
112
|
-
"""
|
|
113
106
|
file_path = params.get("file_path")
|
|
114
|
-
|
|
115
|
-
text_or_search_replace_blocks = params.get("text_or_search_replace_blocks")
|
|
107
|
+
blocks = params.get("text_or_search_replace_blocks")
|
|
116
108
|
|
|
117
109
|
if not file_path:
|
|
118
110
|
raise ValueError("Error: No file path provided.")
|
|
119
111
|
|
|
120
|
-
if
|
|
121
|
-
raise ValueError("Error: No
|
|
112
|
+
if blocks is None:
|
|
113
|
+
raise ValueError("Error: No search/replace blocks provided.")
|
|
122
114
|
|
|
123
|
-
if not
|
|
124
|
-
raise ValueError(
|
|
115
|
+
if not isinstance(blocks, list):
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"Error: text_or_search_replace_blocks must be an array of search/replace objects."
|
|
118
|
+
)
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
full_content_mode = is_full_content_mode(blocks)
|
|
121
|
+
|
|
122
|
+
if full_content_mode:
|
|
123
|
+
content = blocks[0].get("replace", "")
|
|
124
|
+
result = file_editing_service.write_or_edit_file(
|
|
125
|
+
file_path=file_path,
|
|
126
|
+
is_search_replace=False,
|
|
127
|
+
text_or_search_replace_blocks=content,
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
blocks_string = convert_blocks_to_string(blocks)
|
|
131
|
+
result = file_editing_service.write_or_edit_file(
|
|
132
|
+
file_path=file_path,
|
|
133
|
+
is_search_replace=True,
|
|
134
|
+
text_or_search_replace_blocks=blocks_string,
|
|
135
|
+
)
|
|
131
136
|
|
|
132
137
|
if result["status"] == "success":
|
|
133
138
|
parts = [f"{result['file_path']}"]
|
|
@@ -171,13 +176,6 @@ def get_file_write_or_edit_tool_handler(
|
|
|
171
176
|
|
|
172
177
|
|
|
173
178
|
def register(service_instance: Optional[FileEditingService] = None, agent=None):
|
|
174
|
-
"""
|
|
175
|
-
Register file editing tools with AgentCrew tool registry.
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
service_instance: Optional FileEditingService instance
|
|
179
|
-
agent: Optional agent to register with directly
|
|
180
|
-
"""
|
|
181
179
|
from AgentCrew.modules.tools.registration import register_tool
|
|
182
180
|
|
|
183
181
|
if service_instance is None:
|