wcgw 2.4.3__py3-none-any.whl → 2.6.1__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.
Potentially problematic release.
This version of wcgw might be problematic. Click here for more details.
- wcgw/client/anthropic_client.py +7 -17
- wcgw/client/common.py +3 -1
- wcgw/client/mcp_server/server.py +41 -36
- wcgw/client/openai_client.py +21 -36
- wcgw/client/openai_utils.py +5 -5
- wcgw/client/repo_ops/display_tree.py +127 -0
- wcgw/client/repo_ops/path_prob.py +58 -0
- wcgw/client/repo_ops/paths_model.vocab +20000 -0
- wcgw/client/repo_ops/paths_tokens.model +80042 -0
- wcgw/client/repo_ops/repo_context.py +148 -0
- wcgw/client/tools.py +220 -115
- wcgw/relay/serve.py +3 -3
- wcgw/types_.py +6 -4
- {wcgw-2.4.3.dist-info → wcgw-2.6.1.dist-info}/METADATA +19 -56
- {wcgw-2.4.3.dist-info → wcgw-2.6.1.dist-info}/RECORD +18 -12
- wcgw-2.6.1.dist-info/licenses/LICENSE +213 -0
- {wcgw-2.4.3.dist-info → wcgw-2.6.1.dist-info}/WHEEL +0 -0
- {wcgw-2.4.3.dist-info → wcgw-2.6.1.dist-info}/entry_points.txt +0 -0
wcgw/client/tools.py
CHANGED
|
@@ -1,61 +1,57 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import datetime
|
|
3
|
+
import importlib.metadata
|
|
3
4
|
import json
|
|
4
5
|
import mimetypes
|
|
5
|
-
|
|
6
|
+
import os
|
|
6
7
|
import re
|
|
7
8
|
import shlex
|
|
8
|
-
import importlib.metadata
|
|
9
9
|
import time
|
|
10
10
|
import traceback
|
|
11
|
+
import uuid
|
|
12
|
+
from difflib import SequenceMatcher
|
|
13
|
+
from pathlib import Path
|
|
11
14
|
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
12
15
|
from typing import (
|
|
13
16
|
Callable,
|
|
14
|
-
DefaultDict,
|
|
15
17
|
Literal,
|
|
16
18
|
Optional,
|
|
17
19
|
ParamSpec,
|
|
18
20
|
Type,
|
|
19
21
|
TypeVar,
|
|
20
22
|
)
|
|
21
|
-
import uuid
|
|
22
|
-
import humanize
|
|
23
|
-
from pydantic import BaseModel, TypeAdapter
|
|
24
|
-
import typer
|
|
25
|
-
from .computer_use import run_computer_tool
|
|
26
|
-
from websockets.sync.client import connect as syncconnect
|
|
27
23
|
|
|
28
|
-
import os
|
|
29
|
-
import tiktoken
|
|
30
24
|
import pexpect
|
|
31
|
-
from typer import Typer
|
|
32
|
-
import websockets
|
|
33
|
-
|
|
34
|
-
import rich
|
|
35
25
|
import pyte
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
import rich
|
|
27
|
+
import tokenizers # type: ignore
|
|
28
|
+
import typer
|
|
29
|
+
import websockets
|
|
38
30
|
from openai.types.chat import (
|
|
39
31
|
ChatCompletionMessageParam,
|
|
40
32
|
)
|
|
41
|
-
from
|
|
33
|
+
from pydantic import BaseModel, TypeAdapter
|
|
34
|
+
from syntax_checker import check_syntax
|
|
35
|
+
from typer import Typer
|
|
36
|
+
from websockets.sync.client import connect as syncconnect
|
|
42
37
|
|
|
43
38
|
from ..types_ import (
|
|
44
39
|
BashCommand,
|
|
45
40
|
BashInteraction,
|
|
46
|
-
WriteIfEmpty,
|
|
47
|
-
FileEditFindReplace,
|
|
48
41
|
FileEdit,
|
|
42
|
+
FileEditFindReplace,
|
|
43
|
+
GetScreenInfo,
|
|
49
44
|
Initialize,
|
|
50
|
-
|
|
45
|
+
Keyboard,
|
|
46
|
+
Mouse,
|
|
47
|
+
ReadFiles,
|
|
51
48
|
ReadImage,
|
|
52
49
|
ResetShell,
|
|
53
|
-
Mouse,
|
|
54
|
-
Keyboard,
|
|
55
50
|
ScreenShot,
|
|
56
|
-
|
|
51
|
+
WriteIfEmpty,
|
|
57
52
|
)
|
|
58
|
-
|
|
53
|
+
from .computer_use import run_computer_tool
|
|
54
|
+
from .repo_ops.repo_context import get_repo_context
|
|
59
55
|
from .sys_utils import command_run
|
|
60
56
|
|
|
61
57
|
|
|
@@ -178,19 +174,23 @@ def _ensure_env_and_bg_jobs(shell: pexpect.spawn) -> Optional[int]: # type: ign
|
|
|
178
174
|
shell.expect(PROMPT, timeout=0.2)
|
|
179
175
|
shell.sendline("jobs | wc -l")
|
|
180
176
|
before = ""
|
|
177
|
+
|
|
181
178
|
while not _is_int(before): # Consume all previous output
|
|
182
179
|
try:
|
|
183
180
|
shell.expect(PROMPT, timeout=0.2)
|
|
184
181
|
except pexpect.TIMEOUT:
|
|
185
182
|
console.print(f"Couldn't get exit code, before: {before}")
|
|
186
183
|
raise
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
|
|
185
|
+
before_val = shell.before
|
|
186
|
+
if not isinstance(before_val, str):
|
|
187
|
+
before_val = str(before_val)
|
|
188
|
+
assert isinstance(before_val, str)
|
|
189
|
+
before_lines = render_terminal_output(before_val)
|
|
190
190
|
before = "\n".join(before_lines).strip()
|
|
191
191
|
|
|
192
192
|
try:
|
|
193
|
-
return int(
|
|
193
|
+
return int(before)
|
|
194
194
|
except ValueError:
|
|
195
195
|
raise ValueError(f"Malformed output: {before}")
|
|
196
196
|
|
|
@@ -244,10 +244,12 @@ class BashState:
|
|
|
244
244
|
return self._cwd
|
|
245
245
|
|
|
246
246
|
def update_cwd(self) -> str:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
self.shell.sendline("pwd")
|
|
248
|
+
self.shell.expect(PROMPT, timeout=0.2)
|
|
249
|
+
before_val = self.shell.before
|
|
250
|
+
if not isinstance(before_val, str):
|
|
251
|
+
before_val = str(before_val)
|
|
252
|
+
before_lines = render_terminal_output(before_val)
|
|
251
253
|
current_dir = "\n".join(before_lines).strip()
|
|
252
254
|
self._cwd = current_dir
|
|
253
255
|
return current_dir
|
|
@@ -259,9 +261,17 @@ class BashState:
|
|
|
259
261
|
def get_pending_for(self) -> str:
|
|
260
262
|
if isinstance(self._state, datetime.datetime):
|
|
261
263
|
timedelta = datetime.datetime.now() - self._state
|
|
262
|
-
return
|
|
263
|
-
|
|
264
|
+
return (
|
|
265
|
+
str(
|
|
266
|
+
int(
|
|
267
|
+
(
|
|
268
|
+
timedelta + datetime.timedelta(seconds=TIMEOUT)
|
|
269
|
+
).total_seconds()
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
+ " seconds"
|
|
264
273
|
)
|
|
274
|
+
|
|
265
275
|
return "Not pending"
|
|
266
276
|
|
|
267
277
|
@property
|
|
@@ -279,16 +289,46 @@ class BashState:
|
|
|
279
289
|
BASH_STATE = BashState()
|
|
280
290
|
|
|
281
291
|
|
|
282
|
-
def
|
|
292
|
+
def initialize(
|
|
293
|
+
any_workspace_path: str, read_files_: list[str], max_tokens: Optional[int]
|
|
294
|
+
) -> str:
|
|
295
|
+
reset_shell()
|
|
296
|
+
|
|
297
|
+
repo_context = ""
|
|
298
|
+
|
|
299
|
+
if any_workspace_path:
|
|
300
|
+
if os.path.exists(any_workspace_path):
|
|
301
|
+
repo_context, folder_to_start = get_repo_context(any_workspace_path, 200)
|
|
302
|
+
|
|
303
|
+
BASH_STATE.shell.sendline(f"cd {shlex.quote(str(folder_to_start))}")
|
|
304
|
+
BASH_STATE.shell.expect(PROMPT, timeout=0.2)
|
|
305
|
+
BASH_STATE.update_cwd()
|
|
306
|
+
|
|
307
|
+
repo_context = f"---\n# Workspace structure\n{repo_context}\n---\n"
|
|
308
|
+
else:
|
|
309
|
+
return f"\nInfo: Workspace path {any_workspace_path} does not exist\n"
|
|
310
|
+
|
|
311
|
+
initial_files_context = ""
|
|
312
|
+
if read_files_:
|
|
313
|
+
initial_files = read_files(read_files_, max_tokens)
|
|
314
|
+
initial_files_context = f"---\n# Requested files\n{initial_files}\n---\n"
|
|
315
|
+
|
|
283
316
|
uname_sysname = os.uname().sysname
|
|
284
317
|
uname_machine = os.uname().machine
|
|
285
|
-
|
|
318
|
+
|
|
319
|
+
output = f"""
|
|
320
|
+
# Environment
|
|
286
321
|
System: {uname_sysname}
|
|
287
322
|
Machine: {uname_machine}
|
|
288
323
|
Current working directory: {BASH_STATE.cwd}
|
|
289
|
-
|
|
324
|
+
|
|
325
|
+
{repo_context}
|
|
326
|
+
|
|
327
|
+
{initial_files_context}
|
|
290
328
|
"""
|
|
291
329
|
|
|
330
|
+
return output
|
|
331
|
+
|
|
292
332
|
|
|
293
333
|
def reset_shell() -> str:
|
|
294
334
|
BASH_STATE.reset()
|
|
@@ -345,29 +385,11 @@ def get_status() -> str:
|
|
|
345
385
|
T = TypeVar("T")
|
|
346
386
|
|
|
347
387
|
|
|
348
|
-
def save_out_of_context(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
) -> tuple[str, list[Path]]:
|
|
354
|
-
file_contents = list[str]()
|
|
355
|
-
for i in range(0, len(tokens), max_tokens):
|
|
356
|
-
file_contents.append(tokens_converted(tokens[i : i + max_tokens]))
|
|
357
|
-
|
|
358
|
-
if len(file_contents) == 1:
|
|
359
|
-
return file_contents[0], []
|
|
360
|
-
|
|
361
|
-
rest_paths = list[Path]()
|
|
362
|
-
for i, content in enumerate(file_contents):
|
|
363
|
-
if i == 0:
|
|
364
|
-
continue
|
|
365
|
-
file_path = NamedTemporaryFile(delete=False, suffix=suffix).name
|
|
366
|
-
with open(file_path, "w") as f:
|
|
367
|
-
f.write(content)
|
|
368
|
-
rest_paths.append(Path(file_path))
|
|
369
|
-
|
|
370
|
-
return file_contents[0], rest_paths
|
|
388
|
+
def save_out_of_context(content: str, suffix: str) -> str:
|
|
389
|
+
file_path = NamedTemporaryFile(delete=False, suffix=suffix).name
|
|
390
|
+
with open(file_path, "w") as f:
|
|
391
|
+
f.write(content)
|
|
392
|
+
return file_path
|
|
371
393
|
|
|
372
394
|
|
|
373
395
|
def rstrip(lines: list[str]) -> str:
|
|
@@ -404,7 +426,7 @@ def is_status_check(arg: BashInteraction | BashCommand) -> bool:
|
|
|
404
426
|
|
|
405
427
|
|
|
406
428
|
def execute_bash(
|
|
407
|
-
enc:
|
|
429
|
+
enc: tokenizers.Tokenizer,
|
|
408
430
|
bash_arg: BashCommand | BashInteraction,
|
|
409
431
|
max_tokens: Optional[int],
|
|
410
432
|
timeout_s: Optional[float],
|
|
@@ -549,7 +571,7 @@ def execute_bash(
|
|
|
549
571
|
|
|
550
572
|
if max_tokens and len(tokens) >= max_tokens:
|
|
551
573
|
incremental_text = "(...truncated)\n" + enc.decode(
|
|
552
|
-
tokens[-(max_tokens - 1) :]
|
|
574
|
+
tokens.ids[-(max_tokens - 1) :]
|
|
553
575
|
)
|
|
554
576
|
|
|
555
577
|
if is_interrupt:
|
|
@@ -569,21 +591,20 @@ def execute_bash(
|
|
|
569
591
|
|
|
570
592
|
return incremental_text, 0
|
|
571
593
|
|
|
572
|
-
|
|
594
|
+
if not isinstance(BASH_STATE.shell.before, str):
|
|
595
|
+
BASH_STATE.shell.before = str(BASH_STATE.shell.before)
|
|
596
|
+
|
|
573
597
|
output = _incremental_text(BASH_STATE.shell.before, BASH_STATE.pending_output)
|
|
574
598
|
BASH_STATE.set_repl()
|
|
575
599
|
|
|
576
|
-
if is_interrupt:
|
|
577
|
-
return "Interrupt successful", 0.0
|
|
578
|
-
|
|
579
600
|
tokens = enc.encode(output)
|
|
580
601
|
if max_tokens and len(tokens) >= max_tokens:
|
|
581
|
-
output = "(...truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
|
|
602
|
+
output = "(...truncated)\n" + enc.decode(tokens.ids[-(max_tokens - 1) :])
|
|
582
603
|
|
|
583
604
|
try:
|
|
584
605
|
exit_status = get_status()
|
|
585
606
|
output += exit_status
|
|
586
|
-
except ValueError
|
|
607
|
+
except ValueError:
|
|
587
608
|
console.print(output)
|
|
588
609
|
console.print(traceback.format_exc())
|
|
589
610
|
console.print("Malformed output, restarting shell", style="red")
|
|
@@ -638,6 +659,19 @@ def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
|
|
|
638
659
|
return wrapper
|
|
639
660
|
|
|
640
661
|
|
|
662
|
+
def truncate_if_over(content: str, max_tokens: Optional[int]) -> str:
|
|
663
|
+
if max_tokens and max_tokens > 0:
|
|
664
|
+
tokens = default_enc.encode(content)
|
|
665
|
+
n_tokens = len(tokens)
|
|
666
|
+
if n_tokens > max_tokens:
|
|
667
|
+
content = (
|
|
668
|
+
default_enc.decode(tokens.ids[: max(0, max_tokens - 100)])
|
|
669
|
+
+ "\n(...truncated)"
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
return content
|
|
673
|
+
|
|
674
|
+
|
|
641
675
|
def read_image_from_shell(file_path: str) -> ImageData:
|
|
642
676
|
if not os.path.isabs(file_path):
|
|
643
677
|
file_path = os.path.join(BASH_STATE.cwd, file_path)
|
|
@@ -666,7 +700,25 @@ def read_image_from_shell(file_path: str) -> ImageData:
|
|
|
666
700
|
return ImageData(media_type=image_type, data=image_b64) # type: ignore
|
|
667
701
|
|
|
668
702
|
|
|
669
|
-
def
|
|
703
|
+
def get_context_for_errors(
|
|
704
|
+
errors: list[tuple[int, int]], file_content: str, max_tokens: Optional[int]
|
|
705
|
+
) -> str:
|
|
706
|
+
file_lines = file_content.split("\n")
|
|
707
|
+
min_line_num = max(0, min([error[0] for error in errors]) - 10)
|
|
708
|
+
max_line_num = min(len(file_lines), max([error[0] for error in errors]) + 10)
|
|
709
|
+
context_lines = file_lines[min_line_num:max_line_num]
|
|
710
|
+
context = "\n".join(context_lines)
|
|
711
|
+
|
|
712
|
+
if max_tokens is not None and max_tokens > 0:
|
|
713
|
+
ntokens = len(default_enc.encode(context))
|
|
714
|
+
if ntokens > max_tokens:
|
|
715
|
+
return "Please re-read the file to understand the context"
|
|
716
|
+
return f"Here's relevant snippet from the file where the syntax errors occured:\n```\n{context}\n```"
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def write_file(
|
|
720
|
+
writefile: WriteIfEmpty, error_on_exist: bool, max_tokens: Optional[int]
|
|
721
|
+
) -> str:
|
|
670
722
|
if not os.path.isabs(writefile.file_path):
|
|
671
723
|
return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
|
|
672
724
|
else:
|
|
@@ -678,9 +730,14 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
|
|
|
678
730
|
if (error_on_exist or error_on_exist_) and os.path.exists(path_):
|
|
679
731
|
content = Path(path_).read_text().strip()
|
|
680
732
|
if content:
|
|
733
|
+
content = truncate_if_over(content, max_tokens)
|
|
734
|
+
|
|
681
735
|
if error_on_exist_:
|
|
682
|
-
return
|
|
683
|
-
|
|
736
|
+
return (
|
|
737
|
+
f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
738
|
+
+ f"\nHere's the existing content:\n```\n{content}\n```"
|
|
739
|
+
)
|
|
740
|
+
else:
|
|
684
741
|
add_overwrite_warning = content
|
|
685
742
|
|
|
686
743
|
# Since we've already errored once, add this to whitelist
|
|
@@ -701,8 +758,13 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
|
|
|
701
758
|
timeout=TIMEOUT,
|
|
702
759
|
)
|
|
703
760
|
if return_code != 0 and content.strip():
|
|
761
|
+
content = truncate_if_over(content, max_tokens)
|
|
762
|
+
|
|
704
763
|
if error_on_exist_:
|
|
705
|
-
return
|
|
764
|
+
return (
|
|
765
|
+
f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
766
|
+
+ f"\nHere's the existing content:\n```\n{content}\n```"
|
|
767
|
+
)
|
|
706
768
|
else:
|
|
707
769
|
add_overwrite_warning = content
|
|
708
770
|
|
|
@@ -735,13 +797,19 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
|
|
|
735
797
|
try:
|
|
736
798
|
check = check_syntax(extension, writefile.file_content)
|
|
737
799
|
syntax_errors = check.description
|
|
800
|
+
|
|
738
801
|
if syntax_errors:
|
|
802
|
+
context_for_errors = get_context_for_errors(
|
|
803
|
+
check.errors, writefile.file_content, max_tokens
|
|
804
|
+
)
|
|
739
805
|
console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
740
806
|
warnings.append(f"""
|
|
741
807
|
---
|
|
742
|
-
Warning: tree-sitter reported syntax errors
|
|
743
|
-
|
|
808
|
+
Warning: tree-sitter reported syntax errors
|
|
809
|
+
Syntax errors:
|
|
744
810
|
{syntax_errors}
|
|
811
|
+
|
|
812
|
+
{context_for_errors}
|
|
745
813
|
---
|
|
746
814
|
""")
|
|
747
815
|
|
|
@@ -751,8 +819,10 @@ Errors:
|
|
|
751
819
|
if add_overwrite_warning:
|
|
752
820
|
warnings.append(
|
|
753
821
|
"\n---\nWarning: a file already existed and it's now overwritten. Was it a mistake? If yes please revert your action."
|
|
754
|
-
"Here's the previous content:\n```\n" + add_overwrite_warning + "\n```"
|
|
755
822
|
"\n---\n"
|
|
823
|
+
+ "Here's the previous content:\n```\n"
|
|
824
|
+
+ add_overwrite_warning
|
|
825
|
+
+ "\n```"
|
|
756
826
|
)
|
|
757
827
|
|
|
758
828
|
return "Success" + "".join(warnings)
|
|
@@ -878,9 +948,9 @@ def edit_content(content: str, find_lines: str, replace_with_lines: str) -> str:
|
|
|
878
948
|
)
|
|
879
949
|
|
|
880
950
|
|
|
881
|
-
def do_diff_edit(fedit: FileEdit) -> str:
|
|
951
|
+
def do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
882
952
|
try:
|
|
883
|
-
return _do_diff_edit(fedit)
|
|
953
|
+
return _do_diff_edit(fedit, max_tokens)
|
|
884
954
|
except Exception as e:
|
|
885
955
|
# Try replacing \"
|
|
886
956
|
try:
|
|
@@ -890,13 +960,13 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
890
960
|
'\\"', '"'
|
|
891
961
|
),
|
|
892
962
|
)
|
|
893
|
-
return _do_diff_edit(fedit)
|
|
963
|
+
return _do_diff_edit(fedit, max_tokens)
|
|
894
964
|
except Exception:
|
|
895
965
|
pass
|
|
896
966
|
raise e
|
|
897
967
|
|
|
898
968
|
|
|
899
|
-
def _do_diff_edit(fedit: FileEdit) -> str:
|
|
969
|
+
def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
900
970
|
console.log(f"Editing file: {fedit.file_path}")
|
|
901
971
|
|
|
902
972
|
if not os.path.isabs(fedit.file_path):
|
|
@@ -995,13 +1065,19 @@ def _do_diff_edit(fedit: FileEdit) -> str:
|
|
|
995
1065
|
check = check_syntax(extension, apply_diff_to)
|
|
996
1066
|
syntax_errors = check.description
|
|
997
1067
|
if syntax_errors:
|
|
1068
|
+
context_for_errors = get_context_for_errors(
|
|
1069
|
+
check.errors, apply_diff_to, max_tokens
|
|
1070
|
+
)
|
|
1071
|
+
|
|
998
1072
|
console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
999
1073
|
return f"""Wrote file succesfully.
|
|
1000
1074
|
---
|
|
1001
1075
|
However, tree-sitter reported syntax errors, please re-read the file and fix if there are any errors.
|
|
1002
|
-
|
|
1076
|
+
Syntax errors:
|
|
1003
1077
|
{syntax_errors}
|
|
1004
|
-
|
|
1078
|
+
|
|
1079
|
+
{context_for_errors}
|
|
1080
|
+
"""
|
|
1005
1081
|
except Exception:
|
|
1006
1082
|
pass
|
|
1007
1083
|
|
|
@@ -1041,7 +1117,7 @@ TOOLS = (
|
|
|
1041
1117
|
| AIAssistant
|
|
1042
1118
|
| DoneFlag
|
|
1043
1119
|
| ReadImage
|
|
1044
|
-
|
|
|
1120
|
+
| ReadFiles
|
|
1045
1121
|
| Initialize
|
|
1046
1122
|
| Mouse
|
|
1047
1123
|
| Keyboard
|
|
@@ -1076,8 +1152,8 @@ def which_tool_name(name: str) -> Type[TOOLS]:
|
|
|
1076
1152
|
return DoneFlag
|
|
1077
1153
|
elif name == "ReadImage":
|
|
1078
1154
|
return ReadImage
|
|
1079
|
-
elif name == "
|
|
1080
|
-
return
|
|
1155
|
+
elif name == "ReadFiles":
|
|
1156
|
+
return ReadFiles
|
|
1081
1157
|
elif name == "Initialize":
|
|
1082
1158
|
return Initialize
|
|
1083
1159
|
elif name == "Mouse":
|
|
@@ -1097,7 +1173,7 @@ TOOL_CALLS: list[TOOLS] = []
|
|
|
1097
1173
|
|
|
1098
1174
|
def get_tool_output(
|
|
1099
1175
|
args: dict[object, object] | TOOLS,
|
|
1100
|
-
enc:
|
|
1176
|
+
enc: tokenizers.Tokenizer,
|
|
1101
1177
|
limit: float,
|
|
1102
1178
|
loop_call: Callable[[str, float], tuple[str, float]],
|
|
1103
1179
|
max_tokens: Optional[int],
|
|
@@ -1118,10 +1194,10 @@ def get_tool_output(
|
|
|
1118
1194
|
output = execute_bash(enc, arg, max_tokens, arg.wait_for_seconds)
|
|
1119
1195
|
elif isinstance(arg, WriteIfEmpty):
|
|
1120
1196
|
console.print("Calling write file tool")
|
|
1121
|
-
output = write_file(arg, True), 0
|
|
1197
|
+
output = write_file(arg, True, max_tokens), 0
|
|
1122
1198
|
elif isinstance(arg, FileEdit):
|
|
1123
1199
|
console.print("Calling full file edit tool")
|
|
1124
|
-
output = do_diff_edit(arg), 0.0
|
|
1200
|
+
output = do_diff_edit(arg, max_tokens), 0.0
|
|
1125
1201
|
elif isinstance(arg, DoneFlag):
|
|
1126
1202
|
console.print("Calling mark finish tool")
|
|
1127
1203
|
output = mark_finish(arg), 0.0
|
|
@@ -1131,17 +1207,18 @@ def get_tool_output(
|
|
|
1131
1207
|
elif isinstance(arg, ReadImage):
|
|
1132
1208
|
console.print("Calling read image tool")
|
|
1133
1209
|
output = read_image_from_shell(arg.file_path), 0.0
|
|
1134
|
-
elif isinstance(arg,
|
|
1210
|
+
elif isinstance(arg, ReadFiles):
|
|
1135
1211
|
console.print("Calling read file tool")
|
|
1136
|
-
output =
|
|
1212
|
+
output = read_files(arg.file_paths, max_tokens), 0.0
|
|
1137
1213
|
elif isinstance(arg, ResetShell):
|
|
1138
1214
|
console.print("Calling reset shell tool")
|
|
1139
1215
|
output = reset_shell(), 0.0
|
|
1140
1216
|
elif isinstance(arg, Initialize):
|
|
1141
1217
|
console.print("Calling initial info tool")
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1218
|
+
output = (
|
|
1219
|
+
initialize(arg.any_workspace_path, arg.initial_files_to_read, max_tokens),
|
|
1220
|
+
0.0,
|
|
1221
|
+
)
|
|
1145
1222
|
elif isinstance(arg, (Mouse, Keyboard, ScreenShot, GetScreenInfo)):
|
|
1146
1223
|
console.print(f"Calling {type(arg).__name__} tool")
|
|
1147
1224
|
outputs_cost = run_computer_tool(arg), 0.0
|
|
@@ -1190,7 +1267,9 @@ def get_tool_output(
|
|
|
1190
1267
|
|
|
1191
1268
|
History = list[ChatCompletionMessageParam]
|
|
1192
1269
|
|
|
1193
|
-
default_enc =
|
|
1270
|
+
default_enc: tokenizers.Tokenizer = tokenizers.Tokenizer.from_pretrained(
|
|
1271
|
+
"Xenova/claude-tokenizer"
|
|
1272
|
+
)
|
|
1194
1273
|
curr_cost = 0.0
|
|
1195
1274
|
|
|
1196
1275
|
|
|
@@ -1203,7 +1282,7 @@ class Mdata(BaseModel):
|
|
|
1203
1282
|
| FileEditFindReplace
|
|
1204
1283
|
| FileEdit
|
|
1205
1284
|
| str
|
|
1206
|
-
|
|
|
1285
|
+
| ReadFiles
|
|
1207
1286
|
| Initialize
|
|
1208
1287
|
)
|
|
1209
1288
|
|
|
@@ -1276,43 +1355,69 @@ def app(
|
|
|
1276
1355
|
register_client(server_url, client_uuid or "")
|
|
1277
1356
|
|
|
1278
1357
|
|
|
1279
|
-
def
|
|
1280
|
-
|
|
1358
|
+
def read_files(file_paths: list[str], max_tokens: Optional[int]) -> str:
|
|
1359
|
+
message = ""
|
|
1360
|
+
for i, file in enumerate(file_paths):
|
|
1361
|
+
try:
|
|
1362
|
+
content, truncated, tokens = read_file(file, max_tokens)
|
|
1363
|
+
except Exception as e:
|
|
1364
|
+
message += f"\n{file}: {str(e)}\n"
|
|
1365
|
+
continue
|
|
1281
1366
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1367
|
+
if max_tokens:
|
|
1368
|
+
max_tokens = max_tokens - tokens
|
|
1284
1369
|
|
|
1285
|
-
|
|
1370
|
+
message += f"\n``` {file}\n{content}\n"
|
|
1371
|
+
|
|
1372
|
+
if truncated or (max_tokens and max_tokens <= 0):
|
|
1373
|
+
not_reading = file_paths[i + 1 :]
|
|
1374
|
+
if not_reading:
|
|
1375
|
+
message += f'\nNot reading the rest of the files: {", ".join(not_reading)} due to token limit, please call again'
|
|
1376
|
+
break
|
|
1377
|
+
else:
|
|
1378
|
+
message += "```"
|
|
1379
|
+
|
|
1380
|
+
return message
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
def read_file(file_path: str, max_tokens: Optional[int]) -> tuple[str, bool, int]:
|
|
1384
|
+
console.print(f"Reading file: {file_path}")
|
|
1385
|
+
|
|
1386
|
+
if not os.path.isabs(file_path):
|
|
1387
|
+
raise ValueError(
|
|
1388
|
+
f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
|
|
1389
|
+
)
|
|
1390
|
+
|
|
1391
|
+
BASH_STATE.add_to_whitelist_for_overwrite(file_path)
|
|
1286
1392
|
|
|
1287
1393
|
if not BASH_STATE.is_in_docker:
|
|
1288
|
-
path = Path(
|
|
1394
|
+
path = Path(file_path)
|
|
1289
1395
|
if not path.exists():
|
|
1290
|
-
|
|
1396
|
+
raise ValueError(f"Error: file {file_path} does not exist")
|
|
1291
1397
|
|
|
1292
1398
|
with path.open("r") as f:
|
|
1293
|
-
content = f.read()
|
|
1399
|
+
content = f.read(10_000_000)
|
|
1294
1400
|
|
|
1295
1401
|
else:
|
|
1296
1402
|
return_code, content, stderr = command_run(
|
|
1297
|
-
f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(
|
|
1403
|
+
f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(file_path)}",
|
|
1298
1404
|
timeout=TIMEOUT,
|
|
1299
1405
|
)
|
|
1300
1406
|
if return_code != 0:
|
|
1301
1407
|
raise Exception(
|
|
1302
|
-
f"Error: cat {
|
|
1408
|
+
f"Error: cat {file_path} failed with code {return_code}\nstdout: {content}\nstderr: {stderr}"
|
|
1303
1409
|
)
|
|
1304
1410
|
|
|
1411
|
+
truncated = False
|
|
1412
|
+
tokens_counts = 0
|
|
1305
1413
|
if max_tokens is not None:
|
|
1306
1414
|
tokens = default_enc.encode(content)
|
|
1415
|
+
tokens_counts = len(tokens)
|
|
1307
1416
|
if len(tokens) > max_tokens:
|
|
1308
|
-
content
|
|
1309
|
-
|
|
1310
|
-
max_tokens
|
|
1311
|
-
Path(readfile.file_path).suffix,
|
|
1312
|
-
default_enc.decode,
|
|
1417
|
+
content = default_enc.decode(tokens.ids[:max_tokens])
|
|
1418
|
+
rest = save_out_of_context(
|
|
1419
|
+
default_enc.decode(tokens.ids[max_tokens:]), Path(file_path).suffix
|
|
1313
1420
|
)
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
return content
|
|
1421
|
+
content += f"\n(...truncated)\n---\nI've saved the continuation in a new file. Please read: `{rest}`"
|
|
1422
|
+
truncated = True
|
|
1423
|
+
return content, truncated, tokens_counts
|
wcgw/relay/serve.py
CHANGED
|
@@ -21,7 +21,7 @@ from ..types_ import (
|
|
|
21
21
|
FileEditFindReplace,
|
|
22
22
|
FileEdit,
|
|
23
23
|
Initialize,
|
|
24
|
-
|
|
24
|
+
ReadFiles,
|
|
25
25
|
ResetShell,
|
|
26
26
|
Specials,
|
|
27
27
|
)
|
|
@@ -35,7 +35,7 @@ class Mdata(BaseModel):
|
|
|
35
35
|
| ResetShell
|
|
36
36
|
| FileEditFindReplace
|
|
37
37
|
| FileEdit
|
|
38
|
-
|
|
|
38
|
+
| ReadFiles
|
|
39
39
|
| Initialize
|
|
40
40
|
| str
|
|
41
41
|
)
|
|
@@ -259,7 +259,7 @@ async def bash_interaction(bash_interaction: BashInteractionWithUUID) -> str:
|
|
|
259
259
|
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
260
260
|
|
|
261
261
|
|
|
262
|
-
class ReadFileWithUUID(
|
|
262
|
+
class ReadFileWithUUID(ReadFiles):
|
|
263
263
|
user_id: UUID
|
|
264
264
|
|
|
265
265
|
|
wcgw/types_.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import re
|
|
2
1
|
from typing import Literal, Optional, Sequence
|
|
2
|
+
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
5
|
|
|
@@ -31,9 +31,9 @@ class WriteIfEmpty(BaseModel):
|
|
|
31
31
|
file_content: str
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class
|
|
35
|
-
|
|
36
|
-
type: Literal["
|
|
34
|
+
class ReadFiles(BaseModel):
|
|
35
|
+
file_paths: list[str]
|
|
36
|
+
type: Literal["ReadFiles"]
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
class FileEditFindReplace(BaseModel):
|
|
@@ -53,6 +53,8 @@ class FileEdit(BaseModel):
|
|
|
53
53
|
|
|
54
54
|
class Initialize(BaseModel):
|
|
55
55
|
type: Literal["Initialize"]
|
|
56
|
+
any_workspace_path: str
|
|
57
|
+
initial_files_to_read: list[str]
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
class GetScreenInfo(BaseModel):
|