wcgw 2.2.1__tar.gz → 2.3.0__tar.gz
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.
Potentially problematic release.
This version of wcgw might be problematic. Click here for more details.
- {wcgw-2.2.1 → wcgw-2.3.0}/PKG-INFO +3 -3
- {wcgw-2.2.1 → wcgw-2.3.0}/gpt_instructions.txt +2 -1
- {wcgw-2.2.1 → wcgw-2.3.0}/pyproject.toml +3 -3
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/anthropic_client.py +6 -3
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/mcp_server/server.py +10 -5
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/openai_client.py +2 -1
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/tools.py +220 -73
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/types_.py +2 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/uv.lock +131 -134
- {wcgw-2.2.1 → wcgw-2.3.0}/.github/workflows/python-publish.yml +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/.github/workflows/python-tests.yml +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/.github/workflows/python-types.yml +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/.gitignore +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/.python-version +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/.vscode/settings.json +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/README.md +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/gpt_action_json_schema.json +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/openai.md +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/__init__.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/__init__.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/__init__.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/__main__.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/cli.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/common.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/computer_use.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/diff-instructions.txt +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/mcp_server/Readme.md +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/mcp_server/__init__.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/openai_utils.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/client/sys_utils.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/relay/serve.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/src/wcgw/relay/static/privacy.txt +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/static/claude-ss.jpg +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/static/computer-use.jpg +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/static/example.jpg +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/static/rocket-icon.png +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/static/ss1.png +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/tests/test_basic.py +0 -0
- {wcgw-2.2.1 → wcgw-2.3.0}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Shell and coding agent on claude and chatgpt
|
|
5
5
|
Project-URL: Homepage, https://github.com/rusiaaman/wcgw
|
|
6
6
|
Author-email: Aman Rusia <gapypi@arcfu.com>
|
|
@@ -9,7 +9,6 @@ Requires-Dist: anthropic>=0.39.0
|
|
|
9
9
|
Requires-Dist: fastapi>=0.115.0
|
|
10
10
|
Requires-Dist: humanize>=4.11.0
|
|
11
11
|
Requires-Dist: mcp
|
|
12
|
-
Requires-Dist: nltk>=3.9.1
|
|
13
12
|
Requires-Dist: openai>=1.46.0
|
|
14
13
|
Requires-Dist: petname>=2.6
|
|
15
14
|
Requires-Dist: pexpect>=4.9.0
|
|
@@ -19,6 +18,7 @@ Requires-Dist: python-dotenv>=1.0.1
|
|
|
19
18
|
Requires-Dist: rich>=13.8.1
|
|
20
19
|
Requires-Dist: semantic-version>=2.10.0
|
|
21
20
|
Requires-Dist: shell>=1.0.1
|
|
21
|
+
Requires-Dist: syntax-checker==0.2.10
|
|
22
22
|
Requires-Dist: tiktoken==0.7.0
|
|
23
23
|
Requires-Dist: toml>=0.10.2
|
|
24
24
|
Requires-Dist: typer>=0.12.5
|
|
@@ -16,13 +16,14 @@ Instructions for `BashCommand`:
|
|
|
16
16
|
- Do not use interactive commands like nano. Prefer writing simpler commands.
|
|
17
17
|
- Status of the command and the current working directory will always be returned at the end.
|
|
18
18
|
- Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
|
|
19
|
-
- The first line might be `(...truncated)` if the output is too long.
|
|
19
|
+
- The first or the last line might be `(...truncated)` if the output is too long.
|
|
20
20
|
- The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
|
|
21
21
|
- Run long running commands in background using screen instead of "&".
|
|
22
22
|
|
|
23
23
|
Instructions for `Read File`
|
|
24
24
|
- Read full content of a file.
|
|
25
25
|
- Provide absolute file path only.
|
|
26
|
+
- Use this instead of 'cat' from BashCommand
|
|
26
27
|
|
|
27
28
|
Instructions for `Write if Empty`
|
|
28
29
|
- Write content to an empty or non-existent file. Provide file path and content. Use this instead of BashCommand for writing new files.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
authors = [{ name = "Aman Rusia", email = "gapypi@arcfu.com" }]
|
|
3
3
|
name = "wcgw"
|
|
4
|
-
version = "2.
|
|
4
|
+
version = "2.3.0"
|
|
5
5
|
description = "Shell and coding agent on claude and chatgpt"
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
requires-python = ">=3.11, <3.13"
|
|
@@ -22,10 +22,10 @@ dependencies = [
|
|
|
22
22
|
"websockets>=13.1",
|
|
23
23
|
"pydantic>=2.9.2",
|
|
24
24
|
"semantic-version>=2.10.0",
|
|
25
|
-
"nltk>=3.9.1",
|
|
26
25
|
"anthropic>=0.39.0",
|
|
27
|
-
"mcp",
|
|
28
26
|
"humanize>=4.11.0",
|
|
27
|
+
"mcp",
|
|
28
|
+
"syntax-checker==0.2.10",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[project.urls]
|
|
@@ -169,10 +169,11 @@ def loop(
|
|
|
169
169
|
- Do not use interactive commands like nano. Prefer writing simpler commands.
|
|
170
170
|
- Status of the command and the current working directory will always be returned at the end.
|
|
171
171
|
- Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
|
|
172
|
-
- The first line might be `(...truncated)` if the output is too long.
|
|
172
|
+
- The first or the last line might be `(...truncated)` if the output is too long.
|
|
173
173
|
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
|
|
174
174
|
- The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
|
|
175
175
|
- Run long running commands in background using screen instead of "&".
|
|
176
|
+
- Use longer wait_for_seconds if the command is expected to run for a long time.
|
|
176
177
|
""",
|
|
177
178
|
),
|
|
178
179
|
ToolParam(
|
|
@@ -185,8 +186,9 @@ def loop(
|
|
|
185
186
|
- Send send_specials=["Enter"] to recheck status of a running program.
|
|
186
187
|
- Only one of send_text, send_specials, send_ascii should be provided.
|
|
187
188
|
- This returns within 5 seconds, for heavy programs keep checking status for upto 10 turns before asking user to continue checking again.
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
- Programs don't hang easily, so most likely explanation for no output is usually that the program is still running, and you need to check status again using ["Enter"].
|
|
190
|
+
- Do not send Ctrl-c before checking for status till 10 minutes or whatever is appropriate for the program to finish.
|
|
191
|
+
- Set longer wait_for_seconds when program is expected to run for a long time.
|
|
190
192
|
""",
|
|
191
193
|
),
|
|
192
194
|
ToolParam(
|
|
@@ -195,6 +197,7 @@ def loop(
|
|
|
195
197
|
description="""
|
|
196
198
|
- Read full file content
|
|
197
199
|
- Provide absolute file path only
|
|
200
|
+
- Use this instead of 'cat' from BashCommand
|
|
198
201
|
""",
|
|
199
202
|
),
|
|
200
203
|
ToolParam(
|
|
@@ -89,10 +89,11 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
89
89
|
- Do not use interactive commands like nano. Prefer writing simpler commands.
|
|
90
90
|
- Status of the command and the current working directory will always be returned at the end.
|
|
91
91
|
- Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
|
|
92
|
-
- The first line might be `(...truncated)` if the output is too long.
|
|
92
|
+
- The first or the last line might be `(...truncated)` if the output is too long.
|
|
93
93
|
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
|
|
94
|
-
- The control will return to you in
|
|
94
|
+
- The control will return to you in 3 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
|
|
95
95
|
- Run long running commands in background using screen instead of "&".
|
|
96
|
+
- Use longer wait_for_seconds if the command is expected to run for a long time.
|
|
96
97
|
""",
|
|
97
98
|
),
|
|
98
99
|
ToolParam(
|
|
@@ -105,8 +106,9 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
105
106
|
- Send send_specials=["Enter"] to recheck status of a running program.
|
|
106
107
|
- Only one of send_text, send_specials, send_ascii should be provided.
|
|
107
108
|
- This returns within 3 seconds, for heavy programs keep checking status for upto 10 turns before asking user to continue checking again.
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
- Programs don't hang easily, so most likely explanation for no output is usually that the program is still running, and you need to check status again using ["Enter"].
|
|
110
|
+
- Do not send Ctrl-c before checking for status till 10 minutes or whatever is appropriate for the program to finish.
|
|
111
|
+
- Set longer wait_for_seconds when program is expected to run for a long time.
|
|
110
112
|
""",
|
|
111
113
|
),
|
|
112
114
|
ToolParam(
|
|
@@ -115,6 +117,7 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
115
117
|
description="""
|
|
116
118
|
- Read full file content
|
|
117
119
|
- Provide absolute file path only
|
|
120
|
+
- Use this instead of 'cat' from BashCommand
|
|
118
121
|
""",
|
|
119
122
|
),
|
|
120
123
|
ToolParam(
|
|
@@ -247,7 +250,9 @@ async def handle_call_tool(
|
|
|
247
250
|
- Always read relevant files before editing.
|
|
248
251
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
249
252
|
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
250
|
-
|
|
253
|
+
- Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using shell tools.
|
|
254
|
+
- Do not use Ctrl-c or Ctrl-z or interrupt commands without asking the user, because often the program don't show any update but they still are running.
|
|
255
|
+
- Do not use echo to write multi-line files, always use FileEdit tool to update a code.
|
|
251
256
|
|
|
252
257
|
Additional instructions:
|
|
253
258
|
Always run `pwd` if you get any file or directory not found error to make sure you're not lost, or to get absolute cwd.
|
|
@@ -172,7 +172,7 @@ def loop(
|
|
|
172
172
|
- Do not use interactive commands like nano. Prefer writing simpler commands.
|
|
173
173
|
- Status of the command and the current working directory will always be returned at the end.
|
|
174
174
|
- Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
|
|
175
|
-
- The first line might be `(...truncated)` if the output is too long.
|
|
175
|
+
- The first or the last line might be `(...truncated)` if the output is too long.
|
|
176
176
|
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
|
|
177
177
|
- The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
|
|
178
178
|
- Run long running commands in background using screen instead of "&".
|
|
@@ -192,6 +192,7 @@ def loop(
|
|
|
192
192
|
description="""
|
|
193
193
|
- Read full file content
|
|
194
194
|
- Provide absolute file path only
|
|
195
|
+
- Use this instead of 'cat' from BashCommand
|
|
195
196
|
""",
|
|
196
197
|
),
|
|
197
198
|
openai.pydantic_function_tool(
|
|
@@ -13,7 +13,7 @@ import threading
|
|
|
13
13
|
import importlib.metadata
|
|
14
14
|
import time
|
|
15
15
|
import traceback
|
|
16
|
-
from tempfile import TemporaryDirectory
|
|
16
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
17
17
|
from typing import (
|
|
18
18
|
Callable,
|
|
19
19
|
Literal,
|
|
@@ -42,7 +42,7 @@ import rich
|
|
|
42
42
|
import pyte
|
|
43
43
|
from dotenv import load_dotenv
|
|
44
44
|
|
|
45
|
-
import
|
|
45
|
+
from syntax_checker import check_syntax
|
|
46
46
|
from openai import OpenAI
|
|
47
47
|
from openai.types.chat import (
|
|
48
48
|
ChatCompletionMessageParam,
|
|
@@ -50,7 +50,7 @@ from openai.types.chat import (
|
|
|
50
50
|
ChatCompletionMessage,
|
|
51
51
|
ParsedChatCompletionMessage,
|
|
52
52
|
)
|
|
53
|
-
from
|
|
53
|
+
from difflib import SequenceMatcher
|
|
54
54
|
|
|
55
55
|
from ..types_ import (
|
|
56
56
|
BashCommand,
|
|
@@ -183,6 +183,7 @@ class BashState:
|
|
|
183
183
|
self._is_in_docker: Optional[str] = ""
|
|
184
184
|
self._cwd: str = os.getcwd()
|
|
185
185
|
self._shell = start_shell()
|
|
186
|
+
self._whitelist_for_overwrite: set[str] = set()
|
|
186
187
|
|
|
187
188
|
# Get exit info to ensure shell is ready
|
|
188
189
|
_get_exit_code(self._shell)
|
|
@@ -235,6 +236,13 @@ class BashState:
|
|
|
235
236
|
)
|
|
236
237
|
return "Not pending"
|
|
237
238
|
|
|
239
|
+
@property
|
|
240
|
+
def whitelist_for_overwrite(self) -> set[str]:
|
|
241
|
+
return self._whitelist_for_overwrite
|
|
242
|
+
|
|
243
|
+
def add_to_whitelist_for_overwrite(self, file_path: str) -> None:
|
|
244
|
+
self._whitelist_for_overwrite.add(file_path)
|
|
245
|
+
|
|
238
246
|
|
|
239
247
|
BASH_STATE = BashState()
|
|
240
248
|
|
|
@@ -269,7 +277,7 @@ def update_repl_prompt(command: str) -> bool:
|
|
|
269
277
|
BASH_STATE.shell.sendintr()
|
|
270
278
|
index = BASH_STATE.shell.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
|
|
271
279
|
if index == 0:
|
|
272
|
-
return
|
|
280
|
+
return True
|
|
273
281
|
before = BASH_STATE.shell.before or ""
|
|
274
282
|
assert before, "Something went wrong updating repl prompt"
|
|
275
283
|
PROMPT = before.split("\n")[-1].strip()
|
|
@@ -301,6 +309,34 @@ def get_status() -> str:
|
|
|
301
309
|
return status.rstrip()
|
|
302
310
|
|
|
303
311
|
|
|
312
|
+
T = TypeVar("T")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def save_out_of_context(
|
|
316
|
+
tokens: list[T],
|
|
317
|
+
max_tokens: int,
|
|
318
|
+
suffix: str,
|
|
319
|
+
tokens_converted: Callable[[list[T]], str],
|
|
320
|
+
) -> tuple[str, list[Path]]:
|
|
321
|
+
file_contents = list[str]()
|
|
322
|
+
for i in range(0, len(tokens), max_tokens):
|
|
323
|
+
file_contents.append(tokens_converted(tokens[i : i + max_tokens]))
|
|
324
|
+
|
|
325
|
+
if len(file_contents) == 1:
|
|
326
|
+
return file_contents[0], []
|
|
327
|
+
|
|
328
|
+
rest_paths = list[Path]()
|
|
329
|
+
for i, content in enumerate(file_contents):
|
|
330
|
+
if i == 0:
|
|
331
|
+
continue
|
|
332
|
+
file_path = NamedTemporaryFile(delete=False, suffix=suffix).name
|
|
333
|
+
with open(file_path, "w") as f:
|
|
334
|
+
f.write(content)
|
|
335
|
+
rest_paths.append(Path(file_path))
|
|
336
|
+
|
|
337
|
+
return file_contents[0], rest_paths
|
|
338
|
+
|
|
339
|
+
|
|
304
340
|
def execute_bash(
|
|
305
341
|
enc: tiktoken.Encoding,
|
|
306
342
|
bash_arg: BashCommand | BashInteraction,
|
|
@@ -412,7 +448,7 @@ def execute_bash(
|
|
|
412
448
|
tokens = enc.encode(text)
|
|
413
449
|
|
|
414
450
|
if max_tokens and len(tokens) >= max_tokens:
|
|
415
|
-
text = "...
|
|
451
|
+
text = "(...truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
|
|
416
452
|
|
|
417
453
|
if is_interrupt:
|
|
418
454
|
text = (
|
|
@@ -441,7 +477,7 @@ Otherwise, you may want to try Ctrl-c again or program specific exit interactive
|
|
|
441
477
|
|
|
442
478
|
tokens = enc.encode(output)
|
|
443
479
|
if max_tokens and len(tokens) >= max_tokens:
|
|
444
|
-
output = "...
|
|
480
|
+
output = "(...truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
|
|
445
481
|
|
|
446
482
|
try:
|
|
447
483
|
exit_status = get_status()
|
|
@@ -490,8 +526,6 @@ class ImageData(BaseModel):
|
|
|
490
526
|
|
|
491
527
|
Param = ParamSpec("Param")
|
|
492
528
|
|
|
493
|
-
T = TypeVar("T")
|
|
494
|
-
|
|
495
529
|
|
|
496
530
|
def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
|
|
497
531
|
def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
|
|
@@ -537,20 +571,19 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
|
|
|
537
571
|
else:
|
|
538
572
|
path_ = writefile.file_path
|
|
539
573
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
len(TOOL_CALLS) > 1
|
|
543
|
-
and isinstance(TOOL_CALLS[-2], FileEdit)
|
|
544
|
-
and TOOL_CALLS[-2].file_path == path_
|
|
545
|
-
)
|
|
546
|
-
and error_on_exist
|
|
547
|
-
)
|
|
548
|
-
|
|
574
|
+
error_on_exist_ = error_on_exist and path_ not in BASH_STATE.whitelist_for_overwrite
|
|
575
|
+
add_overwrite_warning = ""
|
|
549
576
|
if not BASH_STATE.is_in_docker:
|
|
550
|
-
if error_on_exist and os.path.exists(path_):
|
|
551
|
-
|
|
552
|
-
if
|
|
553
|
-
|
|
577
|
+
if (error_on_exist or error_on_exist_) and os.path.exists(path_):
|
|
578
|
+
content = Path(path_).read_text().strip()
|
|
579
|
+
if content:
|
|
580
|
+
if error_on_exist_:
|
|
581
|
+
return f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
582
|
+
elif error_on_exist:
|
|
583
|
+
add_overwrite_warning = content
|
|
584
|
+
|
|
585
|
+
# Since we've already errored once, add this to whitelist
|
|
586
|
+
BASH_STATE.add_to_whitelist_for_overwrite(path_)
|
|
554
587
|
|
|
555
588
|
path = Path(path_)
|
|
556
589
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -561,12 +594,19 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
|
|
|
561
594
|
except OSError as e:
|
|
562
595
|
return f"Error: {e}"
|
|
563
596
|
else:
|
|
564
|
-
if error_on_exist:
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
597
|
+
if error_on_exist or error_on_exist_:
|
|
598
|
+
return_code, content, stderr = command_run(
|
|
599
|
+
f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(path_)}",
|
|
600
|
+
timeout=TIMEOUT,
|
|
601
|
+
)
|
|
602
|
+
if return_code != 0 and content.strip():
|
|
603
|
+
if error_on_exist_:
|
|
604
|
+
return f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
605
|
+
else:
|
|
606
|
+
add_overwrite_warning = content
|
|
607
|
+
|
|
608
|
+
# Since we've already errored once, add this to whitelist
|
|
609
|
+
BASH_STATE.add_to_whitelist_for_overwrite(path_)
|
|
570
610
|
|
|
571
611
|
with TemporaryDirectory() as tmpdir:
|
|
572
612
|
tmppath = os.path.join(tmpdir, os.path.basename(path_))
|
|
@@ -586,41 +626,75 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
|
|
|
586
626
|
if rcode != 0:
|
|
587
627
|
return f"Error: Write failed with code {rcode}"
|
|
588
628
|
|
|
629
|
+
extension = Path(path_).suffix.lstrip(".")
|
|
630
|
+
|
|
589
631
|
console.print(f"File written to {path_}")
|
|
590
|
-
|
|
632
|
+
|
|
633
|
+
warnings = []
|
|
634
|
+
try:
|
|
635
|
+
check = check_syntax(extension, writefile.file_content)
|
|
636
|
+
syntax_errors = check.description
|
|
637
|
+
if syntax_errors:
|
|
638
|
+
console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
639
|
+
warnings.append(f"""
|
|
640
|
+
---
|
|
641
|
+
Warning: tree-sitter reported syntax errors, please re-read the file and fix if any errors.
|
|
642
|
+
Errors:
|
|
643
|
+
{syntax_errors}
|
|
644
|
+
---
|
|
645
|
+
""")
|
|
646
|
+
|
|
647
|
+
except Exception:
|
|
648
|
+
pass
|
|
649
|
+
|
|
650
|
+
if add_overwrite_warning:
|
|
651
|
+
warnings.append(
|
|
652
|
+
"\n---\nWarning: a file already existed and it's now overwritten. Was it a mistake? If yes please revert your action."
|
|
653
|
+
"Here's the previous content:\n```\n" + add_overwrite_warning + "\n```"
|
|
654
|
+
"\n---\n"
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
return "Success" + "".join(warnings)
|
|
591
658
|
|
|
592
659
|
|
|
593
660
|
def find_least_edit_distance_substring(
|
|
594
|
-
|
|
595
|
-
) -> tuple[str,
|
|
596
|
-
|
|
597
|
-
content_lines = [
|
|
598
|
-
line.strip() for line in orig_content_lines
|
|
599
|
-
] # Remove trailing and leading space for calculating edit distance
|
|
661
|
+
orig_content_lines: list[str], find_lines: list[str]
|
|
662
|
+
) -> tuple[list[str], str]:
|
|
663
|
+
# Prepare content lines, stripping whitespace and keeping track of original indices
|
|
664
|
+
content_lines = [line.strip() for line in orig_content_lines]
|
|
600
665
|
new_to_original_indices = {}
|
|
601
666
|
new_content_lines = []
|
|
602
|
-
for i in
|
|
603
|
-
if not
|
|
667
|
+
for i, line in enumerate(content_lines):
|
|
668
|
+
if not line:
|
|
604
669
|
continue
|
|
605
|
-
new_content_lines.append(
|
|
670
|
+
new_content_lines.append(line)
|
|
606
671
|
new_to_original_indices[len(new_content_lines) - 1] = i
|
|
607
672
|
content_lines = new_content_lines
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
#
|
|
613
|
-
|
|
673
|
+
|
|
674
|
+
# Prepare find lines, removing empty lines
|
|
675
|
+
find_lines = [line.strip() for line in find_lines if line.strip()]
|
|
676
|
+
|
|
677
|
+
# Initialize variables for best match tracking
|
|
678
|
+
max_similarity = 0.0
|
|
614
679
|
min_edit_distance_lines = []
|
|
680
|
+
context_lines = []
|
|
681
|
+
|
|
682
|
+
# For each possible starting position in content
|
|
615
683
|
for i in range(max(1, len(content_lines) - len(find_lines) + 1)):
|
|
616
|
-
|
|
684
|
+
# Calculate similarity for the block starting at position i
|
|
685
|
+
block_similarity = 0.0
|
|
617
686
|
for j in range(len(find_lines)):
|
|
618
687
|
if (i + j) < len(content_lines):
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
688
|
+
# Use SequenceMatcher for more efficient similarity calculation
|
|
689
|
+
similarity = SequenceMatcher(
|
|
690
|
+
None, content_lines[i + j], find_lines[j]
|
|
691
|
+
).ratio()
|
|
692
|
+
block_similarity += similarity
|
|
693
|
+
|
|
694
|
+
# If this block is more similar than previous best
|
|
695
|
+
if block_similarity > max_similarity:
|
|
696
|
+
max_similarity = block_similarity
|
|
697
|
+
# Map back to original line indices
|
|
624
698
|
orig_start_index = new_to_original_indices[i]
|
|
625
699
|
orig_end_index = (
|
|
626
700
|
new_to_original_indices.get(
|
|
@@ -628,34 +702,79 @@ def find_least_edit_distance_substring(
|
|
|
628
702
|
)
|
|
629
703
|
+ 1
|
|
630
704
|
)
|
|
705
|
+
# Get the original lines
|
|
631
706
|
min_edit_distance_lines = orig_content_lines[
|
|
707
|
+
orig_start_index:orig_end_index
|
|
708
|
+
]
|
|
709
|
+
# Get context (10 lines before and after)
|
|
710
|
+
context_lines = orig_content_lines[
|
|
632
711
|
max(0, orig_start_index - 10) : (orig_end_index + 10)
|
|
633
712
|
]
|
|
634
|
-
return "\n".join(min_edit_distance_lines), min_edit_distance
|
|
635
713
|
|
|
714
|
+
return (
|
|
715
|
+
min_edit_distance_lines,
|
|
716
|
+
"\n".join(context_lines),
|
|
717
|
+
)
|
|
636
718
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
719
|
+
|
|
720
|
+
def lines_replacer(
|
|
721
|
+
orig_content_lines: list[str], search_lines: list[str], replace_lines: list[str]
|
|
722
|
+
) -> str:
|
|
723
|
+
# Validation for empty search
|
|
724
|
+
search_lines = list(filter(None, [x.strip() for x in search_lines]))
|
|
725
|
+
|
|
726
|
+
# Create mapping of non-empty lines to original indices
|
|
727
|
+
new_to_original_indices = []
|
|
728
|
+
new_content_lines = []
|
|
729
|
+
for i, line in enumerate(orig_content_lines):
|
|
730
|
+
stripped = line.strip()
|
|
731
|
+
if not stripped:
|
|
732
|
+
continue
|
|
733
|
+
new_content_lines.append(stripped)
|
|
734
|
+
new_to_original_indices.append(i)
|
|
735
|
+
|
|
736
|
+
if not new_content_lines and not search_lines:
|
|
737
|
+
return "\n".join(replace_lines)
|
|
738
|
+
elif not search_lines:
|
|
739
|
+
raise ValueError("Search block is empty")
|
|
740
|
+
elif not new_content_lines:
|
|
741
|
+
raise ValueError("File content is empty")
|
|
742
|
+
|
|
743
|
+
# Search for matching block
|
|
744
|
+
for i in range(len(new_content_lines) - len(search_lines) + 1):
|
|
745
|
+
if all(
|
|
746
|
+
new_content_lines[i + j] == search_lines[j]
|
|
747
|
+
for j in range(len(search_lines))
|
|
748
|
+
):
|
|
749
|
+
start_idx = new_to_original_indices[i]
|
|
750
|
+
end_idx = new_to_original_indices[i + len(search_lines) - 1] + 1
|
|
751
|
+
return "\n".join(
|
|
752
|
+
orig_content_lines[:start_idx]
|
|
753
|
+
+ replace_lines
|
|
754
|
+
+ orig_content_lines[end_idx:]
|
|
648
755
|
)
|
|
649
|
-
raise Exception(
|
|
650
|
-
f"""Error: no match found for the provided search block.
|
|
651
|
-
Requested search block: \n```\n{find_lines}\n```
|
|
652
|
-
Possible relevant section in the file:\n---\n```\n{closest_match}\n```\n---\nFile not edited
|
|
653
|
-
\nPlease retry with exact search. Re-read the file if unsure.
|
|
654
|
-
"""
|
|
655
|
-
)
|
|
656
756
|
|
|
657
|
-
|
|
658
|
-
|
|
757
|
+
raise ValueError("Search block not found in content")
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def edit_content(content: str, find_lines: str, replace_with_lines: str) -> str:
|
|
761
|
+
replace_with_lines_ = replace_with_lines.split("\n")
|
|
762
|
+
find_lines_ = find_lines.split("\n")
|
|
763
|
+
content_lines_ = content.split("\n")
|
|
764
|
+
try:
|
|
765
|
+
return lines_replacer(content_lines_, find_lines_, replace_with_lines_)
|
|
766
|
+
except ValueError:
|
|
767
|
+
pass
|
|
768
|
+
|
|
769
|
+
_, context_lines = find_least_edit_distance_substring(content_lines_, find_lines_)
|
|
770
|
+
|
|
771
|
+
raise Exception(
|
|
772
|
+
f"""Error: no match found for the provided search block.
|
|
773
|
+
Requested search block: \n```\n{find_lines}\n```
|
|
774
|
+
Possible relevant section in the file:\n---\n```\n{context_lines}\n```\n---\nFile not edited
|
|
775
|
+
\nPlease retry with exact search. Re-read the file if unsure.
|
|
776
|
+
"""
|
|
777
|
+
)
|
|
659
778
|
|
|
660
779
|
|
|
661
780
|
def do_diff_edit(fedit: FileEdit) -> str:
|
|
@@ -686,6 +805,9 @@ def _do_diff_edit(fedit: FileEdit) -> str:
|
|
|
686
805
|
else:
|
|
687
806
|
path_ = fedit.file_path
|
|
688
807
|
|
|
808
|
+
# The LLM is now aware that the file exists
|
|
809
|
+
BASH_STATE.add_to_whitelist_for_overwrite(path_)
|
|
810
|
+
|
|
689
811
|
if not BASH_STATE.is_in_docker:
|
|
690
812
|
if not os.path.exists(path_):
|
|
691
813
|
raise Exception(f"Error: file {path_} does not exist")
|
|
@@ -766,6 +888,22 @@ def _do_diff_edit(fedit: FileEdit) -> str:
|
|
|
766
888
|
if rcode != 0:
|
|
767
889
|
raise Exception(f"Error: Write failed with code {rcode}")
|
|
768
890
|
|
|
891
|
+
syntax_errors = ""
|
|
892
|
+
extension = Path(path_).suffix.lstrip(".")
|
|
893
|
+
try:
|
|
894
|
+
check = check_syntax(extension, apply_diff_to)
|
|
895
|
+
syntax_errors = check.description
|
|
896
|
+
if syntax_errors:
|
|
897
|
+
console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
898
|
+
return f"""Wrote file succesfully.
|
|
899
|
+
---
|
|
900
|
+
However, tree-sitter reported syntax errors, please re-read the file and fix if there are any errors.
|
|
901
|
+
Errors:
|
|
902
|
+
{syntax_errors}
|
|
903
|
+
"""
|
|
904
|
+
except Exception:
|
|
905
|
+
pass
|
|
906
|
+
|
|
769
907
|
return "Success"
|
|
770
908
|
|
|
771
909
|
|
|
@@ -876,7 +1014,7 @@ def get_tool_output(
|
|
|
876
1014
|
output = ask_confirmation(arg), 0.0
|
|
877
1015
|
elif isinstance(arg, (BashCommand | BashInteraction)):
|
|
878
1016
|
console.print("Calling execute bash tool")
|
|
879
|
-
output = execute_bash(enc, arg, max_tokens,
|
|
1017
|
+
output = execute_bash(enc, arg, max_tokens, arg.wait_for_seconds)
|
|
880
1018
|
elif isinstance(arg, WriteIfEmpty):
|
|
881
1019
|
console.print("Calling write file tool")
|
|
882
1020
|
output = write_file(arg, True), 0
|
|
@@ -1045,6 +1183,8 @@ def read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
|
|
|
1045
1183
|
if not os.path.isabs(readfile.file_path):
|
|
1046
1184
|
return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
|
|
1047
1185
|
|
|
1186
|
+
BASH_STATE.add_to_whitelist_for_overwrite(readfile.file_path)
|
|
1187
|
+
|
|
1048
1188
|
if not BASH_STATE.is_in_docker:
|
|
1049
1189
|
path = Path(readfile.file_path)
|
|
1050
1190
|
if not path.exists():
|
|
@@ -1066,7 +1206,14 @@ def read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
|
|
|
1066
1206
|
if max_tokens is not None:
|
|
1067
1207
|
tokens = default_enc.encode(content)
|
|
1068
1208
|
if len(tokens) > max_tokens:
|
|
1069
|
-
content =
|
|
1070
|
-
|
|
1209
|
+
content, rest = save_out_of_context(
|
|
1210
|
+
tokens,
|
|
1211
|
+
max_tokens - 100,
|
|
1212
|
+
Path(readfile.file_path).suffix,
|
|
1213
|
+
default_enc.decode,
|
|
1214
|
+
)
|
|
1215
|
+
if rest:
|
|
1216
|
+
rest_ = "\n".join(map(str, rest))
|
|
1217
|
+
content += f"\n(...truncated)\n---\nI've split the rest of the file into multiple files. Here are the remaining splits, please read them:\n{rest_}"
|
|
1071
1218
|
|
|
1072
1219
|
return content
|
|
@@ -5,6 +5,7 @@ from pydantic import BaseModel
|
|
|
5
5
|
|
|
6
6
|
class BashCommand(BaseModel):
|
|
7
7
|
command: str
|
|
8
|
+
wait_for_seconds: Optional[int] = None
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
Specials = Literal[
|
|
@@ -17,6 +18,7 @@ class BashInteraction(BaseModel):
|
|
|
17
18
|
send_text: Optional[str] = None
|
|
18
19
|
send_specials: Optional[Sequence[Specials]] = None
|
|
19
20
|
send_ascii: Optional[Sequence[int]] = None
|
|
21
|
+
wait_for_seconds: Optional[int] = None
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class ReadImage(BaseModel):
|