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/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
- from pathlib import Path
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
- from syntax_checker import check_syntax
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 difflib import SequenceMatcher
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
- ReadFile,
45
+ Keyboard,
46
+ Mouse,
47
+ ReadFiles,
51
48
  ReadImage,
52
49
  ResetShell,
53
- Mouse,
54
- Keyboard,
55
50
  ScreenShot,
56
- GetScreenInfo,
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
- assert isinstance(shell.before, str)
188
- # Render because there could be some anscii escape sequences still set like in google colab env
189
- before_lines = render_terminal_output(shell.before)
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((before))
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
- BASH_STATE.shell.sendline("pwd")
248
- BASH_STATE.shell.expect(PROMPT, timeout=0.2)
249
- assert isinstance(BASH_STATE.shell.before, str)
250
- before_lines = render_terminal_output(BASH_STATE.shell.before)
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 humanize.naturaldelta(
263
- timedelta + datetime.timedelta(seconds=TIMEOUT)
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 initial_info() -> str:
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
- return f"""
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
- wcgw version: {importlib.metadata.version("wcgw")}
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
- tokens: list[T],
350
- max_tokens: int,
351
- suffix: str,
352
- tokens_converted: Callable[[list[T]], str],
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: tiktoken.Encoding,
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
- assert isinstance(BASH_STATE.shell.before, str)
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 as e:
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 write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
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 f"Error: can't write to existing file {path_}, use other functions to edit the file"
683
- elif error_on_exist:
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 f"Error: can't write to existing file {path_}, use other functions to edit the file"
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, please re-read the file and fix if any errors.
743
- Errors:
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
- Errors:
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
- | ReadFile
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 == "ReadFile":
1080
- return ReadFile
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: tiktoken.Encoding,
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, ReadFile):
1210
+ elif isinstance(arg, ReadFiles):
1135
1211
  console.print("Calling read file tool")
1136
- output = read_file(arg, max_tokens), 0.0
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
- # First force reset
1143
- reset_shell()
1144
- output = initial_info(), 0.0
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 = tiktoken.encoding_for_model("gpt-4o")
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
- | ReadFile
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 read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
1280
- console.print(f"Reading file: {readfile.file_path}")
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
- if not os.path.isabs(readfile.file_path):
1283
- return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
1367
+ if max_tokens:
1368
+ max_tokens = max_tokens - tokens
1284
1369
 
1285
- BASH_STATE.add_to_whitelist_for_overwrite(readfile.file_path)
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(readfile.file_path)
1394
+ path = Path(file_path)
1289
1395
  if not path.exists():
1290
- return f"Error: file {readfile.file_path} does not exist"
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(readfile.file_path)}",
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 {readfile.file_path} failed with code {return_code}\nstdout: {content}\nstderr: {stderr}"
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, rest = save_out_of_context(
1309
- tokens,
1310
- max_tokens - 100,
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
- if rest:
1315
- rest_ = "\n".join(map(str, rest))
1316
- 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_}"
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
- ReadFile,
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
- | ReadFile
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(ReadFile):
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 ReadFile(BaseModel):
35
- file_path: str # The path to the file to read
36
- type: Literal["ReadFile"]
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):