acontext 0.1.5__py3-none-any.whl → 0.1.6__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.
acontext/agent/sandbox.py
CHANGED
|
@@ -283,6 +283,7 @@ class TextEditorTool(BaseTool):
|
|
|
283
283
|
},
|
|
284
284
|
"view_range": {
|
|
285
285
|
"type": ["array", "null"],
|
|
286
|
+
"items": {"type": "integer"},
|
|
286
287
|
"description": "Optional for 'view' command. An array [start_line, end_line] to view specific lines. If not provided, shows the first 200 lines.",
|
|
287
288
|
},
|
|
288
289
|
}
|
acontext/agent/text_editor.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Text editor file operations for sandbox environments."""
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
|
+
import posixpath
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
if TYPE_CHECKING:
|
|
@@ -41,53 +42,53 @@ def view_file(
|
|
|
41
42
|
Returns:
|
|
42
43
|
A dict with file content and metadata, or error information.
|
|
43
44
|
"""
|
|
44
|
-
|
|
45
|
-
check_cmd = f"wc -l < {escape_for_shell(path)} 2>/dev/null || echo 'FILE_NOT_FOUND'"
|
|
46
|
-
result = ctx.client.sandboxes.exec_command(
|
|
47
|
-
sandbox_id=ctx.sandbox_id,
|
|
48
|
-
command=check_cmd,
|
|
49
|
-
timeout=timeout,
|
|
50
|
-
)
|
|
45
|
+
escaped_path = escape_for_shell(path)
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
"error": f"File not found: {path}",
|
|
55
|
-
"stderr": result.stderr,
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
total_lines = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
|
|
59
|
-
|
|
60
|
-
# Build the view command with line numbers
|
|
47
|
+
# Build combined command: check existence, get total lines, and view content in one exec
|
|
61
48
|
if view_range and len(view_range) == 2:
|
|
62
49
|
start_line, end_line = view_range
|
|
63
|
-
|
|
50
|
+
view_cmd = (
|
|
51
|
+
f"sed -n '{start_line},{end_line}p' {escaped_path} | nl -ba -v {start_line}"
|
|
52
|
+
)
|
|
64
53
|
else:
|
|
65
|
-
# Default to first 200 lines if no range specified
|
|
66
54
|
max_lines = 200
|
|
67
|
-
|
|
55
|
+
view_cmd = f"head -n {max_lines} {escaped_path} | nl -ba"
|
|
68
56
|
start_line = 1
|
|
69
57
|
|
|
58
|
+
# Single combined command: outputs "TOTAL:<n>" on first line, then file content
|
|
59
|
+
cmd = (
|
|
60
|
+
f"if [ ! -f {escaped_path} ]; then echo 'FILE_NOT_FOUND'; exit 1; fi; "
|
|
61
|
+
f'echo "TOTAL:$(wc -l < {escaped_path})"; {view_cmd}'
|
|
62
|
+
)
|
|
63
|
+
|
|
70
64
|
result = ctx.client.sandboxes.exec_command(
|
|
71
65
|
sandbox_id=ctx.sandbox_id,
|
|
72
66
|
command=cmd,
|
|
73
67
|
timeout=timeout,
|
|
74
68
|
)
|
|
75
69
|
|
|
76
|
-
if result.exit_code != 0:
|
|
70
|
+
if result.exit_code != 0 or "FILE_NOT_FOUND" in result.stdout:
|
|
77
71
|
return {
|
|
78
|
-
"error": f"
|
|
72
|
+
"error": f"File not found: {path}",
|
|
79
73
|
"stderr": result.stderr,
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
# Parse output: first line is "TOTAL:<n>", rest is content
|
|
77
|
+
lines = result.stdout.split("\n", 1)
|
|
78
|
+
total_lines = 0
|
|
79
|
+
content = ""
|
|
80
|
+
|
|
81
|
+
if lines and lines[0].startswith("TOTAL:"):
|
|
82
|
+
total_str = lines[0][6:].strip()
|
|
83
|
+
total_lines = int(total_str) if total_str.isdigit() else 0
|
|
84
|
+
content = lines[1] if len(lines) > 1 else ""
|
|
85
|
+
|
|
86
|
+
content_lines = content.rstrip("\n").split("\n") if content.strip() else []
|
|
86
87
|
num_lines = len(content_lines)
|
|
87
88
|
|
|
88
89
|
return {
|
|
89
90
|
"file_type": "text",
|
|
90
|
-
"content": truncate_content(
|
|
91
|
+
"content": truncate_content(content),
|
|
91
92
|
"numLines": num_lines,
|
|
92
93
|
"startLine": start_line if view_range else 1,
|
|
93
94
|
"totalLines": total_lines + 1, # wc -l doesn't count last line without newline
|
|
@@ -108,41 +109,35 @@ def create_file(
|
|
|
108
109
|
Returns:
|
|
109
110
|
A dict with creation status or error information.
|
|
110
111
|
"""
|
|
111
|
-
|
|
112
|
-
check_cmd = f"test -f {escape_for_shell(path)} && echo 'EXISTS' || echo 'NEW'"
|
|
113
|
-
check_result = ctx.client.sandboxes.exec_command(
|
|
114
|
-
sandbox_id=ctx.sandbox_id,
|
|
115
|
-
command=check_cmd,
|
|
116
|
-
timeout=timeout,
|
|
117
|
-
)
|
|
118
|
-
is_update = "EXISTS" in check_result.stdout
|
|
119
|
-
|
|
120
|
-
# Create directory if needed
|
|
121
|
-
dir_path = "/".join(path.split("/")[:-1])
|
|
122
|
-
if dir_path:
|
|
123
|
-
mkdir_cmd = f"mkdir -p {escape_for_shell(dir_path)}"
|
|
124
|
-
ctx.client.sandboxes.exec_command(
|
|
125
|
-
sandbox_id=ctx.sandbox_id,
|
|
126
|
-
command=mkdir_cmd,
|
|
127
|
-
timeout=timeout,
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
# Write file using base64 encoding to safely transfer content
|
|
112
|
+
escaped_path = escape_for_shell(path)
|
|
131
113
|
encoded_content = base64.b64encode(file_text.encode()).decode()
|
|
132
|
-
|
|
114
|
+
|
|
115
|
+
# Get directory path for mkdir
|
|
116
|
+
dir_path = posixpath.dirname(path)
|
|
117
|
+
mkdir_part = f"mkdir -p {escape_for_shell(dir_path)} && " if dir_path else ""
|
|
118
|
+
|
|
119
|
+
# Single combined command: check existence, create dir, write file
|
|
120
|
+
cmd = (
|
|
121
|
+
f"is_update=$(test -f {escaped_path} && echo 1 || echo 0); "
|
|
122
|
+
f"{mkdir_part}"
|
|
123
|
+
f"echo {escape_for_shell(encoded_content)} | base64 -d > {escaped_path} && "
|
|
124
|
+
f'echo "STATUS:$is_update"'
|
|
125
|
+
)
|
|
133
126
|
|
|
134
127
|
result = ctx.client.sandboxes.exec_command(
|
|
135
128
|
sandbox_id=ctx.sandbox_id,
|
|
136
|
-
command=
|
|
129
|
+
command=cmd,
|
|
137
130
|
timeout=timeout,
|
|
138
131
|
)
|
|
139
132
|
|
|
140
|
-
if result.exit_code != 0:
|
|
133
|
+
if result.exit_code != 0 or "STATUS:" not in result.stdout:
|
|
141
134
|
return {
|
|
142
135
|
"error": f"Failed to create file: {path}",
|
|
143
136
|
"stderr": result.stderr,
|
|
144
137
|
}
|
|
145
138
|
|
|
139
|
+
is_update = "STATUS:1" in result.stdout
|
|
140
|
+
|
|
146
141
|
return {
|
|
147
142
|
"is_file_update": is_update,
|
|
148
143
|
"message": f"File {'updated' if is_update else 'created'}: {path}",
|
|
@@ -154,6 +149,9 @@ def str_replace(
|
|
|
154
149
|
) -> dict:
|
|
155
150
|
"""Replace a string in a file.
|
|
156
151
|
|
|
152
|
+
Uses a Python script on the sandbox to avoid transferring the entire file.
|
|
153
|
+
Only the base64-encoded old_str and new_str are sent.
|
|
154
|
+
|
|
157
155
|
Args:
|
|
158
156
|
ctx: The sandbox context.
|
|
159
157
|
path: The file path to modify.
|
|
@@ -162,85 +160,62 @@ def str_replace(
|
|
|
162
160
|
timeout: Optional timeout for command execution.
|
|
163
161
|
|
|
164
162
|
Returns:
|
|
165
|
-
A dict with
|
|
163
|
+
A dict with success message or error details.
|
|
166
164
|
"""
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
old_b64 = base64.b64encode(old_str.encode()).decode()
|
|
166
|
+
new_b64 = base64.b64encode(new_str.encode()).decode()
|
|
167
|
+
|
|
168
|
+
# Write Python script to a temp file and execute it
|
|
169
|
+
# This avoids shell escaping issues with inline python -c
|
|
170
|
+
py_script = f'''import sys, base64, os
|
|
171
|
+
old = base64.b64decode("{old_b64}").decode()
|
|
172
|
+
new = base64.b64decode("{new_b64}").decode()
|
|
173
|
+
path = "{path}"
|
|
174
|
+
if not os.path.exists(path):
|
|
175
|
+
print("FILE_NOT_FOUND")
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
with open(path, "r") as f:
|
|
178
|
+
content = f.read()
|
|
179
|
+
count = content.count(old)
|
|
180
|
+
if count == 0:
|
|
181
|
+
print("NOT_FOUND")
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
if count > 1:
|
|
184
|
+
print(f"MULTIPLE:{{count}}")
|
|
185
|
+
sys.exit(0)
|
|
186
|
+
with open(path, "w") as f:
|
|
187
|
+
f.write(content.replace(old, new, 1))
|
|
188
|
+
print("SUCCESS")
|
|
189
|
+
'''
|
|
190
|
+
# Base64 encode the script itself to avoid any escaping issues
|
|
191
|
+
script_b64 = base64.b64encode(py_script.encode()).decode()
|
|
192
|
+
cmd = f"echo {escape_for_shell(script_b64)} | base64 -d | python3"
|
|
193
|
+
|
|
169
194
|
result = ctx.client.sandboxes.exec_command(
|
|
170
195
|
sandbox_id=ctx.sandbox_id,
|
|
171
|
-
command=
|
|
196
|
+
command=cmd,
|
|
172
197
|
timeout=timeout,
|
|
173
198
|
)
|
|
174
199
|
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
"error": f"File not found: {path}",
|
|
178
|
-
"stderr": result.stderr,
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
original_content = result.stdout
|
|
182
|
-
|
|
183
|
-
# Check if old_str exists in the file
|
|
184
|
-
if old_str not in original_content:
|
|
185
|
-
return {
|
|
186
|
-
"error": f"String not found in file: {old_str[:50]}...",
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
# Count occurrences
|
|
190
|
-
occurrences = original_content.count(old_str)
|
|
191
|
-
if occurrences > 1:
|
|
192
|
-
return {
|
|
193
|
-
"error": f"Multiple occurrences ({occurrences}) of the string found. Please provide more context to make the match unique.",
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
# Perform the replacement
|
|
197
|
-
new_content = original_content.replace(old_str, new_str, 1)
|
|
198
|
-
|
|
199
|
-
# Find the line numbers affected
|
|
200
|
-
old_lines = original_content.split("\n")
|
|
201
|
-
new_lines = new_content.split("\n")
|
|
200
|
+
output = result.stdout.strip()
|
|
202
201
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
for i, (old_line, new_line) in enumerate(zip(old_lines, new_lines)):
|
|
206
|
-
if old_line != new_line:
|
|
207
|
-
old_start = i + 1
|
|
208
|
-
break
|
|
202
|
+
if result.exit_code != 0 or output == "FILE_NOT_FOUND":
|
|
203
|
+
return {"error": f"File not found: {path}", "stderr": result.stderr}
|
|
209
204
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
|
|
205
|
+
if output == "NOT_FOUND":
|
|
206
|
+
return {"error": f"String not found in file: {old_str[:50]}..."}
|
|
213
207
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
command=write_cmd,
|
|
217
|
-
timeout=timeout,
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
if result.exit_code != 0:
|
|
208
|
+
if output.startswith("MULTIPLE:"):
|
|
209
|
+
count = output.split(":")[1]
|
|
221
210
|
return {
|
|
222
|
-
"error": f"
|
|
223
|
-
"
|
|
211
|
+
"error": f"Multiple occurrences ({count}) of the string found. "
|
|
212
|
+
"Please provide more context to make the match unique."
|
|
224
213
|
}
|
|
225
214
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
new_str_lines = new_str.count("\n") + 1
|
|
215
|
+
if output == "SUCCESS":
|
|
216
|
+
return {"msg": "Successfully replaced text at exactly one location."}
|
|
229
217
|
|
|
230
|
-
|
|
231
|
-
diff_lines = []
|
|
232
|
-
for line in old_str.split("\n"):
|
|
233
|
-
diff_lines.append(f"-{line}")
|
|
234
|
-
for line in new_str.split("\n"):
|
|
235
|
-
diff_lines.append(f"+{line}")
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
"oldStart": old_start,
|
|
239
|
-
"oldLines": old_str_lines,
|
|
240
|
-
"newStart": old_start,
|
|
241
|
-
"newLines": new_str_lines,
|
|
242
|
-
"lines": diff_lines,
|
|
243
|
-
}
|
|
218
|
+
return {"error": f"Unexpected response: {output}", "stderr": result.stderr}
|
|
244
219
|
|
|
245
220
|
|
|
246
221
|
# ============================================================================
|
|
@@ -249,7 +224,10 @@ def str_replace(
|
|
|
249
224
|
|
|
250
225
|
|
|
251
226
|
async def async_view_file(
|
|
252
|
-
ctx: "AsyncSandboxContext",
|
|
227
|
+
ctx: "AsyncSandboxContext",
|
|
228
|
+
path: str,
|
|
229
|
+
view_range: list | None,
|
|
230
|
+
timeout: float | None,
|
|
253
231
|
) -> dict:
|
|
254
232
|
"""View file content with line numbers (async).
|
|
255
233
|
|
|
@@ -262,50 +240,53 @@ async def async_view_file(
|
|
|
262
240
|
Returns:
|
|
263
241
|
A dict with file content and metadata, or error information.
|
|
264
242
|
"""
|
|
265
|
-
|
|
266
|
-
result = await ctx.client.sandboxes.exec_command(
|
|
267
|
-
sandbox_id=ctx.sandbox_id,
|
|
268
|
-
command=check_cmd,
|
|
269
|
-
timeout=timeout,
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
if "FILE_NOT_FOUND" in result.stdout or result.exit_code != 0:
|
|
273
|
-
return {
|
|
274
|
-
"error": f"File not found: {path}",
|
|
275
|
-
"stderr": result.stderr,
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
total_lines = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
|
|
243
|
+
escaped_path = escape_for_shell(path)
|
|
279
244
|
|
|
245
|
+
# Build combined command: check existence, get total lines, and view content in one exec
|
|
280
246
|
if view_range and len(view_range) == 2:
|
|
281
247
|
start_line, end_line = view_range
|
|
282
|
-
|
|
248
|
+
view_cmd = (
|
|
249
|
+
f"sed -n '{start_line},{end_line}p' {escaped_path} | nl -ba -v {start_line}"
|
|
250
|
+
)
|
|
283
251
|
else:
|
|
284
|
-
# Default to first 200 lines if no range specified
|
|
285
252
|
max_lines = 200
|
|
286
|
-
|
|
253
|
+
view_cmd = f"head -n {max_lines} {escaped_path} | nl -ba"
|
|
287
254
|
start_line = 1
|
|
288
255
|
|
|
256
|
+
# Single combined command: outputs "TOTAL:<n>" on first line, then file content
|
|
257
|
+
cmd = (
|
|
258
|
+
f"if [ ! -f {escaped_path} ]; then echo 'FILE_NOT_FOUND'; exit 1; fi; "
|
|
259
|
+
f'echo "TOTAL:$(wc -l < {escaped_path})"; {view_cmd}'
|
|
260
|
+
)
|
|
261
|
+
|
|
289
262
|
result = await ctx.client.sandboxes.exec_command(
|
|
290
263
|
sandbox_id=ctx.sandbox_id,
|
|
291
264
|
command=cmd,
|
|
292
265
|
timeout=timeout,
|
|
293
266
|
)
|
|
294
267
|
|
|
295
|
-
if result.exit_code != 0:
|
|
268
|
+
if result.exit_code != 0 or "FILE_NOT_FOUND" in result.stdout:
|
|
296
269
|
return {
|
|
297
|
-
"error": f"
|
|
270
|
+
"error": f"File not found: {path}",
|
|
298
271
|
"stderr": result.stderr,
|
|
299
272
|
}
|
|
300
273
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
274
|
+
# Parse output: first line is "TOTAL:<n>", rest is content
|
|
275
|
+
lines = result.stdout.split("\n", 1)
|
|
276
|
+
total_lines = 0
|
|
277
|
+
content = ""
|
|
278
|
+
|
|
279
|
+
if lines and lines[0].startswith("TOTAL:"):
|
|
280
|
+
total_str = lines[0][6:].strip()
|
|
281
|
+
total_lines = int(total_str) if total_str.isdigit() else 0
|
|
282
|
+
content = lines[1] if len(lines) > 1 else ""
|
|
283
|
+
|
|
284
|
+
content_lines = content.rstrip("\n").split("\n") if content.strip() else []
|
|
304
285
|
num_lines = len(content_lines)
|
|
305
286
|
|
|
306
287
|
return {
|
|
307
288
|
"file_type": "text",
|
|
308
|
-
"content": truncate_content(
|
|
289
|
+
"content": truncate_content(content),
|
|
309
290
|
"numLines": num_lines,
|
|
310
291
|
"startLine": start_line if view_range else 1,
|
|
311
292
|
"totalLines": total_lines + 1,
|
|
@@ -326,38 +307,35 @@ async def async_create_file(
|
|
|
326
307
|
Returns:
|
|
327
308
|
A dict with creation status or error information.
|
|
328
309
|
"""
|
|
329
|
-
|
|
330
|
-
check_result = await ctx.client.sandboxes.exec_command(
|
|
331
|
-
sandbox_id=ctx.sandbox_id,
|
|
332
|
-
command=check_cmd,
|
|
333
|
-
timeout=timeout,
|
|
334
|
-
)
|
|
335
|
-
is_update = "EXISTS" in check_result.stdout
|
|
336
|
-
|
|
337
|
-
dir_path = "/".join(path.split("/")[:-1])
|
|
338
|
-
if dir_path:
|
|
339
|
-
mkdir_cmd = f"mkdir -p {escape_for_shell(dir_path)}"
|
|
340
|
-
await ctx.client.sandboxes.exec_command(
|
|
341
|
-
sandbox_id=ctx.sandbox_id,
|
|
342
|
-
command=mkdir_cmd,
|
|
343
|
-
timeout=timeout,
|
|
344
|
-
)
|
|
345
|
-
|
|
310
|
+
escaped_path = escape_for_shell(path)
|
|
346
311
|
encoded_content = base64.b64encode(file_text.encode()).decode()
|
|
347
|
-
|
|
312
|
+
|
|
313
|
+
# Get directory path for mkdir
|
|
314
|
+
dir_path = posixpath.dirname(path)
|
|
315
|
+
mkdir_part = f"mkdir -p {escape_for_shell(dir_path)} && " if dir_path else ""
|
|
316
|
+
|
|
317
|
+
# Single combined command: check existence, create dir, write file
|
|
318
|
+
cmd = (
|
|
319
|
+
f"is_update=$(test -f {escaped_path} && echo 1 || echo 0); "
|
|
320
|
+
f"{mkdir_part}"
|
|
321
|
+
f"echo {escape_for_shell(encoded_content)} | base64 -d > {escaped_path} && "
|
|
322
|
+
f'echo "STATUS:$is_update"'
|
|
323
|
+
)
|
|
348
324
|
|
|
349
325
|
result = await ctx.client.sandboxes.exec_command(
|
|
350
326
|
sandbox_id=ctx.sandbox_id,
|
|
351
|
-
command=
|
|
327
|
+
command=cmd,
|
|
352
328
|
timeout=timeout,
|
|
353
329
|
)
|
|
354
330
|
|
|
355
|
-
if result.exit_code != 0:
|
|
331
|
+
if result.exit_code != 0 or "STATUS:" not in result.stdout:
|
|
356
332
|
return {
|
|
357
333
|
"error": f"Failed to create file: {path}",
|
|
358
334
|
"stderr": result.stderr,
|
|
359
335
|
}
|
|
360
336
|
|
|
337
|
+
is_update = "STATUS:1" in result.stdout
|
|
338
|
+
|
|
361
339
|
return {
|
|
362
340
|
"is_file_update": is_update,
|
|
363
341
|
"message": f"File {'updated' if is_update else 'created'}: {path}",
|
|
@@ -365,10 +343,17 @@ async def async_create_file(
|
|
|
365
343
|
|
|
366
344
|
|
|
367
345
|
async def async_str_replace(
|
|
368
|
-
ctx: "AsyncSandboxContext",
|
|
346
|
+
ctx: "AsyncSandboxContext",
|
|
347
|
+
path: str,
|
|
348
|
+
old_str: str,
|
|
349
|
+
new_str: str,
|
|
350
|
+
timeout: float | None,
|
|
369
351
|
) -> dict:
|
|
370
352
|
"""Replace a string in a file (async).
|
|
371
353
|
|
|
354
|
+
Uses a Python script on the sandbox to avoid transferring the entire file.
|
|
355
|
+
Only the base64-encoded old_str and new_str are sent.
|
|
356
|
+
|
|
372
357
|
Args:
|
|
373
358
|
ctx: The async sandbox context.
|
|
374
359
|
path: The file path to modify.
|
|
@@ -377,73 +362,59 @@ async def async_str_replace(
|
|
|
377
362
|
timeout: Optional timeout for command execution.
|
|
378
363
|
|
|
379
364
|
Returns:
|
|
380
|
-
A dict with
|
|
365
|
+
A dict with success message or error details.
|
|
381
366
|
"""
|
|
382
|
-
|
|
367
|
+
old_b64 = base64.b64encode(old_str.encode()).decode()
|
|
368
|
+
new_b64 = base64.b64encode(new_str.encode()).decode()
|
|
369
|
+
|
|
370
|
+
# Write Python script to a temp file and execute it
|
|
371
|
+
# This avoids shell escaping issues with inline python -c
|
|
372
|
+
py_script = f'''import sys, base64, os
|
|
373
|
+
old = base64.b64decode("{old_b64}").decode()
|
|
374
|
+
new = base64.b64decode("{new_b64}").decode()
|
|
375
|
+
path = "{path}"
|
|
376
|
+
if not os.path.exists(path):
|
|
377
|
+
print("FILE_NOT_FOUND")
|
|
378
|
+
sys.exit(1)
|
|
379
|
+
with open(path, "r") as f:
|
|
380
|
+
content = f.read()
|
|
381
|
+
count = content.count(old)
|
|
382
|
+
if count == 0:
|
|
383
|
+
print("NOT_FOUND")
|
|
384
|
+
sys.exit(0)
|
|
385
|
+
if count > 1:
|
|
386
|
+
print(f"MULTIPLE:{{count}}")
|
|
387
|
+
sys.exit(0)
|
|
388
|
+
with open(path, "w") as f:
|
|
389
|
+
f.write(content.replace(old, new, 1))
|
|
390
|
+
print("SUCCESS")
|
|
391
|
+
'''
|
|
392
|
+
# Base64 encode the script itself to avoid any escaping issues
|
|
393
|
+
script_b64 = base64.b64encode(py_script.encode()).decode()
|
|
394
|
+
cmd = f"echo {escape_for_shell(script_b64)} | base64 -d | python3"
|
|
395
|
+
|
|
383
396
|
result = await ctx.client.sandboxes.exec_command(
|
|
384
397
|
sandbox_id=ctx.sandbox_id,
|
|
385
|
-
command=
|
|
398
|
+
command=cmd,
|
|
386
399
|
timeout=timeout,
|
|
387
400
|
)
|
|
388
401
|
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
"error": f"File not found: {path}",
|
|
392
|
-
"stderr": result.stderr,
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
original_content = result.stdout
|
|
396
|
-
|
|
397
|
-
if old_str not in original_content:
|
|
398
|
-
return {
|
|
399
|
-
"error": f"String not found in file: {old_str[:50]}...",
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
occurrences = original_content.count(old_str)
|
|
403
|
-
if occurrences > 1:
|
|
404
|
-
return {
|
|
405
|
-
"error": f"Multiple occurrences ({occurrences}) of the string found. Please provide more context to make the match unique.",
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
new_content = original_content.replace(old_str, new_str, 1)
|
|
409
|
-
|
|
410
|
-
old_lines = original_content.split("\n")
|
|
411
|
-
new_lines = new_content.split("\n")
|
|
402
|
+
output = result.stdout.strip()
|
|
412
403
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if old_line != new_line:
|
|
416
|
-
old_start = i + 1
|
|
417
|
-
break
|
|
404
|
+
if result.exit_code != 0 or output == "FILE_NOT_FOUND":
|
|
405
|
+
return {"error": f"File not found: {path}", "stderr": result.stderr}
|
|
418
406
|
|
|
419
|
-
|
|
420
|
-
|
|
407
|
+
if output == "NOT_FOUND":
|
|
408
|
+
return {"error": f"String not found in file: {old_str[:50]}..."}
|
|
421
409
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
command=write_cmd,
|
|
425
|
-
timeout=timeout,
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
if result.exit_code != 0:
|
|
410
|
+
if output.startswith("MULTIPLE:"):
|
|
411
|
+
count = output.split(":")[1]
|
|
429
412
|
return {
|
|
430
|
-
"error": f"
|
|
431
|
-
"
|
|
413
|
+
"error": f"Multiple occurrences ({count}) of the string found. "
|
|
414
|
+
"Please provide more context to make the match unique."
|
|
432
415
|
}
|
|
433
416
|
|
|
434
|
-
|
|
435
|
-
|
|
417
|
+
if output == "SUCCESS":
|
|
418
|
+
return {"msg": "Successfully replaced text at exactly one location."}
|
|
436
419
|
|
|
437
|
-
|
|
438
|
-
for line in old_str.split("\n"):
|
|
439
|
-
diff_lines.append(f"-{line}")
|
|
440
|
-
for line in new_str.split("\n"):
|
|
441
|
-
diff_lines.append(f"+{line}")
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
"oldStart": old_start,
|
|
445
|
-
"oldLines": old_str_lines,
|
|
446
|
-
"newStart": old_start,
|
|
447
|
-
"newLines": new_str_lines,
|
|
448
|
-
"lines": diff_lines,
|
|
449
|
-
}
|
|
420
|
+
return {"error": f"Unexpected response: {output}", "stderr": result.stderr}
|
|
@@ -5,9 +5,9 @@ acontext/agent/__init__.py,sha256=lx5HqeVDfIxkbGfAt0ju8hkk8EPsgFvKmMrQzjNXR8w,21
|
|
|
5
5
|
acontext/agent/base.py,sha256=BDTqfqkPrPzvQIH_llOX6S_bkcDw3E30J5x9yd47Q6g,3526
|
|
6
6
|
acontext/agent/disk.py,sha256=q-3DRkLcQLfdAXADJM3GPo6reWPzhKQrnyBiZdBZvIY,22775
|
|
7
7
|
acontext/agent/prompts.py,sha256=awhYYClNAROnTAfzKKD8G5gHfTFs-1KqMUEWCEnlMMQ,4954
|
|
8
|
-
acontext/agent/sandbox.py,sha256=
|
|
8
|
+
acontext/agent/sandbox.py,sha256=ziGGs2yqJaik_9o7S3f9G-CzcndmRs9fC3WDUi_UGbI,19016
|
|
9
9
|
acontext/agent/skill.py,sha256=uB7teH7OAk4Cy6pOLRCJFBi5KZbyMMgZSLEt77TLB20,12880
|
|
10
|
-
acontext/agent/text_editor.py,sha256=
|
|
10
|
+
acontext/agent/text_editor.py,sha256=mrUtMfAtgpOOwYnh2xQpWRc0lCoE9u2_WMTVRuFiWRQ,13390
|
|
11
11
|
acontext/async_client.py,sha256=WjqW7ruMtlYTBlk6PQ1-1yhjFSBBxGqI3oxrfEqqIpQ,8471
|
|
12
12
|
acontext/client.py,sha256=XggELxXU9-8BcAcHOoiqQZypfPmkhe7SyJ3plHq-A9M,8217
|
|
13
13
|
acontext/client_types.py,sha256=oR6lSDMjEERx0mIVOpKaTXbEu_lfoxe3fDpzA-ALE-8,1063
|
|
@@ -36,6 +36,6 @@ acontext/types/skill.py,sha256=v7OAvtkwTeZAkLG7NQMcwFoSJ4g-HDWjPZ4EoMyU3mk,2779
|
|
|
36
36
|
acontext/types/tool.py,sha256=UlX6JIGRKd96vPayOkyi6pGwFi6w9GwWwAxr4BcqVts,670
|
|
37
37
|
acontext/types/user.py,sha256=dBzHCqULSJ3Sqw7T8nA0U8Sctz76Pd0hm1qsHvtEIBQ,1264
|
|
38
38
|
acontext/uploads.py,sha256=6twnqQOY_eerNuEjeSKsE_3S0IfJUiczXtAy4aXqDl8,1379
|
|
39
|
-
acontext-0.1.
|
|
40
|
-
acontext-0.1.
|
|
41
|
-
acontext-0.1.
|
|
39
|
+
acontext-0.1.6.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
|
|
40
|
+
acontext-0.1.6.dist-info/METADATA,sha256=xUgSw00x4t4EHQR7WLgfwlnjEgXJjD1LH2mk4t_KKSo,888
|
|
41
|
+
acontext-0.1.6.dist-info/RECORD,,
|