wcgw 2.8.10__py3-none-any.whl → 3.0.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/__init__.py +1 -1
- wcgw/client/bash_state/bash_state.py +788 -0
- wcgw/client/encoder/__init__.py +47 -0
- wcgw/client/file_ops/diff_edit.py +52 -1
- wcgw/client/mcp_server/Readme.md +2 -88
- wcgw/client/mcp_server/__init__.py +2 -2
- wcgw/client/mcp_server/server.py +54 -214
- wcgw/client/modes.py +16 -21
- wcgw/client/repo_ops/display_tree.py +1 -12
- wcgw/client/tool_prompts.py +99 -0
- wcgw/client/tools.py +286 -1086
- wcgw/py.typed +0 -0
- wcgw/relay/client.py +95 -0
- wcgw/relay/serve.py +4 -69
- wcgw/types_.py +57 -63
- {wcgw-2.8.10.dist-info → wcgw-3.0.1.dist-info}/METADATA +4 -3
- {wcgw-2.8.10.dist-info → wcgw-3.0.1.dist-info}/RECORD +23 -20
- wcgw_cli/anthropic_client.py +269 -366
- wcgw_cli/cli.py +0 -2
- wcgw_cli/openai_client.py +224 -280
- wcgw/client/computer_use.py +0 -435
- wcgw/client/sys_utils.py +0 -41
- {wcgw-2.8.10.dist-info → wcgw-3.0.1.dist-info}/WHEEL +0 -0
- {wcgw-2.8.10.dist-info → wcgw-3.0.1.dist-info}/entry_points.txt +0 -0
- {wcgw-2.8.10.dist-info → wcgw-3.0.1.dist-info}/licenses/LICENSE +0 -0
wcgw/client/tools.py
CHANGED
|
@@ -1,489 +1,121 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
import datetime
|
|
3
2
|
import fnmatch
|
|
4
3
|
import glob
|
|
5
|
-
import importlib.metadata
|
|
6
4
|
import json
|
|
7
5
|
import mimetypes
|
|
8
6
|
import os
|
|
9
|
-
import platform
|
|
10
|
-
import re
|
|
11
|
-
import shlex
|
|
12
|
-
import subprocess
|
|
13
|
-
import time
|
|
14
7
|
import traceback
|
|
15
|
-
import
|
|
8
|
+
from dataclasses import dataclass
|
|
16
9
|
from os.path import expanduser
|
|
17
10
|
from pathlib import Path
|
|
18
|
-
from tempfile import NamedTemporaryFile
|
|
11
|
+
from tempfile import NamedTemporaryFile
|
|
19
12
|
from typing import (
|
|
20
13
|
Any,
|
|
21
14
|
Callable,
|
|
22
15
|
Literal,
|
|
23
16
|
Optional,
|
|
24
17
|
ParamSpec,
|
|
25
|
-
Protocol,
|
|
26
18
|
Type,
|
|
27
19
|
TypeVar,
|
|
28
20
|
)
|
|
29
21
|
|
|
30
|
-
import pexpect
|
|
31
|
-
import pyte
|
|
32
|
-
import rich
|
|
33
|
-
import tokenizers # type: ignore
|
|
34
|
-
import typer
|
|
35
|
-
import websockets
|
|
36
22
|
from openai.types.chat import (
|
|
37
23
|
ChatCompletionMessageParam,
|
|
38
24
|
)
|
|
39
|
-
from pydantic import BaseModel, TypeAdapter
|
|
25
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
40
26
|
from syntax_checker import check_syntax
|
|
41
|
-
|
|
42
|
-
from
|
|
27
|
+
|
|
28
|
+
from wcgw.client.bash_state.bash_state import get_status
|
|
43
29
|
|
|
44
30
|
from ..types_ import (
|
|
45
31
|
BashCommand,
|
|
46
|
-
BashInteraction,
|
|
47
32
|
CodeWriterMode,
|
|
33
|
+
Console,
|
|
48
34
|
ContextSave,
|
|
49
35
|
FileEdit,
|
|
50
|
-
FileEditFindReplace,
|
|
51
|
-
GetScreenInfo,
|
|
52
36
|
Initialize,
|
|
53
|
-
Keyboard,
|
|
54
37
|
Modes,
|
|
55
38
|
ModesConfig,
|
|
56
|
-
Mouse,
|
|
57
39
|
ReadFiles,
|
|
58
40
|
ReadImage,
|
|
59
|
-
ResetShell,
|
|
60
|
-
ScreenShot,
|
|
61
41
|
WriteIfEmpty,
|
|
62
42
|
)
|
|
63
|
-
from .
|
|
43
|
+
from .bash_state.bash_state import (
|
|
44
|
+
BashState,
|
|
45
|
+
execute_bash,
|
|
46
|
+
)
|
|
47
|
+
from .encoder import EncoderDecoder, get_default_encoder
|
|
64
48
|
from .file_ops.search_replace import search_replace_edit
|
|
65
49
|
from .memory import load_memory, save_memory
|
|
66
50
|
from .modes import (
|
|
67
51
|
ARCHITECT_PROMPT,
|
|
68
52
|
WCGW_PROMPT,
|
|
69
|
-
BashCommandMode,
|
|
70
|
-
FileEditMode,
|
|
71
|
-
WriteIfEmptyMode,
|
|
72
53
|
code_writer_prompt,
|
|
73
54
|
modes_to_state,
|
|
74
55
|
)
|
|
75
56
|
from .repo_ops.repo_context import get_repo_context
|
|
76
|
-
from .sys_utils import command_run
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class Console(Protocol):
|
|
80
|
-
def print(self, msg: str, *args: Any, **kwargs: Any) -> None: ...
|
|
81
|
-
|
|
82
|
-
def log(self, msg: str, *args: Any, **kwargs: Any) -> None: ...
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
console: Console = rich.console.Console(style="magenta", highlight=False, markup=False)
|
|
86
|
-
|
|
87
|
-
TIMEOUT = 5
|
|
88
|
-
TIMEOUT_WHILE_OUTPUT = 20
|
|
89
|
-
OUTPUT_WAIT_PATIENCE = 3
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def render_terminal_output(text: str) -> list[str]:
|
|
93
|
-
screen = pyte.Screen(160, 500)
|
|
94
|
-
screen.set_mode(pyte.modes.LNM)
|
|
95
|
-
stream = pyte.Stream(screen)
|
|
96
|
-
stream.feed(text)
|
|
97
|
-
# Filter out empty lines
|
|
98
|
-
dsp = screen.display[::-1]
|
|
99
|
-
for i, line in enumerate(dsp):
|
|
100
|
-
if line.strip():
|
|
101
|
-
break
|
|
102
|
-
else:
|
|
103
|
-
i = len(dsp)
|
|
104
|
-
lines = screen.display[: len(dsp) - i]
|
|
105
|
-
return lines
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def get_incremental_output(old_output: list[str], new_output: list[str]) -> list[str]:
|
|
109
|
-
nold = len(old_output)
|
|
110
|
-
nnew = len(new_output)
|
|
111
|
-
if not old_output:
|
|
112
|
-
return new_output
|
|
113
|
-
for i in range(nnew - 1, -1, -1):
|
|
114
|
-
if new_output[i] != old_output[-1]:
|
|
115
|
-
continue
|
|
116
|
-
for j in range(i - 1, -1, -1):
|
|
117
|
-
if (nold - 1 + j - i) < 0:
|
|
118
|
-
break
|
|
119
|
-
if new_output[j] != old_output[-1 + j - i]:
|
|
120
|
-
break
|
|
121
|
-
else:
|
|
122
|
-
return new_output[i + 1 :]
|
|
123
|
-
return new_output
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class Confirmation(BaseModel):
|
|
127
|
-
prompt: str
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def ask_confirmation(prompt: Confirmation) -> str:
|
|
131
|
-
response = input(prompt.prompt + " [y/n] ")
|
|
132
|
-
return "Yes" if response.lower() == "y" else "No"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
PROMPT_CONST = "#" + "@wcgw@#"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def is_mac() -> bool:
|
|
139
|
-
return platform.system() == "Darwin"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def get_tmpdir() -> str:
|
|
143
|
-
current_tmpdir = os.environ.get("TMPDIR", "")
|
|
144
|
-
if current_tmpdir or not is_mac():
|
|
145
|
-
return current_tmpdir
|
|
146
|
-
try:
|
|
147
|
-
# Fix issue while running ocrmypdf -> tesseract -> leptonica, set TMPDIR
|
|
148
|
-
# https://github.com/tesseract-ocr/tesseract/issues/4333
|
|
149
|
-
result = subprocess.check_output(
|
|
150
|
-
["getconf", "DARWIN_USER_TEMP_DIR"],
|
|
151
|
-
text=True,
|
|
152
|
-
).strip()
|
|
153
|
-
return result
|
|
154
|
-
except subprocess.CalledProcessError:
|
|
155
|
-
return "//tmp"
|
|
156
|
-
except Exception:
|
|
157
|
-
return ""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def start_shell(is_restricted_mode: bool, initial_dir: str) -> pexpect.spawn: # type: ignore[type-arg]
|
|
161
|
-
cmd = "/bin/bash"
|
|
162
|
-
if is_restricted_mode:
|
|
163
|
-
cmd += " -r"
|
|
164
|
-
|
|
165
|
-
overrideenv = {**os.environ, "PS1": PROMPT_CONST, "TMPDIR": get_tmpdir()}
|
|
166
|
-
try:
|
|
167
|
-
shell = pexpect.spawn(
|
|
168
|
-
cmd,
|
|
169
|
-
env=overrideenv, # type: ignore[arg-type]
|
|
170
|
-
echo=False,
|
|
171
|
-
encoding="utf-8",
|
|
172
|
-
timeout=TIMEOUT,
|
|
173
|
-
cwd=initial_dir,
|
|
174
|
-
codec_errors="backslashreplace",
|
|
175
|
-
)
|
|
176
|
-
shell.sendline(
|
|
177
|
-
f"export PROMPT_COMMAND= PS1={PROMPT_CONST}"
|
|
178
|
-
) # Unset prompt command to avoid interfering
|
|
179
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
180
|
-
except Exception as e:
|
|
181
|
-
console.print(traceback.format_exc())
|
|
182
|
-
console.log(f"Error starting shell: {e}. Retrying without rc ...")
|
|
183
|
-
|
|
184
|
-
shell = pexpect.spawn(
|
|
185
|
-
"/bin/bash --noprofile --norc",
|
|
186
|
-
env=overrideenv, # type: ignore[arg-type]
|
|
187
|
-
echo=False,
|
|
188
|
-
encoding="utf-8",
|
|
189
|
-
timeout=TIMEOUT,
|
|
190
|
-
codec_errors="backslashreplace",
|
|
191
|
-
)
|
|
192
|
-
shell.sendline(f"export PS1={PROMPT_CONST}")
|
|
193
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
194
|
-
|
|
195
|
-
shell.sendline("stty -icanon -echo")
|
|
196
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
197
|
-
shell.sendline("set +o pipefail")
|
|
198
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
199
|
-
shell.sendline("export GIT_PAGER=cat PAGER=cat")
|
|
200
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
201
|
-
return shell
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _is_int(mystr: str) -> bool:
|
|
205
|
-
try:
|
|
206
|
-
int(mystr)
|
|
207
|
-
return True
|
|
208
|
-
except ValueError:
|
|
209
|
-
return False
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
class BashState:
|
|
216
|
-
def __init__(
|
|
217
|
-
self,
|
|
218
|
-
working_dir: str,
|
|
219
|
-
bash_command_mode: Optional[BashCommandMode],
|
|
220
|
-
file_edit_mode: Optional[FileEditMode],
|
|
221
|
-
write_if_empty_mode: Optional[WriteIfEmptyMode],
|
|
222
|
-
mode: Optional[Modes],
|
|
223
|
-
whitelist_for_overwrite: Optional[set[str]] = None,
|
|
224
|
-
) -> None:
|
|
225
|
-
self._cwd = working_dir or os.getcwd()
|
|
226
|
-
self._bash_command_mode: BashCommandMode = bash_command_mode or BashCommandMode(
|
|
227
|
-
"normal_mode", "all"
|
|
228
|
-
)
|
|
229
|
-
self._file_edit_mode: FileEditMode = file_edit_mode or FileEditMode("all")
|
|
230
|
-
self._write_if_empty_mode: WriteIfEmptyMode = (
|
|
231
|
-
write_if_empty_mode or WriteIfEmptyMode("all")
|
|
232
|
-
)
|
|
233
|
-
self._mode = mode or Modes.wcgw
|
|
234
|
-
self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
|
|
235
|
-
self._prompt = PROMPT_CONST
|
|
236
|
-
self._init_shell()
|
|
237
|
-
|
|
238
|
-
@property
|
|
239
|
-
def mode(self) -> Modes:
|
|
240
|
-
return self._mode
|
|
241
|
-
|
|
242
|
-
@property
|
|
243
|
-
def bash_command_mode(self) -> BashCommandMode:
|
|
244
|
-
return self._bash_command_mode
|
|
245
|
-
|
|
246
|
-
@property
|
|
247
|
-
def file_edit_mode(self) -> FileEditMode:
|
|
248
|
-
return self._file_edit_mode
|
|
249
|
-
|
|
250
|
-
@property
|
|
251
|
-
def write_if_empty_mode(self) -> WriteIfEmptyMode:
|
|
252
|
-
return self._write_if_empty_mode
|
|
253
|
-
|
|
254
|
-
def ensure_env_and_bg_jobs(self) -> Optional[int]:
|
|
255
|
-
if self._prompt != PROMPT_CONST:
|
|
256
|
-
return None
|
|
257
|
-
shell = self.shell
|
|
258
|
-
# First reset the prompt in case venv was sourced or other reasons.
|
|
259
|
-
shell.sendline(f"export PS1={self._prompt}")
|
|
260
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
261
|
-
# Reset echo also if it was enabled
|
|
262
|
-
shell.sendline("stty -icanon -echo")
|
|
263
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
264
|
-
shell.sendline("set +o pipefail")
|
|
265
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
266
|
-
shell.sendline("export GIT_PAGER=cat PAGER=cat")
|
|
267
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
268
|
-
shell.sendline("jobs | wc -l")
|
|
269
|
-
before = ""
|
|
270
|
-
counts = 0
|
|
271
|
-
while not _is_int(before): # Consume all previous output
|
|
272
|
-
try:
|
|
273
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
274
|
-
except pexpect.TIMEOUT:
|
|
275
|
-
console.print(f"Couldn't get exit code, before: {before}")
|
|
276
|
-
raise
|
|
277
|
-
|
|
278
|
-
before_val = shell.before
|
|
279
|
-
if not isinstance(before_val, str):
|
|
280
|
-
before_val = str(before_val)
|
|
281
|
-
assert isinstance(before_val, str)
|
|
282
|
-
before_lines = render_terminal_output(before_val)
|
|
283
|
-
before = "\n".join(before_lines).strip()
|
|
284
|
-
counts += 1
|
|
285
|
-
if counts > 100:
|
|
286
|
-
raise ValueError(
|
|
287
|
-
"Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
try:
|
|
291
|
-
return int(before)
|
|
292
|
-
except ValueError:
|
|
293
|
-
raise ValueError(f"Malformed output: {before}")
|
|
294
|
-
|
|
295
|
-
def _init_shell(self) -> None:
|
|
296
|
-
self._prompt = PROMPT_CONST
|
|
297
|
-
self._state: Literal["repl"] | datetime.datetime = "repl"
|
|
298
|
-
self._is_in_docker: Optional[str] = ""
|
|
299
|
-
# Ensure self._cwd exists
|
|
300
|
-
os.makedirs(self._cwd, exist_ok=True)
|
|
301
|
-
self._shell = start_shell(
|
|
302
|
-
self._bash_command_mode.bash_mode == "restricted_mode",
|
|
303
|
-
self._cwd,
|
|
304
|
-
)
|
|
305
57
|
|
|
306
|
-
self._pending_output = ""
|
|
307
58
|
|
|
308
|
-
|
|
309
|
-
|
|
59
|
+
@dataclass
|
|
60
|
+
class Context:
|
|
61
|
+
bash_state: BashState
|
|
62
|
+
console: Console
|
|
310
63
|
|
|
311
|
-
@property
|
|
312
|
-
def shell(self) -> pexpect.spawn: # type: ignore
|
|
313
|
-
return self._shell
|
|
314
|
-
|
|
315
|
-
def set_pending(self, last_pending_output: str) -> None:
|
|
316
|
-
if not isinstance(self._state, datetime.datetime):
|
|
317
|
-
self._state = datetime.datetime.now()
|
|
318
|
-
self._pending_output = last_pending_output
|
|
319
|
-
|
|
320
|
-
def set_repl(self) -> None:
|
|
321
|
-
self._state = "repl"
|
|
322
|
-
self._pending_output = ""
|
|
323
|
-
|
|
324
|
-
@property
|
|
325
|
-
def state(self) -> BASH_CLF_OUTPUT:
|
|
326
|
-
if self._state == "repl":
|
|
327
|
-
return "repl"
|
|
328
|
-
return "pending"
|
|
329
|
-
|
|
330
|
-
@property
|
|
331
|
-
def is_in_docker(self) -> Optional[str]:
|
|
332
|
-
return self._is_in_docker
|
|
333
64
|
|
|
334
|
-
|
|
335
|
-
self._is_in_docker = docker_image_id
|
|
65
|
+
INITIALIZED = False
|
|
336
66
|
|
|
337
|
-
@property
|
|
338
|
-
def cwd(self) -> str:
|
|
339
|
-
return self._cwd
|
|
340
67
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
before_val = self.shell.before
|
|
349
|
-
if not isinstance(before_val, str):
|
|
350
|
-
before_val = str(before_val)
|
|
351
|
-
before_lines = render_terminal_output(before_val)
|
|
352
|
-
current_dir = "\n".join(before_lines).strip()
|
|
353
|
-
self._cwd = current_dir
|
|
354
|
-
return current_dir
|
|
355
|
-
|
|
356
|
-
def reset_shell(self) -> None:
|
|
357
|
-
self.shell.close(True)
|
|
358
|
-
self._init_shell()
|
|
359
|
-
|
|
360
|
-
def serialize(self) -> dict[str, Any]:
|
|
361
|
-
"""Serialize BashState to a dictionary for saving"""
|
|
362
|
-
return {
|
|
363
|
-
"bash_command_mode": self._bash_command_mode.serialize(),
|
|
364
|
-
"file_edit_mode": self._file_edit_mode.serialize(),
|
|
365
|
-
"write_if_empty_mode": self._write_if_empty_mode.serialize(),
|
|
366
|
-
"whitelist_for_overwrite": list(self._whitelist_for_overwrite),
|
|
367
|
-
"mode": self._mode,
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
@staticmethod
|
|
371
|
-
def parse_state(
|
|
372
|
-
state: dict[str, Any],
|
|
373
|
-
) -> tuple[BashCommandMode, FileEditMode, WriteIfEmptyMode, Modes, list[str]]:
|
|
374
|
-
return (
|
|
375
|
-
BashCommandMode.deserialize(state["bash_command_mode"]),
|
|
376
|
-
FileEditMode.deserialize(state["file_edit_mode"]),
|
|
377
|
-
WriteIfEmptyMode.deserialize(state["write_if_empty_mode"]),
|
|
378
|
-
Modes[str(state["mode"])],
|
|
379
|
-
state["whitelist_for_overwrite"],
|
|
68
|
+
def get_mode_prompt(context: Context) -> str:
|
|
69
|
+
mode_prompt = ""
|
|
70
|
+
if context.bash_state.mode == "code_writer":
|
|
71
|
+
mode_prompt = code_writer_prompt(
|
|
72
|
+
context.bash_state.file_edit_mode.allowed_globs,
|
|
73
|
+
context.bash_state.write_if_empty_mode.allowed_globs,
|
|
74
|
+
"all" if context.bash_state.bash_command_mode.allowed_commands else [],
|
|
380
75
|
)
|
|
76
|
+
elif context.bash_state.mode == "architect":
|
|
77
|
+
mode_prompt = ARCHITECT_PROMPT
|
|
78
|
+
else:
|
|
79
|
+
mode_prompt = WCGW_PROMPT
|
|
381
80
|
|
|
382
|
-
|
|
383
|
-
self,
|
|
384
|
-
bash_command_mode: BashCommandMode,
|
|
385
|
-
file_edit_mode: FileEditMode,
|
|
386
|
-
write_if_empty_mode: WriteIfEmptyMode,
|
|
387
|
-
mode: Modes,
|
|
388
|
-
whitelist_for_overwrite: list[str],
|
|
389
|
-
cwd: str,
|
|
390
|
-
) -> None:
|
|
391
|
-
"""Create a new BashState instance from a serialized state dictionary"""
|
|
392
|
-
self._bash_command_mode = bash_command_mode
|
|
393
|
-
self._cwd = cwd or self._cwd
|
|
394
|
-
self._file_edit_mode = file_edit_mode
|
|
395
|
-
self._write_if_empty_mode = write_if_empty_mode
|
|
396
|
-
self._whitelist_for_overwrite = set(whitelist_for_overwrite)
|
|
397
|
-
self._mode = mode
|
|
398
|
-
self.reset_shell()
|
|
399
|
-
|
|
400
|
-
def get_pending_for(self) -> str:
|
|
401
|
-
if isinstance(self._state, datetime.datetime):
|
|
402
|
-
timedelta = datetime.datetime.now() - self._state
|
|
403
|
-
return (
|
|
404
|
-
str(
|
|
405
|
-
int(
|
|
406
|
-
(
|
|
407
|
-
timedelta + datetime.timedelta(seconds=TIMEOUT)
|
|
408
|
-
).total_seconds()
|
|
409
|
-
)
|
|
410
|
-
)
|
|
411
|
-
+ " seconds"
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
return "Not pending"
|
|
415
|
-
|
|
416
|
-
@property
|
|
417
|
-
def whitelist_for_overwrite(self) -> set[str]:
|
|
418
|
-
return self._whitelist_for_overwrite
|
|
419
|
-
|
|
420
|
-
def add_to_whitelist_for_overwrite(self, file_path: str) -> None:
|
|
421
|
-
self._whitelist_for_overwrite.add(file_path)
|
|
422
|
-
|
|
423
|
-
@property
|
|
424
|
-
def pending_output(self) -> str:
|
|
425
|
-
return self._pending_output
|
|
426
|
-
|
|
427
|
-
def update_repl_prompt(self, command: str) -> bool:
|
|
428
|
-
if re.match(r"^wcgw_update_prompt\(\)$", command.strip()):
|
|
429
|
-
self.shell.sendintr()
|
|
430
|
-
index = self.shell.expect([self._prompt, pexpect.TIMEOUT], timeout=0.2)
|
|
431
|
-
if index == 0:
|
|
432
|
-
return True
|
|
433
|
-
before = self.shell.before or ""
|
|
434
|
-
assert before, "Something went wrong updating repl prompt"
|
|
435
|
-
self._prompt = before.split("\n")[-1].strip()
|
|
436
|
-
# Escape all regex
|
|
437
|
-
self._prompt = re.escape(self._prompt)
|
|
438
|
-
console.print(f"Trying to update prompt to: {self._prompt.encode()!r}")
|
|
439
|
-
index = 0
|
|
440
|
-
counts = 0
|
|
441
|
-
while index == 0:
|
|
442
|
-
# Consume all REPL prompts till now
|
|
443
|
-
index = self.shell.expect([self._prompt, pexpect.TIMEOUT], timeout=0.2)
|
|
444
|
-
counts += 1
|
|
445
|
-
if counts > 100:
|
|
446
|
-
raise ValueError(
|
|
447
|
-
"Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
|
|
448
|
-
)
|
|
449
|
-
console.print(f"Prompt updated to: {self._prompt}")
|
|
450
|
-
return True
|
|
451
|
-
return False
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
BASH_STATE = BashState(os.getcwd(), None, None, None, None)
|
|
455
|
-
INITIALIZED = False
|
|
81
|
+
return mode_prompt
|
|
456
82
|
|
|
457
83
|
|
|
458
84
|
def initialize(
|
|
85
|
+
type: Literal["user_asked_change_workspace", "first_call"],
|
|
86
|
+
context: Context,
|
|
459
87
|
any_workspace_path: str,
|
|
460
88
|
read_files_: list[str],
|
|
461
89
|
task_id_to_resume: str,
|
|
462
90
|
max_tokens: Optional[int],
|
|
463
91
|
mode: ModesConfig,
|
|
464
|
-
) -> str:
|
|
465
|
-
global BASH_STATE
|
|
466
|
-
|
|
92
|
+
) -> tuple[str, Context]:
|
|
467
93
|
# Expand the workspace path
|
|
468
|
-
any_workspace_path = expand_user(any_workspace_path
|
|
94
|
+
any_workspace_path = expand_user(any_workspace_path)
|
|
469
95
|
repo_context = ""
|
|
470
96
|
|
|
471
97
|
memory = ""
|
|
472
|
-
|
|
473
|
-
if task_id_to_resume:
|
|
474
|
-
try:
|
|
475
|
-
project_root_path, task_mem, bash_state = load_memory(
|
|
476
|
-
task_id_to_resume,
|
|
477
|
-
max_tokens,
|
|
478
|
-
lambda x: default_enc.encode(x).ids,
|
|
479
|
-
lambda x: default_enc.decode(x),
|
|
480
|
-
)
|
|
481
|
-
memory = "Following is the retrieved task:\n" + task_mem
|
|
482
|
-
if os.path.exists(project_root_path):
|
|
483
|
-
any_workspace_path = project_root_path
|
|
98
|
+
loaded_state = None
|
|
484
99
|
|
|
485
|
-
|
|
486
|
-
|
|
100
|
+
if type == "first_call":
|
|
101
|
+
if task_id_to_resume:
|
|
102
|
+
try:
|
|
103
|
+
project_root_path, task_mem, loaded_state = load_memory(
|
|
104
|
+
task_id_to_resume,
|
|
105
|
+
max_tokens,
|
|
106
|
+
lambda x: default_enc.encoder(x),
|
|
107
|
+
lambda x: default_enc.decoder(x),
|
|
108
|
+
)
|
|
109
|
+
memory = "Following is the retrieved task:\n" + task_mem
|
|
110
|
+
if os.path.exists(project_root_path):
|
|
111
|
+
any_workspace_path = project_root_path
|
|
112
|
+
|
|
113
|
+
except Exception:
|
|
114
|
+
memory = f'Error: Unable to load task with ID "{task_id_to_resume}" '
|
|
115
|
+
elif task_id_to_resume:
|
|
116
|
+
memory = (
|
|
117
|
+
"Warning: task can only be resumed in a new conversation. No task loaded."
|
|
118
|
+
)
|
|
487
119
|
|
|
488
120
|
folder_to_start = None
|
|
489
121
|
if any_workspace_path:
|
|
@@ -494,7 +126,7 @@ def initialize(
|
|
|
494
126
|
if not read_files_:
|
|
495
127
|
read_files_ = [any_workspace_path]
|
|
496
128
|
any_workspace_path = os.path.dirname(any_workspace_path)
|
|
497
|
-
repo_context, folder_to_start = get_repo_context(any_workspace_path,
|
|
129
|
+
repo_context, folder_to_start = get_repo_context(any_workspace_path, 50)
|
|
498
130
|
|
|
499
131
|
repo_context = f"---\n# Workspace structure\n{repo_context}\n---\n"
|
|
500
132
|
|
|
@@ -513,42 +145,49 @@ def initialize(
|
|
|
513
145
|
f"\nInfo: Workspace path {any_workspace_path} does not exist."
|
|
514
146
|
)
|
|
515
147
|
# Restore bash state if available
|
|
516
|
-
if
|
|
148
|
+
if loaded_state is not None:
|
|
517
149
|
try:
|
|
518
|
-
parsed_state = BashState.parse_state(
|
|
150
|
+
parsed_state = BashState.parse_state(loaded_state)
|
|
519
151
|
if mode == "wcgw":
|
|
520
|
-
|
|
152
|
+
context.bash_state.load_state(
|
|
521
153
|
parsed_state[0],
|
|
522
154
|
parsed_state[1],
|
|
523
155
|
parsed_state[2],
|
|
524
156
|
parsed_state[3],
|
|
525
|
-
parsed_state[4] + list(
|
|
157
|
+
parsed_state[4] + list(context.bash_state.whitelist_for_overwrite),
|
|
526
158
|
str(folder_to_start) if folder_to_start else "",
|
|
527
159
|
)
|
|
528
160
|
else:
|
|
529
161
|
state = modes_to_state(mode)
|
|
530
|
-
|
|
162
|
+
context.bash_state.load_state(
|
|
531
163
|
state[0],
|
|
532
164
|
state[1],
|
|
533
165
|
state[2],
|
|
534
166
|
state[3],
|
|
535
|
-
parsed_state[4] + list(
|
|
167
|
+
parsed_state[4] + list(context.bash_state.whitelist_for_overwrite),
|
|
536
168
|
str(folder_to_start) if folder_to_start else "",
|
|
537
169
|
)
|
|
538
170
|
except ValueError:
|
|
539
|
-
console.print(traceback.format_exc())
|
|
540
|
-
console.print("Error: couldn't load bash state")
|
|
171
|
+
context.console.print(traceback.format_exc())
|
|
172
|
+
context.console.print("Error: couldn't load bash state")
|
|
541
173
|
pass
|
|
174
|
+
mode_prompt = get_mode_prompt(context)
|
|
542
175
|
else:
|
|
176
|
+
mode_changed = is_mode_change(mode, context.bash_state)
|
|
543
177
|
state = modes_to_state(mode)
|
|
544
|
-
|
|
178
|
+
context.bash_state.load_state(
|
|
545
179
|
state[0],
|
|
546
180
|
state[1],
|
|
547
181
|
state[2],
|
|
548
182
|
state[3],
|
|
549
|
-
list(
|
|
183
|
+
list(context.bash_state.whitelist_for_overwrite),
|
|
550
184
|
str(folder_to_start) if folder_to_start else "",
|
|
551
185
|
)
|
|
186
|
+
if type == "first_call" or mode_changed:
|
|
187
|
+
mode_prompt = get_mode_prompt(context)
|
|
188
|
+
else:
|
|
189
|
+
mode_prompt = ""
|
|
190
|
+
|
|
552
191
|
del mode
|
|
553
192
|
|
|
554
193
|
initial_files_context = ""
|
|
@@ -558,31 +197,19 @@ def initialize(
|
|
|
558
197
|
os.path.join(folder_to_start, f) if not os.path.isabs(f) else f
|
|
559
198
|
for f in read_files_
|
|
560
199
|
]
|
|
561
|
-
initial_files = read_files(read_files_, max_tokens)
|
|
200
|
+
initial_files = read_files(read_files_, max_tokens, context)
|
|
562
201
|
initial_files_context = f"---\n# Requested files\n{initial_files}\n---\n"
|
|
563
202
|
|
|
564
203
|
uname_sysname = os.uname().sysname
|
|
565
204
|
uname_machine = os.uname().machine
|
|
566
205
|
|
|
567
|
-
mode_prompt = ""
|
|
568
|
-
if BASH_STATE.mode == Modes.code_writer:
|
|
569
|
-
mode_prompt = code_writer_prompt(
|
|
570
|
-
BASH_STATE.file_edit_mode.allowed_globs,
|
|
571
|
-
BASH_STATE.write_if_empty_mode.allowed_globs,
|
|
572
|
-
"all" if BASH_STATE.bash_command_mode.allowed_commands else [],
|
|
573
|
-
)
|
|
574
|
-
elif BASH_STATE.mode == Modes.architect:
|
|
575
|
-
mode_prompt = ARCHITECT_PROMPT
|
|
576
|
-
else:
|
|
577
|
-
mode_prompt = WCGW_PROMPT
|
|
578
|
-
|
|
579
206
|
output = f"""
|
|
580
207
|
{mode_prompt}
|
|
581
208
|
|
|
582
209
|
# Environment
|
|
583
210
|
System: {uname_sysname}
|
|
584
211
|
Machine: {uname_machine}
|
|
585
|
-
Initialized in directory (also cwd): {
|
|
212
|
+
Initialized in directory (also cwd): {context.bash_state.cwd}
|
|
586
213
|
|
|
587
214
|
{repo_context}
|
|
588
215
|
|
|
@@ -595,38 +222,68 @@ Initialized in directory (also cwd): {BASH_STATE.cwd}
|
|
|
595
222
|
|
|
596
223
|
global INITIALIZED
|
|
597
224
|
INITIALIZED = True
|
|
598
|
-
|
|
599
|
-
return output
|
|
225
|
+
return output, context
|
|
600
226
|
|
|
601
227
|
|
|
602
|
-
def
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
228
|
+
def is_mode_change(mode_config: ModesConfig, bash_state: BashState) -> bool:
|
|
229
|
+
allowed = modes_to_state(mode_config)
|
|
230
|
+
bash_allowed = (
|
|
231
|
+
bash_state.bash_command_mode,
|
|
232
|
+
bash_state.file_edit_mode,
|
|
233
|
+
bash_state.write_if_empty_mode,
|
|
234
|
+
bash_state.mode,
|
|
235
|
+
)
|
|
236
|
+
return allowed != bash_allowed
|
|
606
237
|
|
|
607
|
-
WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run multiple shell sessions, likely a previous program hasn't exited.
|
|
608
|
-
1. Get its output using `send_ascii: [10] or send_specials: ["Enter"]`
|
|
609
|
-
2. Use `send_ascii` or `send_specials` to give inputs to the running program, don't use `BashCommand` OR
|
|
610
|
-
3. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
|
|
611
|
-
4. Send the process in background using `send_specials: ["Ctrl-z"]` followed by BashCommand: `bg`
|
|
612
|
-
"""
|
|
613
238
|
|
|
239
|
+
def reset_wcgw(
|
|
240
|
+
context: Context,
|
|
241
|
+
starting_directory: str,
|
|
242
|
+
mode_name: Optional[Modes],
|
|
243
|
+
change_mode: ModesConfig,
|
|
244
|
+
) -> str:
|
|
245
|
+
global INITIALIZED
|
|
246
|
+
if mode_name:
|
|
247
|
+
# Get new state configuration
|
|
248
|
+
bash_command_mode, file_edit_mode, write_if_empty_mode, mode = modes_to_state(
|
|
249
|
+
change_mode
|
|
250
|
+
)
|
|
614
251
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
252
|
+
# Reset shell with new mode
|
|
253
|
+
context.bash_state.load_state(
|
|
254
|
+
bash_command_mode,
|
|
255
|
+
file_edit_mode,
|
|
256
|
+
write_if_empty_mode,
|
|
257
|
+
mode,
|
|
258
|
+
list(context.bash_state.whitelist_for_overwrite),
|
|
259
|
+
starting_directory,
|
|
260
|
+
)
|
|
261
|
+
mode_prompt = get_mode_prompt(context)
|
|
262
|
+
INITIALIZED = True
|
|
263
|
+
return (
|
|
264
|
+
f"Reset successful with mode change to {mode_name}.\n"
|
|
265
|
+
+ mode_prompt
|
|
266
|
+
+ "\n"
|
|
267
|
+
+ get_status(context.bash_state)
|
|
268
|
+
)
|
|
621
269
|
else:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
270
|
+
# Regular reset without mode change - keep same mode but update directory
|
|
271
|
+
bash_command_mode = context.bash_state.bash_command_mode
|
|
272
|
+
file_edit_mode = context.bash_state.file_edit_mode
|
|
273
|
+
write_if_empty_mode = context.bash_state.write_if_empty_mode
|
|
274
|
+
mode = context.bash_state.mode
|
|
275
|
+
|
|
276
|
+
# Reload state with new directory
|
|
277
|
+
context.bash_state.load_state(
|
|
278
|
+
bash_command_mode,
|
|
279
|
+
file_edit_mode,
|
|
280
|
+
write_if_empty_mode,
|
|
281
|
+
mode,
|
|
282
|
+
list(context.bash_state.whitelist_for_overwrite),
|
|
283
|
+
starting_directory,
|
|
284
|
+
)
|
|
285
|
+
INITIALIZED = True
|
|
286
|
+
return "Reset successful" + get_status(context.bash_state)
|
|
630
287
|
|
|
631
288
|
|
|
632
289
|
T = TypeVar("T")
|
|
@@ -639,238 +296,12 @@ def save_out_of_context(content: str, suffix: str) -> str:
|
|
|
639
296
|
return file_path
|
|
640
297
|
|
|
641
298
|
|
|
642
|
-
def
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
def expand_user(path: str, docker_id: Optional[str]) -> str:
|
|
647
|
-
if not path or not path.startswith("~") or docker_id:
|
|
299
|
+
def expand_user(path: str) -> str:
|
|
300
|
+
if not path or not path.startswith("~"):
|
|
648
301
|
return path
|
|
649
302
|
return expanduser(path)
|
|
650
303
|
|
|
651
304
|
|
|
652
|
-
def _incremental_text(text: str, last_pending_output: str) -> str:
|
|
653
|
-
# text = render_terminal_output(text[-100_000:])
|
|
654
|
-
text = text[-100_000:]
|
|
655
|
-
|
|
656
|
-
last_pending_output_rendered_lines = render_terminal_output(last_pending_output)
|
|
657
|
-
last_pending_output_rendered = "\n".join(last_pending_output_rendered_lines)
|
|
658
|
-
last_rendered_lines = last_pending_output_rendered.split("\n")
|
|
659
|
-
if not last_rendered_lines:
|
|
660
|
-
return rstrip(render_terminal_output(text))
|
|
661
|
-
|
|
662
|
-
text = text[len(last_pending_output) :]
|
|
663
|
-
old_rendered_applied = render_terminal_output(last_pending_output_rendered + text)
|
|
664
|
-
# True incremental is then
|
|
665
|
-
rendered = get_incremental_output(last_rendered_lines[:-1], old_rendered_applied)
|
|
666
|
-
|
|
667
|
-
if not rendered:
|
|
668
|
-
return ""
|
|
669
|
-
|
|
670
|
-
if rendered[0] == last_rendered_lines[-1]:
|
|
671
|
-
rendered = rendered[1:]
|
|
672
|
-
return rstrip(rendered)
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
def is_status_check(arg: BashInteraction | BashCommand) -> bool:
|
|
676
|
-
return isinstance(arg, BashInteraction) and (
|
|
677
|
-
arg.send_specials == ["Enter"] or arg.send_ascii == [10]
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def execute_bash(
|
|
682
|
-
enc: tokenizers.Tokenizer,
|
|
683
|
-
bash_arg: BashCommand | BashInteraction,
|
|
684
|
-
max_tokens: Optional[int],
|
|
685
|
-
timeout_s: Optional[float],
|
|
686
|
-
) -> tuple[str, float]:
|
|
687
|
-
try:
|
|
688
|
-
is_interrupt = False
|
|
689
|
-
if isinstance(bash_arg, BashCommand):
|
|
690
|
-
if BASH_STATE.bash_command_mode.allowed_commands == "none":
|
|
691
|
-
return "Error: BashCommand not allowed in current mode", 0.0
|
|
692
|
-
updated_repl_mode = BASH_STATE.update_repl_prompt(bash_arg.command)
|
|
693
|
-
if updated_repl_mode:
|
|
694
|
-
BASH_STATE.set_repl()
|
|
695
|
-
response = (
|
|
696
|
-
"Prompt updated, you can execute REPL lines using BashCommand now"
|
|
697
|
-
)
|
|
698
|
-
console.print(response)
|
|
699
|
-
return (
|
|
700
|
-
response,
|
|
701
|
-
0,
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
console.print(f"$ {bash_arg.command}")
|
|
705
|
-
if BASH_STATE.state == "pending":
|
|
706
|
-
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
707
|
-
command = bash_arg.command.strip()
|
|
708
|
-
|
|
709
|
-
if "\n" in command:
|
|
710
|
-
raise ValueError(
|
|
711
|
-
"Command should not contain newline character in middle. Run only one command at a time."
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
for i in range(0, len(command), 128):
|
|
715
|
-
BASH_STATE.shell.send(command[i : i + 128])
|
|
716
|
-
BASH_STATE.shell.send(BASH_STATE.shell.linesep)
|
|
717
|
-
|
|
718
|
-
else:
|
|
719
|
-
if (
|
|
720
|
-
sum(
|
|
721
|
-
[
|
|
722
|
-
int(bool(bash_arg.send_text)),
|
|
723
|
-
int(bool(bash_arg.send_specials)),
|
|
724
|
-
int(bool(bash_arg.send_ascii)),
|
|
725
|
-
]
|
|
726
|
-
)
|
|
727
|
-
!= 1
|
|
728
|
-
):
|
|
729
|
-
return (
|
|
730
|
-
"Failure: exactly one of send_text, send_specials or send_ascii should be provided",
|
|
731
|
-
0.0,
|
|
732
|
-
)
|
|
733
|
-
if bash_arg.send_specials:
|
|
734
|
-
console.print(f"Sending special sequence: {bash_arg.send_specials}")
|
|
735
|
-
for char in bash_arg.send_specials:
|
|
736
|
-
if char == "Key-up":
|
|
737
|
-
BASH_STATE.shell.send("\033[A")
|
|
738
|
-
elif char == "Key-down":
|
|
739
|
-
BASH_STATE.shell.send("\033[B")
|
|
740
|
-
elif char == "Key-left":
|
|
741
|
-
BASH_STATE.shell.send("\033[D")
|
|
742
|
-
elif char == "Key-right":
|
|
743
|
-
BASH_STATE.shell.send("\033[C")
|
|
744
|
-
elif char == "Enter":
|
|
745
|
-
BASH_STATE.shell.send("\n")
|
|
746
|
-
elif char == "Ctrl-c":
|
|
747
|
-
BASH_STATE.shell.sendintr()
|
|
748
|
-
is_interrupt = True
|
|
749
|
-
elif char == "Ctrl-d":
|
|
750
|
-
BASH_STATE.shell.sendintr()
|
|
751
|
-
is_interrupt = True
|
|
752
|
-
elif char == "Ctrl-z":
|
|
753
|
-
BASH_STATE.shell.send("\x1a")
|
|
754
|
-
else:
|
|
755
|
-
raise Exception(f"Unknown special character: {char}")
|
|
756
|
-
elif bash_arg.send_ascii:
|
|
757
|
-
console.print(f"Sending ASCII sequence: {bash_arg.send_ascii}")
|
|
758
|
-
for ascii_char in bash_arg.send_ascii:
|
|
759
|
-
BASH_STATE.shell.send(chr(ascii_char))
|
|
760
|
-
if ascii_char == 3:
|
|
761
|
-
is_interrupt = True
|
|
762
|
-
else:
|
|
763
|
-
if bash_arg.send_text is None:
|
|
764
|
-
return (
|
|
765
|
-
"Failure: at least one of send_text, send_specials or send_ascii should be provided",
|
|
766
|
-
0.0,
|
|
767
|
-
)
|
|
768
|
-
|
|
769
|
-
updated_repl_mode = BASH_STATE.update_repl_prompt(bash_arg.send_text)
|
|
770
|
-
if updated_repl_mode:
|
|
771
|
-
BASH_STATE.set_repl()
|
|
772
|
-
response = "Prompt updated, you can execute REPL lines using BashCommand now"
|
|
773
|
-
console.print(response)
|
|
774
|
-
return (
|
|
775
|
-
response,
|
|
776
|
-
0,
|
|
777
|
-
)
|
|
778
|
-
console.print(f"Interact text: {bash_arg.send_text}")
|
|
779
|
-
for i in range(0, len(bash_arg.send_text), 128):
|
|
780
|
-
BASH_STATE.shell.send(bash_arg.send_text[i : i + 128])
|
|
781
|
-
BASH_STATE.shell.send(BASH_STATE.shell.linesep)
|
|
782
|
-
|
|
783
|
-
except KeyboardInterrupt:
|
|
784
|
-
BASH_STATE.shell.sendintr()
|
|
785
|
-
BASH_STATE.shell.expect(BASH_STATE.prompt)
|
|
786
|
-
return "---\n\nFailure: user interrupted the execution", 0.0
|
|
787
|
-
|
|
788
|
-
wait = min(timeout_s or TIMEOUT, TIMEOUT_WHILE_OUTPUT)
|
|
789
|
-
index = BASH_STATE.shell.expect([BASH_STATE.prompt, pexpect.TIMEOUT], timeout=wait)
|
|
790
|
-
if index == 1:
|
|
791
|
-
text = BASH_STATE.shell.before or ""
|
|
792
|
-
incremental_text = _incremental_text(text, BASH_STATE.pending_output)
|
|
793
|
-
|
|
794
|
-
second_wait_success = False
|
|
795
|
-
if is_status_check(bash_arg):
|
|
796
|
-
# There's some text in BashInteraction mode wait for TIMEOUT_WHILE_OUTPUT
|
|
797
|
-
remaining = TIMEOUT_WHILE_OUTPUT - wait
|
|
798
|
-
patience = OUTPUT_WAIT_PATIENCE
|
|
799
|
-
if not incremental_text:
|
|
800
|
-
patience -= 1
|
|
801
|
-
itext = incremental_text
|
|
802
|
-
while remaining > 0 and patience > 0:
|
|
803
|
-
index = BASH_STATE.shell.expect(
|
|
804
|
-
[BASH_STATE.prompt, pexpect.TIMEOUT], timeout=wait
|
|
805
|
-
)
|
|
806
|
-
if index == 0:
|
|
807
|
-
second_wait_success = True
|
|
808
|
-
break
|
|
809
|
-
else:
|
|
810
|
-
_itext = BASH_STATE.shell.before or ""
|
|
811
|
-
_itext = _incremental_text(_itext, BASH_STATE.pending_output)
|
|
812
|
-
if _itext != itext:
|
|
813
|
-
patience = 3
|
|
814
|
-
else:
|
|
815
|
-
patience -= 1
|
|
816
|
-
itext = _itext
|
|
817
|
-
|
|
818
|
-
remaining = remaining - wait
|
|
819
|
-
|
|
820
|
-
if not second_wait_success:
|
|
821
|
-
text = BASH_STATE.shell.before or ""
|
|
822
|
-
incremental_text = _incremental_text(text, BASH_STATE.pending_output)
|
|
823
|
-
|
|
824
|
-
if not second_wait_success:
|
|
825
|
-
BASH_STATE.set_pending(text)
|
|
826
|
-
|
|
827
|
-
tokens = enc.encode(incremental_text)
|
|
828
|
-
|
|
829
|
-
if max_tokens and len(tokens) >= max_tokens:
|
|
830
|
-
incremental_text = "(...truncated)\n" + enc.decode(
|
|
831
|
-
tokens.ids[-(max_tokens - 1) :]
|
|
832
|
-
)
|
|
833
|
-
|
|
834
|
-
if is_interrupt:
|
|
835
|
-
incremental_text = (
|
|
836
|
-
incremental_text
|
|
837
|
-
+ """---
|
|
838
|
-
----
|
|
839
|
-
Failure interrupting.
|
|
840
|
-
If any REPL session was previously running or if bashrc was sourced, or if there is issue to other REPL related reasons:
|
|
841
|
-
Run BashCommand: "wcgw_update_prompt()" to reset the PS1 prompt.
|
|
842
|
-
Otherwise, you may want to try Ctrl-c again or program specific exit interactive commands.
|
|
843
|
-
"""
|
|
844
|
-
)
|
|
845
|
-
|
|
846
|
-
exit_status = get_status()
|
|
847
|
-
incremental_text += exit_status
|
|
848
|
-
|
|
849
|
-
return incremental_text, 0
|
|
850
|
-
|
|
851
|
-
if not isinstance(BASH_STATE.shell.before, str):
|
|
852
|
-
BASH_STATE.shell.before = str(BASH_STATE.shell.before)
|
|
853
|
-
|
|
854
|
-
output = _incremental_text(BASH_STATE.shell.before, BASH_STATE.pending_output)
|
|
855
|
-
BASH_STATE.set_repl()
|
|
856
|
-
|
|
857
|
-
tokens = enc.encode(output)
|
|
858
|
-
if max_tokens and len(tokens) >= max_tokens:
|
|
859
|
-
output = "(...truncated)\n" + enc.decode(tokens.ids[-(max_tokens - 1) :])
|
|
860
|
-
|
|
861
|
-
try:
|
|
862
|
-
exit_status = get_status()
|
|
863
|
-
output += exit_status
|
|
864
|
-
except ValueError:
|
|
865
|
-
console.print(output)
|
|
866
|
-
console.print(traceback.format_exc())
|
|
867
|
-
console.print("Malformed output, restarting shell", style="red")
|
|
868
|
-
# Malformed output, restart shell
|
|
869
|
-
BASH_STATE.reset_shell()
|
|
870
|
-
output = "(exit shell has restarted)"
|
|
871
|
-
return output, 0
|
|
872
|
-
|
|
873
|
-
|
|
874
305
|
MEDIA_TYPES = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
875
306
|
|
|
876
307
|
|
|
@@ -886,58 +317,34 @@ class ImageData(BaseModel):
|
|
|
886
317
|
Param = ParamSpec("Param")
|
|
887
318
|
|
|
888
319
|
|
|
889
|
-
def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
|
|
890
|
-
def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
|
|
891
|
-
if BASH_STATE.state == "pending":
|
|
892
|
-
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
893
|
-
|
|
894
|
-
return func(*args, **kwargs)
|
|
895
|
-
|
|
896
|
-
return wrapper
|
|
897
|
-
|
|
898
|
-
|
|
899
320
|
def truncate_if_over(content: str, max_tokens: Optional[int]) -> str:
|
|
900
321
|
if max_tokens and max_tokens > 0:
|
|
901
|
-
tokens = default_enc.
|
|
322
|
+
tokens = default_enc.encoder(content)
|
|
902
323
|
n_tokens = len(tokens)
|
|
903
324
|
if n_tokens > max_tokens:
|
|
904
325
|
content = (
|
|
905
|
-
default_enc.
|
|
326
|
+
default_enc.decoder(tokens[: max(0, max_tokens - 100)])
|
|
906
327
|
+ "\n(...truncated)"
|
|
907
328
|
)
|
|
908
329
|
|
|
909
330
|
return content
|
|
910
331
|
|
|
911
332
|
|
|
912
|
-
def read_image_from_shell(file_path: str) -> ImageData:
|
|
333
|
+
def read_image_from_shell(file_path: str, context: Context) -> ImageData:
|
|
913
334
|
# Expand the path
|
|
914
|
-
file_path = expand_user(file_path
|
|
335
|
+
file_path = expand_user(file_path)
|
|
915
336
|
|
|
916
337
|
if not os.path.isabs(file_path):
|
|
917
|
-
file_path = os.path.join(
|
|
338
|
+
file_path = os.path.join(context.bash_state.cwd, file_path)
|
|
918
339
|
|
|
919
|
-
if not
|
|
920
|
-
|
|
921
|
-
raise ValueError(f"File {file_path} does not exist")
|
|
340
|
+
if not os.path.exists(file_path):
|
|
341
|
+
raise ValueError(f"File {file_path} does not exist")
|
|
922
342
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
else:
|
|
929
|
-
with TemporaryDirectory() as tmpdir:
|
|
930
|
-
rcode = os.system(
|
|
931
|
-
f"docker cp {BASH_STATE.is_in_docker}:{shlex.quote(file_path)} {tmpdir}"
|
|
932
|
-
)
|
|
933
|
-
if rcode != 0:
|
|
934
|
-
raise Exception(f"Error: Read failed with code {rcode}")
|
|
935
|
-
path_ = os.path.join(tmpdir, os.path.basename(file_path))
|
|
936
|
-
with open(path_, "rb") as f:
|
|
937
|
-
image_bytes = f.read()
|
|
938
|
-
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
939
|
-
image_type = mimetypes.guess_type(file_path)[0]
|
|
940
|
-
return ImageData(media_type=image_type, data=image_b64) # type: ignore
|
|
343
|
+
with open(file_path, "rb") as image_file:
|
|
344
|
+
image_bytes = image_file.read()
|
|
345
|
+
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
346
|
+
image_type = mimetypes.guess_type(file_path)[0]
|
|
347
|
+
return ImageData(media_type=image_type, data=image_b64) # type: ignore
|
|
941
348
|
|
|
942
349
|
|
|
943
350
|
def get_context_for_errors(
|
|
@@ -950,95 +357,63 @@ def get_context_for_errors(
|
|
|
950
357
|
context = "\n".join(context_lines)
|
|
951
358
|
|
|
952
359
|
if max_tokens is not None and max_tokens > 0:
|
|
953
|
-
ntokens = len(default_enc.
|
|
360
|
+
ntokens = len(default_enc.encoder(context))
|
|
954
361
|
if ntokens > max_tokens:
|
|
955
362
|
return "Please re-read the file to understand the context"
|
|
956
363
|
return f"Here's relevant snippet from the file where the syntax errors occured:\n```\n{context}\n```"
|
|
957
364
|
|
|
958
365
|
|
|
959
366
|
def write_file(
|
|
960
|
-
writefile: WriteIfEmpty,
|
|
367
|
+
writefile: WriteIfEmpty,
|
|
368
|
+
error_on_exist: bool,
|
|
369
|
+
max_tokens: Optional[int],
|
|
370
|
+
context: Context,
|
|
961
371
|
) -> str:
|
|
962
372
|
if not os.path.isabs(writefile.file_path):
|
|
963
|
-
return f"Failure: file_path should be absolute path, current working directory is {
|
|
373
|
+
return f"Failure: file_path should be absolute path, current working directory is {context.bash_state.cwd}"
|
|
964
374
|
else:
|
|
965
|
-
path_ = expand_user(writefile.file_path
|
|
375
|
+
path_ = expand_user(writefile.file_path)
|
|
966
376
|
|
|
967
|
-
error_on_exist_ =
|
|
377
|
+
error_on_exist_ = (
|
|
378
|
+
error_on_exist and path_ not in context.bash_state.whitelist_for_overwrite
|
|
379
|
+
)
|
|
968
380
|
|
|
969
381
|
# Validate using write_if_empty_mode after checking whitelist
|
|
970
|
-
allowed_globs =
|
|
382
|
+
allowed_globs = context.bash_state.write_if_empty_mode.allowed_globs
|
|
971
383
|
if allowed_globs != "all" and not any(
|
|
972
384
|
fnmatch.fnmatch(path_, pattern) for pattern in allowed_globs
|
|
973
385
|
):
|
|
974
386
|
return f"Error: updating file {path_} not allowed in current mode. Doesn't match allowed globs: {allowed_globs}"
|
|
387
|
+
|
|
975
388
|
add_overwrite_warning = ""
|
|
976
|
-
if
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
content = truncate_if_over(content, max_tokens)
|
|
981
|
-
|
|
982
|
-
if error_on_exist_:
|
|
983
|
-
return (
|
|
984
|
-
f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
985
|
-
+ f"\nHere's the existing content:\n```\n{content}\n```"
|
|
986
|
-
)
|
|
987
|
-
else:
|
|
988
|
-
add_overwrite_warning = content
|
|
989
|
-
|
|
990
|
-
# Since we've already errored once, add this to whitelist
|
|
991
|
-
BASH_STATE.add_to_whitelist_for_overwrite(path_)
|
|
992
|
-
|
|
993
|
-
path = Path(path_)
|
|
994
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
389
|
+
if (error_on_exist or error_on_exist_) and os.path.exists(path_):
|
|
390
|
+
content = Path(path_).read_text().strip()
|
|
391
|
+
if content:
|
|
392
|
+
content = truncate_if_over(content, max_tokens)
|
|
995
393
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
return_code, content, stderr = command_run(
|
|
1004
|
-
f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(path_)}",
|
|
1005
|
-
timeout=TIMEOUT,
|
|
1006
|
-
)
|
|
1007
|
-
if return_code != 0 and content.strip():
|
|
1008
|
-
content = truncate_if_over(content, max_tokens)
|
|
1009
|
-
|
|
1010
|
-
if error_on_exist_:
|
|
1011
|
-
return (
|
|
1012
|
-
f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
1013
|
-
+ f"\nHere's the existing content:\n```\n{content}\n```"
|
|
1014
|
-
)
|
|
1015
|
-
else:
|
|
1016
|
-
add_overwrite_warning = content
|
|
1017
|
-
|
|
1018
|
-
# Since we've already errored once, add this to whitelist
|
|
1019
|
-
BASH_STATE.add_to_whitelist_for_overwrite(path_)
|
|
1020
|
-
|
|
1021
|
-
with TemporaryDirectory() as tmpdir:
|
|
1022
|
-
tmppath = os.path.join(tmpdir, os.path.basename(path_))
|
|
1023
|
-
with open(tmppath, "w") as f:
|
|
1024
|
-
f.write(writefile.file_content)
|
|
1025
|
-
os.chmod(tmppath, 0o777)
|
|
1026
|
-
parent_dir = os.path.dirname(path_)
|
|
1027
|
-
rcode = os.system(
|
|
1028
|
-
f"docker exec {BASH_STATE.is_in_docker} mkdir -p {parent_dir}"
|
|
1029
|
-
)
|
|
1030
|
-
if rcode != 0:
|
|
1031
|
-
return f"Error: Write failed with code while creating dirs {rcode}"
|
|
394
|
+
if error_on_exist_:
|
|
395
|
+
return (
|
|
396
|
+
f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
397
|
+
+ f"\nHere's the existing content:\n```\n{content}\n```"
|
|
398
|
+
)
|
|
399
|
+
else:
|
|
400
|
+
add_overwrite_warning = content
|
|
1032
401
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
402
|
+
# Since we've already errored once, add this to whitelist
|
|
403
|
+
context.bash_state.add_to_whitelist_for_overwrite(path_)
|
|
404
|
+
|
|
405
|
+
path = Path(path_)
|
|
406
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
with path.open("w") as f:
|
|
410
|
+
f.write(writefile.file_content)
|
|
411
|
+
except OSError as e:
|
|
412
|
+
return f"Error: {e}"
|
|
1038
413
|
|
|
1039
414
|
extension = Path(path_).suffix.lstrip(".")
|
|
1040
415
|
|
|
1041
|
-
console.print(f"File written to {path_}")
|
|
416
|
+
context.console.print(f"File written to {path_}")
|
|
1042
417
|
|
|
1043
418
|
warnings = []
|
|
1044
419
|
try:
|
|
@@ -1049,7 +424,7 @@ def write_file(
|
|
|
1049
424
|
context_for_errors = get_context_for_errors(
|
|
1050
425
|
check.errors, writefile.file_content, max_tokens
|
|
1051
426
|
)
|
|
1052
|
-
console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
427
|
+
context.console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
1053
428
|
warnings.append(f"""
|
|
1054
429
|
---
|
|
1055
430
|
Warning: tree-sitter reported syntax errors
|
|
@@ -1075,9 +450,9 @@ Syntax errors:
|
|
|
1075
450
|
return "Success" + "".join(warnings)
|
|
1076
451
|
|
|
1077
452
|
|
|
1078
|
-
def do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
453
|
+
def do_diff_edit(fedit: FileEdit, max_tokens: Optional[int], context: Context) -> str:
|
|
1079
454
|
try:
|
|
1080
|
-
return _do_diff_edit(fedit, max_tokens)
|
|
455
|
+
return _do_diff_edit(fedit, max_tokens, context)
|
|
1081
456
|
except Exception as e:
|
|
1082
457
|
# Try replacing \"
|
|
1083
458
|
try:
|
|
@@ -1087,24 +462,24 @@ def do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
|
1087
462
|
'\\"', '"'
|
|
1088
463
|
),
|
|
1089
464
|
)
|
|
1090
|
-
return _do_diff_edit(fedit, max_tokens)
|
|
465
|
+
return _do_diff_edit(fedit, max_tokens, context)
|
|
1091
466
|
except Exception:
|
|
1092
467
|
pass
|
|
1093
468
|
raise e
|
|
1094
469
|
|
|
1095
470
|
|
|
1096
|
-
def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
1097
|
-
console.log(f"Editing file: {fedit.file_path}")
|
|
471
|
+
def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int], context: Context) -> str:
|
|
472
|
+
context.console.log(f"Editing file: {fedit.file_path}")
|
|
1098
473
|
|
|
1099
474
|
if not os.path.isabs(fedit.file_path):
|
|
1100
475
|
raise Exception(
|
|
1101
|
-
f"Failure: file_path should be absolute path, current working directory is {
|
|
476
|
+
f"Failure: file_path should be absolute path, current working directory is {context.bash_state.cwd}"
|
|
1102
477
|
)
|
|
1103
478
|
else:
|
|
1104
|
-
path_ = expand_user(fedit.file_path
|
|
479
|
+
path_ = expand_user(fedit.file_path)
|
|
1105
480
|
|
|
1106
481
|
# Validate using file_edit_mode
|
|
1107
|
-
allowed_globs =
|
|
482
|
+
allowed_globs = context.bash_state.file_edit_mode.allowed_globs
|
|
1108
483
|
if allowed_globs != "all" and not any(
|
|
1109
484
|
fnmatch.fnmatch(path_, pattern) for pattern in allowed_globs
|
|
1110
485
|
):
|
|
@@ -1113,48 +488,25 @@ def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
|
1113
488
|
)
|
|
1114
489
|
|
|
1115
490
|
# The LLM is now aware that the file exists
|
|
1116
|
-
|
|
491
|
+
context.bash_state.add_to_whitelist_for_overwrite(path_)
|
|
1117
492
|
|
|
1118
|
-
if not
|
|
1119
|
-
|
|
1120
|
-
raise Exception(f"Error: file {path_} does not exist")
|
|
493
|
+
if not os.path.exists(path_):
|
|
494
|
+
raise Exception(f"Error: file {path_} does not exist")
|
|
1121
495
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
else:
|
|
1125
|
-
# Copy from docker
|
|
1126
|
-
with TemporaryDirectory() as tmpdir:
|
|
1127
|
-
rcode = os.system(
|
|
1128
|
-
f"docker cp {BASH_STATE.is_in_docker}:{shlex.quote(path_)} {tmpdir}"
|
|
1129
|
-
)
|
|
1130
|
-
if rcode != 0:
|
|
1131
|
-
raise Exception(f"Error: Read failed with code {rcode}")
|
|
1132
|
-
path_tmp = os.path.join(tmpdir, os.path.basename(path_))
|
|
1133
|
-
with open(path_tmp, "r") as f:
|
|
1134
|
-
apply_diff_to = f.read()
|
|
496
|
+
with open(path_) as f:
|
|
497
|
+
apply_diff_to = f.read()
|
|
1135
498
|
|
|
1136
499
|
fedit.file_edit_using_search_replace_blocks = (
|
|
1137
500
|
fedit.file_edit_using_search_replace_blocks.strip()
|
|
1138
501
|
)
|
|
1139
502
|
lines = fedit.file_edit_using_search_replace_blocks.split("\n")
|
|
1140
503
|
|
|
1141
|
-
apply_diff_to, comments = search_replace_edit(
|
|
504
|
+
apply_diff_to, comments = search_replace_edit(
|
|
505
|
+
lines, apply_diff_to, context.console.log
|
|
506
|
+
)
|
|
1142
507
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
f.write(apply_diff_to)
|
|
1146
|
-
else:
|
|
1147
|
-
with TemporaryDirectory() as tmpdir:
|
|
1148
|
-
path_tmp = os.path.join(tmpdir, os.path.basename(path_))
|
|
1149
|
-
with open(path_tmp, "w") as f:
|
|
1150
|
-
f.write(apply_diff_to)
|
|
1151
|
-
os.chmod(path_tmp, 0o777)
|
|
1152
|
-
# Copy to docker using docker cp
|
|
1153
|
-
rcode = os.system(
|
|
1154
|
-
f"docker cp {shlex.quote(path_tmp)} {BASH_STATE.is_in_docker}:{shlex.quote(path_)}"
|
|
1155
|
-
)
|
|
1156
|
-
if rcode != 0:
|
|
1157
|
-
raise Exception(f"Error: Write failed with code {rcode}")
|
|
508
|
+
with open(path_, "w") as f:
|
|
509
|
+
f.write(apply_diff_to)
|
|
1158
510
|
|
|
1159
511
|
syntax_errors = ""
|
|
1160
512
|
extension = Path(path_).suffix.lstrip(".")
|
|
@@ -1166,7 +518,7 @@ def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
|
1166
518
|
check.errors, apply_diff_to, max_tokens
|
|
1167
519
|
)
|
|
1168
520
|
|
|
1169
|
-
console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
521
|
+
context.console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
1170
522
|
return f"""{comments}
|
|
1171
523
|
---
|
|
1172
524
|
Tree-sitter reported syntax errors, please re-read the file and fix if there are any errors.
|
|
@@ -1181,45 +533,13 @@ Syntax errors:
|
|
|
1181
533
|
return comments
|
|
1182
534
|
|
|
1183
535
|
|
|
1184
|
-
class DoneFlag(BaseModel):
|
|
1185
|
-
task_output: str
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
def mark_finish(done: DoneFlag) -> DoneFlag:
|
|
1189
|
-
return done
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
class AIAssistant(BaseModel):
|
|
1193
|
-
instruction: str
|
|
1194
|
-
desired_output: str
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
def take_help_of_ai_assistant(
|
|
1198
|
-
aiassistant: AIAssistant,
|
|
1199
|
-
limit: float,
|
|
1200
|
-
loop_call: Callable[[str, float], tuple[str, float]],
|
|
1201
|
-
) -> tuple[str, float]:
|
|
1202
|
-
output, cost = loop_call(aiassistant.instruction, limit)
|
|
1203
|
-
return output, cost
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
536
|
TOOLS = (
|
|
1207
|
-
|
|
1208
|
-
| BashCommand
|
|
1209
|
-
| BashInteraction
|
|
1210
|
-
| ResetShell
|
|
537
|
+
BashCommand
|
|
1211
538
|
| WriteIfEmpty
|
|
1212
|
-
| FileEditFindReplace
|
|
1213
539
|
| FileEdit
|
|
1214
|
-
| AIAssistant
|
|
1215
|
-
| DoneFlag
|
|
1216
540
|
| ReadImage
|
|
1217
541
|
| ReadFiles
|
|
1218
542
|
| Initialize
|
|
1219
|
-
| Mouse
|
|
1220
|
-
| Keyboard
|
|
1221
|
-
| ScreenShot
|
|
1222
|
-
| GetScreenInfo
|
|
1223
543
|
| ContextSave
|
|
1224
544
|
)
|
|
1225
545
|
|
|
@@ -1230,161 +550,133 @@ def which_tool(args: str) -> TOOLS:
|
|
|
1230
550
|
|
|
1231
551
|
|
|
1232
552
|
def which_tool_name(name: str) -> Type[TOOLS]:
|
|
1233
|
-
if name == "
|
|
1234
|
-
return Confirmation
|
|
1235
|
-
elif name == "BashCommand":
|
|
553
|
+
if name == "BashCommand":
|
|
1236
554
|
return BashCommand
|
|
1237
|
-
elif name == "BashInteraction":
|
|
1238
|
-
return BashInteraction
|
|
1239
|
-
elif name == "ResetShell":
|
|
1240
|
-
return ResetShell
|
|
1241
555
|
elif name == "WriteIfEmpty":
|
|
1242
556
|
return WriteIfEmpty
|
|
1243
|
-
elif name == "FileEditFindReplace":
|
|
1244
|
-
return FileEditFindReplace
|
|
1245
557
|
elif name == "FileEdit":
|
|
1246
558
|
return FileEdit
|
|
1247
|
-
elif name == "AIAssistant":
|
|
1248
|
-
return AIAssistant
|
|
1249
|
-
elif name == "DoneFlag":
|
|
1250
|
-
return DoneFlag
|
|
1251
559
|
elif name == "ReadImage":
|
|
1252
560
|
return ReadImage
|
|
1253
561
|
elif name == "ReadFiles":
|
|
1254
562
|
return ReadFiles
|
|
1255
563
|
elif name == "Initialize":
|
|
1256
564
|
return Initialize
|
|
1257
|
-
elif name == "Mouse":
|
|
1258
|
-
return Mouse
|
|
1259
|
-
elif name == "Keyboard":
|
|
1260
|
-
return Keyboard
|
|
1261
|
-
elif name == "ScreenShot":
|
|
1262
|
-
return ScreenShot
|
|
1263
|
-
elif name == "GetScreenInfo":
|
|
1264
|
-
return GetScreenInfo
|
|
1265
565
|
elif name == "ContextSave":
|
|
1266
566
|
return ContextSave
|
|
1267
567
|
else:
|
|
1268
568
|
raise ValueError(f"Unknown tool name: {name}")
|
|
1269
569
|
|
|
1270
570
|
|
|
571
|
+
def parse_tool_by_name(name: str, arguments: dict[str, Any]) -> TOOLS:
|
|
572
|
+
tool_type = which_tool_name(name)
|
|
573
|
+
try:
|
|
574
|
+
return tool_type(**arguments)
|
|
575
|
+
except ValidationError:
|
|
576
|
+
|
|
577
|
+
def try_json(x: str) -> Any:
|
|
578
|
+
if not isinstance(x, str):
|
|
579
|
+
return x
|
|
580
|
+
try:
|
|
581
|
+
return json.loads(x)
|
|
582
|
+
except json.JSONDecodeError:
|
|
583
|
+
return x
|
|
584
|
+
|
|
585
|
+
return tool_type(**{k: try_json(v) for k, v in arguments.items()})
|
|
586
|
+
|
|
587
|
+
|
|
1271
588
|
TOOL_CALLS: list[TOOLS] = []
|
|
1272
589
|
|
|
1273
590
|
|
|
1274
591
|
def get_tool_output(
|
|
592
|
+
context: Context,
|
|
1275
593
|
args: dict[object, object] | TOOLS,
|
|
1276
|
-
enc:
|
|
594
|
+
enc: EncoderDecoder[int],
|
|
1277
595
|
limit: float,
|
|
1278
596
|
loop_call: Callable[[str, float], tuple[str, float]],
|
|
1279
597
|
max_tokens: Optional[int],
|
|
1280
|
-
) -> tuple[list[str | ImageData
|
|
1281
|
-
global
|
|
598
|
+
) -> tuple[list[str | ImageData], float]:
|
|
599
|
+
global TOOL_CALLS, INITIALIZED
|
|
1282
600
|
if isinstance(args, dict):
|
|
1283
601
|
adapter = TypeAdapter[TOOLS](TOOLS, config={"extra": "forbid"})
|
|
1284
602
|
arg = adapter.validate_python(args)
|
|
1285
603
|
else:
|
|
1286
604
|
arg = args
|
|
1287
|
-
output: tuple[str |
|
|
605
|
+
output: tuple[str | ImageData, float]
|
|
1288
606
|
TOOL_CALLS.append(arg)
|
|
1289
607
|
|
|
1290
|
-
if isinstance(arg,
|
|
1291
|
-
console.print("Calling
|
|
1292
|
-
output = ask_confirmation(arg), 0.0
|
|
1293
|
-
elif isinstance(arg, (BashCommand | BashInteraction)):
|
|
1294
|
-
console.print("Calling execute bash tool")
|
|
608
|
+
if isinstance(arg, BashCommand):
|
|
609
|
+
context.console.print("Calling execute bash tool")
|
|
1295
610
|
if not INITIALIZED:
|
|
1296
611
|
raise Exception("Initialize tool not called yet.")
|
|
1297
612
|
|
|
1298
|
-
output = execute_bash(
|
|
613
|
+
output = execute_bash(
|
|
614
|
+
context.bash_state, enc, arg, max_tokens, arg.wait_for_seconds
|
|
615
|
+
)
|
|
1299
616
|
elif isinstance(arg, WriteIfEmpty):
|
|
1300
|
-
console.print("Calling write file tool")
|
|
617
|
+
context.console.print("Calling write file tool")
|
|
1301
618
|
if not INITIALIZED:
|
|
1302
619
|
raise Exception("Initialize tool not called yet.")
|
|
1303
620
|
|
|
1304
|
-
output = write_file(arg, True, max_tokens), 0
|
|
621
|
+
output = write_file(arg, True, max_tokens, context), 0
|
|
1305
622
|
elif isinstance(arg, FileEdit):
|
|
1306
|
-
console.print("Calling full file edit tool")
|
|
623
|
+
context.console.print("Calling full file edit tool")
|
|
1307
624
|
if not INITIALIZED:
|
|
1308
625
|
raise Exception("Initialize tool not called yet.")
|
|
1309
626
|
|
|
1310
|
-
output = do_diff_edit(arg, max_tokens), 0.0
|
|
1311
|
-
elif isinstance(arg, DoneFlag):
|
|
1312
|
-
console.print("Calling mark finish tool")
|
|
1313
|
-
output = mark_finish(arg), 0.0
|
|
1314
|
-
elif isinstance(arg, AIAssistant):
|
|
1315
|
-
console.print("Calling AI assistant tool")
|
|
1316
|
-
output = take_help_of_ai_assistant(arg, limit, loop_call)
|
|
627
|
+
output = do_diff_edit(arg, max_tokens, context), 0.0
|
|
1317
628
|
elif isinstance(arg, ReadImage):
|
|
1318
|
-
console.print("Calling read image tool")
|
|
1319
|
-
output = read_image_from_shell(arg.file_path), 0.0
|
|
629
|
+
context.console.print("Calling read image tool")
|
|
630
|
+
output = read_image_from_shell(arg.file_path, context), 0.0
|
|
1320
631
|
elif isinstance(arg, ReadFiles):
|
|
1321
|
-
console.print("Calling read file tool")
|
|
1322
|
-
output = read_files(arg.file_paths, max_tokens), 0.0
|
|
1323
|
-
elif isinstance(arg, ResetShell):
|
|
1324
|
-
console.print("Calling reset shell tool")
|
|
1325
|
-
output = reset_shell(), 0.0
|
|
632
|
+
context.console.print("Calling read file tool")
|
|
633
|
+
output = read_files(arg.file_paths, max_tokens, context), 0.0
|
|
1326
634
|
elif isinstance(arg, Initialize):
|
|
1327
|
-
console.print("Calling initial info tool")
|
|
1328
|
-
|
|
1329
|
-
|
|
635
|
+
context.console.print("Calling initial info tool")
|
|
636
|
+
if arg.type == "user_asked_mode_change" or arg.type == "reset_shell":
|
|
637
|
+
workspace_path = (
|
|
638
|
+
arg.any_workspace_path
|
|
639
|
+
if os.path.isdir(arg.any_workspace_path)
|
|
640
|
+
else os.path.dirname(arg.any_workspace_path)
|
|
641
|
+
)
|
|
642
|
+
workspace_path = workspace_path if os.path.exists(workspace_path) else ""
|
|
643
|
+
output = (
|
|
644
|
+
reset_wcgw(
|
|
645
|
+
context,
|
|
646
|
+
workspace_path,
|
|
647
|
+
arg.mode_name
|
|
648
|
+
if is_mode_change(arg.mode, context.bash_state)
|
|
649
|
+
else None,
|
|
650
|
+
arg.mode,
|
|
651
|
+
),
|
|
652
|
+
0.0,
|
|
653
|
+
)
|
|
654
|
+
else:
|
|
655
|
+
output_, context = initialize(
|
|
656
|
+
arg.type,
|
|
657
|
+
context,
|
|
1330
658
|
arg.any_workspace_path,
|
|
1331
659
|
arg.initial_files_to_read,
|
|
1332
660
|
arg.task_id_to_resume,
|
|
1333
661
|
max_tokens,
|
|
1334
662
|
arg.mode,
|
|
1335
|
-
)
|
|
1336
|
-
0.0
|
|
1337
|
-
|
|
1338
|
-
elif isinstance(arg, (Mouse, Keyboard, ScreenShot, GetScreenInfo)):
|
|
1339
|
-
console.print(f"Calling {type(arg).__name__} tool")
|
|
1340
|
-
outputs_cost = run_computer_tool(arg), 0.0
|
|
1341
|
-
console.print(outputs_cost[0][0])
|
|
1342
|
-
outputs: list[ImageData | str | DoneFlag] = [outputs_cost[0][0]]
|
|
1343
|
-
imgBs64 = outputs_cost[0][1]
|
|
1344
|
-
if imgBs64:
|
|
1345
|
-
console.print("Captured screenshot")
|
|
1346
|
-
outputs.append(ImageData(media_type="image/png", data=imgBs64))
|
|
1347
|
-
if not BASH_STATE.is_in_docker and isinstance(arg, GetScreenInfo):
|
|
1348
|
-
try:
|
|
1349
|
-
# At this point we should go into the docker env
|
|
1350
|
-
res, _ = execute_bash(
|
|
1351
|
-
enc,
|
|
1352
|
-
BashCommand(
|
|
1353
|
-
command=f"docker exec -it {arg.docker_image_id} sh"
|
|
1354
|
-
),
|
|
1355
|
-
None,
|
|
1356
|
-
0.2,
|
|
1357
|
-
)
|
|
1358
|
-
# At this point we should go into the docker env
|
|
1359
|
-
res, _ = execute_bash(
|
|
1360
|
-
enc,
|
|
1361
|
-
BashInteraction(send_text=f"export PS1={BASH_STATE.prompt}"),
|
|
1362
|
-
None,
|
|
1363
|
-
0.2,
|
|
1364
|
-
)
|
|
1365
|
-
# Do chown of home dir
|
|
1366
|
-
except Exception as e:
|
|
1367
|
-
reset_shell()
|
|
1368
|
-
raise Exception(
|
|
1369
|
-
f"Some error happened while going inside docker. I've reset the shell. Please start again. Error {e}"
|
|
1370
|
-
)
|
|
1371
|
-
BASH_STATE.set_in_docker(arg.docker_image_id)
|
|
1372
|
-
return outputs, outputs_cost[1]
|
|
663
|
+
)
|
|
664
|
+
output = output_, 0.0
|
|
665
|
+
|
|
1373
666
|
elif isinstance(arg, ContextSave):
|
|
1374
|
-
console.print("Calling task memory tool")
|
|
1375
|
-
assert not BASH_STATE.is_in_docker, "KT not supported in docker"
|
|
667
|
+
context.console.print("Calling task memory tool")
|
|
1376
668
|
relevant_files = []
|
|
1377
669
|
warnings = ""
|
|
1378
670
|
for fglob in arg.relevant_file_globs:
|
|
1379
|
-
fglob = expand_user(fglob
|
|
671
|
+
fglob = expand_user(fglob)
|
|
1380
672
|
if not os.path.isabs(fglob) and arg.project_root_path:
|
|
1381
673
|
fglob = os.path.join(arg.project_root_path, fglob)
|
|
1382
674
|
globs = glob.glob(fglob, recursive=True)
|
|
1383
675
|
relevant_files.extend(globs[:1000])
|
|
1384
676
|
if not globs:
|
|
1385
677
|
warnings += f"Warning: No files found for the glob: {fglob}\n"
|
|
1386
|
-
relevant_files_data = read_files(relevant_files[:10_000], None)
|
|
1387
|
-
output_ = save_memory(arg, relevant_files_data,
|
|
678
|
+
relevant_files_data = read_files(relevant_files[:10_000], None, context)
|
|
679
|
+
output_ = save_memory(arg, relevant_files_data, context.bash_state.serialize())
|
|
1388
680
|
if not relevant_files and arg.relevant_file_globs:
|
|
1389
681
|
output_ = f'Error: No files found for the given globs. Context file successfully saved at "{output_}", but please fix the error.'
|
|
1390
682
|
elif warnings:
|
|
@@ -1393,108 +685,25 @@ def get_tool_output(
|
|
|
1393
685
|
else:
|
|
1394
686
|
raise ValueError(f"Unknown tool: {arg}")
|
|
1395
687
|
if isinstance(output[0], str):
|
|
1396
|
-
console.print(str(output[0]))
|
|
688
|
+
context.console.print(str(output[0]))
|
|
1397
689
|
else:
|
|
1398
|
-
console.print(f"Received {type(output[0])} from tool")
|
|
690
|
+
context.console.print(f"Received {type(output[0])} from tool")
|
|
1399
691
|
return [output[0]], output[1]
|
|
1400
692
|
|
|
1401
693
|
|
|
1402
694
|
History = list[ChatCompletionMessageParam]
|
|
1403
695
|
|
|
1404
|
-
default_enc
|
|
1405
|
-
"Xenova/claude-tokenizer"
|
|
1406
|
-
)
|
|
696
|
+
default_enc = get_default_encoder()
|
|
1407
697
|
curr_cost = 0.0
|
|
1408
698
|
|
|
1409
699
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
| BashInteraction
|
|
1414
|
-
| WriteIfEmpty
|
|
1415
|
-
| ResetShell
|
|
1416
|
-
| FileEditFindReplace
|
|
1417
|
-
| FileEdit
|
|
1418
|
-
| str
|
|
1419
|
-
| ReadFiles
|
|
1420
|
-
| Initialize
|
|
1421
|
-
| ContextSave
|
|
1422
|
-
)
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
def register_client(server_url: str, client_uuid: str = "") -> None:
|
|
1426
|
-
global default_enc, curr_cost
|
|
1427
|
-
# Generate a unique UUID for this client
|
|
1428
|
-
if not client_uuid:
|
|
1429
|
-
client_uuid = str(uuid.uuid4())
|
|
1430
|
-
|
|
1431
|
-
# Create the WebSocket connection
|
|
1432
|
-
try:
|
|
1433
|
-
with syncconnect(f"{server_url}/{client_uuid}") as websocket:
|
|
1434
|
-
server_version = str(websocket.recv())
|
|
1435
|
-
print(f"Server version: {server_version}")
|
|
1436
|
-
client_version = importlib.metadata.version("wcgw")
|
|
1437
|
-
websocket.send(client_version)
|
|
1438
|
-
|
|
1439
|
-
print(f"Connected. Share this user id with the chatbot: {client_uuid}")
|
|
1440
|
-
while True:
|
|
1441
|
-
# Wait to receive data from the server
|
|
1442
|
-
message = websocket.recv()
|
|
1443
|
-
mdata = Mdata.model_validate_json(message)
|
|
1444
|
-
if isinstance(mdata.data, str):
|
|
1445
|
-
raise Exception(mdata)
|
|
1446
|
-
try:
|
|
1447
|
-
outputs, cost = get_tool_output(
|
|
1448
|
-
mdata.data, default_enc, 0.0, lambda x, y: ("", 0), 8000
|
|
1449
|
-
)
|
|
1450
|
-
output = outputs[0]
|
|
1451
|
-
curr_cost += cost
|
|
1452
|
-
print(f"{curr_cost=}")
|
|
1453
|
-
except Exception as e:
|
|
1454
|
-
output = f"GOT EXCEPTION while calling tool. Error: {e}"
|
|
1455
|
-
console.print(traceback.format_exc())
|
|
1456
|
-
assert isinstance(output, str)
|
|
1457
|
-
websocket.send(output)
|
|
1458
|
-
|
|
1459
|
-
except (websockets.ConnectionClosed, ConnectionError, OSError):
|
|
1460
|
-
print(f"Connection closed for UUID: {client_uuid}, retrying")
|
|
1461
|
-
time.sleep(0.5)
|
|
1462
|
-
register_client(server_url, client_uuid)
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
run = Typer(pretty_exceptions_show_locals=False, no_args_is_help=True)
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
@run.command()
|
|
1469
|
-
def app(
|
|
1470
|
-
server_url: str = "",
|
|
1471
|
-
client_uuid: Optional[str] = None,
|
|
1472
|
-
version: bool = typer.Option(False, "--version", "-v"),
|
|
1473
|
-
) -> None:
|
|
1474
|
-
if version:
|
|
1475
|
-
version_ = importlib.metadata.version("wcgw")
|
|
1476
|
-
print(f"wcgw version: {version_}")
|
|
1477
|
-
exit()
|
|
1478
|
-
if not server_url:
|
|
1479
|
-
server_url = os.environ.get("WCGW_RELAY_SERVER", "")
|
|
1480
|
-
if not server_url:
|
|
1481
|
-
print(
|
|
1482
|
-
"Error: Please provide relay server url using --server_url or WCGW_RELAY_SERVER environment variable"
|
|
1483
|
-
)
|
|
1484
|
-
print(
|
|
1485
|
-
"\tNOTE: you need to run a relay server first, author doesn't host a relay server anymore."
|
|
1486
|
-
)
|
|
1487
|
-
print("\thttps://github.com/rusiaaman/wcgw/blob/main/openai.md")
|
|
1488
|
-
print("\tExample `--server-url=ws://localhost:8000/v1/register`")
|
|
1489
|
-
raise typer.Exit(1)
|
|
1490
|
-
register_client(server_url, client_uuid or "")
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
def read_files(file_paths: list[str], max_tokens: Optional[int]) -> str:
|
|
700
|
+
def read_files(
|
|
701
|
+
file_paths: list[str], max_tokens: Optional[int], context: Context
|
|
702
|
+
) -> str:
|
|
1494
703
|
message = ""
|
|
1495
704
|
for i, file in enumerate(file_paths):
|
|
1496
705
|
try:
|
|
1497
|
-
content, truncated, tokens = read_file(file, max_tokens)
|
|
706
|
+
content, truncated, tokens = read_file(file, max_tokens, context)
|
|
1498
707
|
except Exception as e:
|
|
1499
708
|
message += f"\n{file}: {str(e)}\n"
|
|
1500
709
|
continue
|
|
@@ -1515,46 +724,37 @@ def read_files(file_paths: list[str], max_tokens: Optional[int]) -> str:
|
|
|
1515
724
|
return message
|
|
1516
725
|
|
|
1517
726
|
|
|
1518
|
-
def read_file(
|
|
1519
|
-
|
|
727
|
+
def read_file(
|
|
728
|
+
file_path: str, max_tokens: Optional[int], context: Context
|
|
729
|
+
) -> tuple[str, bool, int]:
|
|
730
|
+
context.console.print(f"Reading file: {file_path}")
|
|
1520
731
|
|
|
1521
732
|
# Expand the path before checking if it's absolute
|
|
1522
|
-
file_path = expand_user(file_path
|
|
733
|
+
file_path = expand_user(file_path)
|
|
1523
734
|
|
|
1524
735
|
if not os.path.isabs(file_path):
|
|
1525
736
|
raise ValueError(
|
|
1526
|
-
f"Failure: file_path should be absolute path, current working directory is {
|
|
737
|
+
f"Failure: file_path should be absolute path, current working directory is {context.bash_state.cwd}"
|
|
1527
738
|
)
|
|
1528
739
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
if not BASH_STATE.is_in_docker:
|
|
1532
|
-
path = Path(file_path)
|
|
1533
|
-
if not path.exists():
|
|
1534
|
-
raise ValueError(f"Error: file {file_path} does not exist")
|
|
740
|
+
context.bash_state.add_to_whitelist_for_overwrite(file_path)
|
|
1535
741
|
|
|
1536
|
-
|
|
1537
|
-
|
|
742
|
+
path = Path(file_path)
|
|
743
|
+
if not path.exists():
|
|
744
|
+
raise ValueError(f"Error: file {file_path} does not exist")
|
|
1538
745
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(file_path)}",
|
|
1542
|
-
timeout=TIMEOUT,
|
|
1543
|
-
)
|
|
1544
|
-
if return_code != 0:
|
|
1545
|
-
raise Exception(
|
|
1546
|
-
f"Error: cat {file_path} failed with code {return_code}\nstdout: {content}\nstderr: {stderr}"
|
|
1547
|
-
)
|
|
746
|
+
with path.open("r") as f:
|
|
747
|
+
content = f.read(10_000_000)
|
|
1548
748
|
|
|
1549
749
|
truncated = False
|
|
1550
750
|
tokens_counts = 0
|
|
1551
751
|
if max_tokens is not None:
|
|
1552
|
-
tokens = default_enc.
|
|
752
|
+
tokens = default_enc.encoder(content)
|
|
1553
753
|
tokens_counts = len(tokens)
|
|
1554
754
|
if len(tokens) > max_tokens:
|
|
1555
|
-
content = default_enc.
|
|
755
|
+
content = default_enc.decoder(tokens[:max_tokens])
|
|
1556
756
|
rest = save_out_of_context(
|
|
1557
|
-
default_enc.
|
|
757
|
+
default_enc.decoder(tokens[max_tokens:]), Path(file_path).suffix
|
|
1558
758
|
)
|
|
1559
759
|
content += f"\n(...truncated)\n---\nI've saved the continuation in a new file. Please read: `{rest}`"
|
|
1560
760
|
truncated = True
|