wcgw 0.2.0__py3-none-any.whl → 1.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 +2 -2
- wcgw/client/__main__.py +3 -0
- wcgw/{basic.py → client/basic.py} +16 -12
- wcgw/{tools.py → client/tools.py} +248 -132
- wcgw/relay/serve.py +316 -0
- wcgw/relay/static/privacy.txt +7 -0
- wcgw/types_.py +37 -0
- {wcgw-0.2.0.dist-info → wcgw-1.0.1.dist-info}/METADATA +7 -4
- wcgw-1.0.1.dist-info/RECORD +15 -0
- {wcgw-0.2.0.dist-info → wcgw-1.0.1.dist-info}/entry_points.txt +1 -0
- wcgw/__main__.py +0 -3
- wcgw-0.2.0.dist-info/RECORD +0 -12
- /wcgw/{claude.py → client/claude.py} +0 -0
- /wcgw/{common.py → client/common.py} +0 -0
- /wcgw/{openai_adapters.py → client/openai_adapters.py} +0 -0
- /wcgw/{openai_utils.py → client/openai_utils.py} +0 -0
- {wcgw-0.2.0.dist-info → wcgw-1.0.1.dist-info}/WHEEL +0 -0
wcgw/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
from .basic import app, loop
|
|
2
|
-
from .tools import run as listen
|
|
1
|
+
from .client.basic import app, loop
|
|
2
|
+
from .client.tools import run as listen
|
wcgw/client/__main__.py
ADDED
|
@@ -20,16 +20,15 @@ import petname # type: ignore[import-untyped]
|
|
|
20
20
|
from typer import Typer
|
|
21
21
|
import uuid
|
|
22
22
|
|
|
23
|
+
from ..types_ import BashCommand, BashInteraction, ReadImage, Writefile, ResetShell
|
|
24
|
+
|
|
23
25
|
from .common import Models, discard_input
|
|
24
26
|
from .common import CostData, History
|
|
25
27
|
from .openai_utils import get_input_cost, get_output_cost
|
|
26
|
-
from .tools import
|
|
28
|
+
from .tools import ImageData
|
|
27
29
|
|
|
28
30
|
from .tools import (
|
|
29
|
-
BASH_CLF_OUTPUT,
|
|
30
|
-
Confirmation,
|
|
31
31
|
DoneFlag,
|
|
32
|
-
Writefile,
|
|
33
32
|
get_tool_output,
|
|
34
33
|
SHELL,
|
|
35
34
|
start_shell,
|
|
@@ -156,27 +155,32 @@ def loop(
|
|
|
156
155
|
|
|
157
156
|
tools = [
|
|
158
157
|
openai.pydantic_function_tool(
|
|
159
|
-
|
|
158
|
+
BashCommand,
|
|
160
159
|
description="""
|
|
161
|
-
- Execute a bash
|
|
162
|
-
- Execute commands using `execute_command` attribute. You can run python/node/other REPL code lines using `execute_command` too.
|
|
160
|
+
- Execute a bash command. This is stateful (beware with subsequent calls).
|
|
163
161
|
- Do not use interactive commands like nano. Prefer writing simpler commands.
|
|
164
|
-
-
|
|
165
|
-
- The last line is `(pending)` if the program is still running or waiting for your input. You can then send input using `send_ascii` attributes. You get status by sending new line `send_ascii: ["Enter"]` or `send_ascii: [10]`.
|
|
166
|
-
- Optionally the last line is `(won't exit)` in which case you need to kill the process if you want to run a new command.
|
|
162
|
+
- Status of the command and the current working directory will always be returned at the end.
|
|
167
163
|
- Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
|
|
168
164
|
- The first line might be `(...truncated)` if the output is too long.
|
|
169
165
|
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
|
|
170
|
-
- You can run python/node/other REPL code lines using `execute_command` too. NOTE: `execute_command` doesn't create a new shell, it uses the same shell.
|
|
171
166
|
""",
|
|
167
|
+
),
|
|
168
|
+
openai.pydantic_function_tool(
|
|
169
|
+
BashInteraction,
|
|
170
|
+
description="""
|
|
171
|
+
- Interact with running program using this tool.""",
|
|
172
172
|
),
|
|
173
173
|
openai.pydantic_function_tool(
|
|
174
174
|
Writefile,
|
|
175
|
-
description="Write content to a file. Provide file path and content. Use this instead of
|
|
175
|
+
description="Write content to a file. Provide file path and content. Use this instead of BashCommand for writing files.",
|
|
176
176
|
),
|
|
177
177
|
openai.pydantic_function_tool(
|
|
178
178
|
ReadImage, description="Read an image from the shell."
|
|
179
179
|
),
|
|
180
|
+
openai.pydantic_function_tool(
|
|
181
|
+
ResetShell,
|
|
182
|
+
description="Resets the shell. Use only if all interrupts and prompt reset attempts have failed repeatedly.",
|
|
183
|
+
),
|
|
180
184
|
]
|
|
181
185
|
uname_sysname = os.uname().sysname
|
|
182
186
|
uname_machine = os.uname().machine
|
|
@@ -2,9 +2,11 @@ import asyncio
|
|
|
2
2
|
import base64
|
|
3
3
|
import json
|
|
4
4
|
import mimetypes
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
import re
|
|
6
7
|
import sys
|
|
7
8
|
import threading
|
|
9
|
+
import importlib.metadata
|
|
8
10
|
import traceback
|
|
9
11
|
from typing import (
|
|
10
12
|
Callable,
|
|
@@ -12,12 +14,12 @@ from typing import (
|
|
|
12
14
|
NewType,
|
|
13
15
|
Optional,
|
|
14
16
|
ParamSpec,
|
|
15
|
-
Sequence,
|
|
16
17
|
TypeVar,
|
|
17
18
|
TypedDict,
|
|
18
19
|
)
|
|
19
20
|
import uuid
|
|
20
21
|
from pydantic import BaseModel, TypeAdapter
|
|
22
|
+
import typer
|
|
21
23
|
from websockets.sync.client import connect as syncconnect
|
|
22
24
|
|
|
23
25
|
import os
|
|
@@ -39,6 +41,14 @@ from openai.types.chat import (
|
|
|
39
41
|
ChatCompletionMessage,
|
|
40
42
|
ParsedChatCompletionMessage,
|
|
41
43
|
)
|
|
44
|
+
from nltk.metrics.distance import edit_distance
|
|
45
|
+
from ..types_ import FileEditFindReplace, ResetShell, Writefile
|
|
46
|
+
|
|
47
|
+
from ..types_ import BashCommand
|
|
48
|
+
|
|
49
|
+
from ..types_ import BashInteraction
|
|
50
|
+
|
|
51
|
+
from ..types_ import ReadImage
|
|
42
52
|
|
|
43
53
|
from .common import CostData, Models, discard_input
|
|
44
54
|
|
|
@@ -73,15 +83,10 @@ def ask_confirmation(prompt: Confirmation) -> str:
|
|
|
73
83
|
return "Yes" if response.lower() == "y" else "No"
|
|
74
84
|
|
|
75
85
|
|
|
76
|
-
class Writefile(BaseModel):
|
|
77
|
-
file_path: str
|
|
78
|
-
file_content: str
|
|
79
|
-
|
|
80
|
-
|
|
81
86
|
PROMPT = "#@@"
|
|
82
87
|
|
|
83
88
|
|
|
84
|
-
def start_shell() -> pexpect.spawn:
|
|
89
|
+
def start_shell() -> pexpect.spawn: # type: ignore
|
|
85
90
|
SHELL = pexpect.spawn(
|
|
86
91
|
"/bin/bash --noprofile --norc",
|
|
87
92
|
env={**os.environ, **{"PS1": PROMPT}}, # type: ignore[arg-type]
|
|
@@ -133,22 +138,26 @@ def _get_exit_code() -> int:
|
|
|
133
138
|
raise ValueError(f"Malformed output: {before}")
|
|
134
139
|
|
|
135
140
|
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
142
|
+
BASH_STATE: BASH_CLF_OUTPUT = "repl"
|
|
143
|
+
CWD = os.getcwd()
|
|
138
144
|
|
|
139
|
-
class ExecuteBash(BaseModel):
|
|
140
|
-
execute_command: Optional[str] = None
|
|
141
|
-
send_ascii: Optional[Sequence[int | Specials]] = None
|
|
142
145
|
|
|
146
|
+
def reset_shell() -> str:
|
|
147
|
+
global SHELL, BASH_STATE, CWD
|
|
148
|
+
SHELL.close(True)
|
|
149
|
+
SHELL = start_shell()
|
|
150
|
+
BASH_STATE = "repl"
|
|
151
|
+
CWD = os.getcwd()
|
|
152
|
+
return "Reset successful" + get_status()
|
|
143
153
|
|
|
144
|
-
BASH_CLF_OUTPUT = Literal["running", "waiting_for_input", "wont_exit"]
|
|
145
|
-
BASH_STATE: BASH_CLF_OUTPUT = "running"
|
|
146
154
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run multiple shell sessions, likely a previous program hasn't exited.
|
|
156
|
+
1. Get its output using `send_ascii: [10] or send_specials: ["Enter"]`
|
|
157
|
+
2. Use `send_ascii` or `send_specials` to give inputs to the running program, don't use `BashCommand` OR
|
|
158
|
+
3. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
|
|
159
|
+
4. Send the process in background using `send_specials: ["Ctrl-z"]` followed by BashCommand: `bg`
|
|
160
|
+
"""
|
|
152
161
|
|
|
153
162
|
|
|
154
163
|
def update_repl_prompt(command: str) -> bool:
|
|
@@ -173,45 +182,66 @@ def update_repl_prompt(command: str) -> bool:
|
|
|
173
182
|
return False
|
|
174
183
|
|
|
175
184
|
|
|
185
|
+
def get_cwd() -> str:
|
|
186
|
+
SHELL.sendline("pwd")
|
|
187
|
+
SHELL.expect(PROMPT)
|
|
188
|
+
assert isinstance(SHELL.before, str)
|
|
189
|
+
current_dir = render_terminal_output(SHELL.before).strip()
|
|
190
|
+
return current_dir
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_status() -> str:
|
|
194
|
+
global CWD
|
|
195
|
+
exit_code: Optional[int] = None
|
|
196
|
+
|
|
197
|
+
status = "\n\n---\n\n"
|
|
198
|
+
if BASH_STATE == "pending":
|
|
199
|
+
status += "status = still running\n"
|
|
200
|
+
status += "cwd = " + CWD + "\n"
|
|
201
|
+
else:
|
|
202
|
+
exit_code = _get_exit_code()
|
|
203
|
+
status += f"status = exited with code {exit_code}\n"
|
|
204
|
+
CWD = get_cwd()
|
|
205
|
+
status += "cwd = " + CWD + "\n"
|
|
206
|
+
|
|
207
|
+
return status.rstrip()
|
|
208
|
+
|
|
209
|
+
|
|
176
210
|
def execute_bash(
|
|
177
|
-
enc: tiktoken.Encoding,
|
|
211
|
+
enc: tiktoken.Encoding,
|
|
212
|
+
bash_arg: BashCommand | BashInteraction,
|
|
213
|
+
max_tokens: Optional[int],
|
|
178
214
|
) -> tuple[str, float]:
|
|
179
|
-
global SHELL, BASH_STATE
|
|
215
|
+
global SHELL, BASH_STATE, CWD
|
|
180
216
|
try:
|
|
181
217
|
is_interrupt = False
|
|
182
|
-
if bash_arg
|
|
183
|
-
updated_repl_mode = update_repl_prompt(bash_arg.
|
|
218
|
+
if isinstance(bash_arg, BashCommand):
|
|
219
|
+
updated_repl_mode = update_repl_prompt(bash_arg.command)
|
|
184
220
|
if updated_repl_mode:
|
|
185
|
-
BASH_STATE = "
|
|
186
|
-
response =
|
|
221
|
+
BASH_STATE = "repl"
|
|
222
|
+
response = (
|
|
223
|
+
"Prompt updated, you can execute REPL lines using BashCommand now"
|
|
224
|
+
)
|
|
187
225
|
console.print(response)
|
|
188
226
|
return (
|
|
189
227
|
response,
|
|
190
228
|
0,
|
|
191
229
|
)
|
|
192
230
|
|
|
193
|
-
console.print(f"$ {bash_arg.
|
|
194
|
-
if BASH_STATE == "
|
|
231
|
+
console.print(f"$ {bash_arg.command}")
|
|
232
|
+
if BASH_STATE == "pending":
|
|
195
233
|
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
196
|
-
|
|
197
|
-
raise ValueError(
|
|
198
|
-
"""A command is already running that hasn't exited. NOTE: You can't run multiple shell sessions, likely a previous program is in infinite loop.
|
|
199
|
-
Kill the previous program by sending ctrl+c first using `send_ascii`"""
|
|
200
|
-
)
|
|
201
|
-
command = bash_arg.execute_command.strip()
|
|
234
|
+
command = bash_arg.command.strip()
|
|
202
235
|
|
|
203
236
|
if "\n" in command:
|
|
204
237
|
raise ValueError(
|
|
205
238
|
"Command should not contain newline character in middle. Run only one command at a time."
|
|
206
239
|
)
|
|
240
|
+
|
|
207
241
|
SHELL.sendline(command)
|
|
208
|
-
elif bash_arg.
|
|
209
|
-
console.print(f"Sending
|
|
210
|
-
for char in bash_arg.
|
|
211
|
-
if isinstance(char, int):
|
|
212
|
-
SHELL.send(chr(char))
|
|
213
|
-
if char == 3:
|
|
214
|
-
is_interrupt = True
|
|
242
|
+
elif bash_arg.send_specials:
|
|
243
|
+
console.print(f"Sending special sequence: {bash_arg.send_specials}")
|
|
244
|
+
for char in bash_arg.send_specials:
|
|
215
245
|
if char == "Key-up":
|
|
216
246
|
SHELL.send("\033[A")
|
|
217
247
|
elif char == "Key-down":
|
|
@@ -225,21 +255,52 @@ def execute_bash(
|
|
|
225
255
|
elif char == "Ctrl-c":
|
|
226
256
|
SHELL.sendintr()
|
|
227
257
|
is_interrupt = True
|
|
258
|
+
elif char == "Ctrl-d":
|
|
259
|
+
SHELL.sendintr()
|
|
260
|
+
is_interrupt = True
|
|
261
|
+
elif char == "Ctrl-z":
|
|
262
|
+
SHELL.send("\x1a")
|
|
263
|
+
else:
|
|
264
|
+
raise Exception(f"Unknown special character: {char}")
|
|
265
|
+
elif bash_arg.send_ascii:
|
|
266
|
+
console.print(f"Sending ASCII sequence: {bash_arg.send_ascii}")
|
|
267
|
+
for ascii_char in bash_arg.send_ascii:
|
|
268
|
+
SHELL.send(chr(ascii_char))
|
|
269
|
+
if ascii_char == 3:
|
|
270
|
+
is_interrupt = True
|
|
228
271
|
else:
|
|
229
|
-
|
|
230
|
-
|
|
272
|
+
if bash_arg.send_text is None:
|
|
273
|
+
return (
|
|
274
|
+
"Failure: at least one of send_text, send_specials or send_ascii should be provided",
|
|
275
|
+
0.0,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
updated_repl_mode = update_repl_prompt(bash_arg.send_text)
|
|
279
|
+
if updated_repl_mode:
|
|
280
|
+
BASH_STATE = "repl"
|
|
281
|
+
response = (
|
|
282
|
+
"Prompt updated, you can execute REPL lines using BashCommand now"
|
|
283
|
+
)
|
|
284
|
+
console.print(response)
|
|
285
|
+
return (
|
|
286
|
+
response,
|
|
287
|
+
0,
|
|
288
|
+
)
|
|
289
|
+
console.print(f"Interact text: {bash_arg.send_text}")
|
|
290
|
+
SHELL.sendline(bash_arg.send_text)
|
|
291
|
+
|
|
292
|
+
BASH_STATE = "repl"
|
|
231
293
|
|
|
232
294
|
except KeyboardInterrupt:
|
|
233
|
-
SHELL.
|
|
234
|
-
SHELL
|
|
235
|
-
|
|
295
|
+
SHELL.sendintr()
|
|
296
|
+
SHELL.expect(PROMPT)
|
|
297
|
+
return "---\n\nFailure: user interrupted the execution", 0.0
|
|
236
298
|
|
|
237
299
|
wait = 5
|
|
238
300
|
index = SHELL.expect([PROMPT, pexpect.TIMEOUT], timeout=wait)
|
|
239
301
|
if index == 1:
|
|
240
|
-
BASH_STATE = "
|
|
302
|
+
BASH_STATE = "pending"
|
|
241
303
|
text = SHELL.before or ""
|
|
242
|
-
print(text)
|
|
243
304
|
|
|
244
305
|
text = render_terminal_output(text)
|
|
245
306
|
tokens = enc.encode(text)
|
|
@@ -247,23 +308,26 @@ def execute_bash(
|
|
|
247
308
|
if max_tokens and len(tokens) >= max_tokens:
|
|
248
309
|
text = "...(truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
|
|
249
310
|
|
|
250
|
-
last_line = "(pending)"
|
|
251
|
-
text = text + f"\n{last_line}"
|
|
252
|
-
|
|
253
311
|
if is_interrupt:
|
|
254
312
|
text = (
|
|
255
313
|
text
|
|
256
|
-
+ """
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
314
|
+
+ """---
|
|
315
|
+
----
|
|
316
|
+
Failure interrupting.
|
|
317
|
+
If any REPL session was previously running or if bashrc was sourced, or if there is issue to other REPL related reasons:
|
|
318
|
+
Run BashCommand: "wcgw_update_prompt()" to reset the PS1 prompt.
|
|
319
|
+
Otherwise, you may want to try Ctrl-c again or program specific exit interactive commands.
|
|
262
320
|
"""
|
|
263
321
|
)
|
|
264
322
|
|
|
323
|
+
exit_status = get_status()
|
|
324
|
+
text += exit_status
|
|
325
|
+
|
|
265
326
|
return text, 0
|
|
266
327
|
|
|
328
|
+
if is_interrupt:
|
|
329
|
+
return "Interrupt successful", 0.0
|
|
330
|
+
|
|
267
331
|
assert isinstance(SHELL.before, str)
|
|
268
332
|
output = render_terminal_output(SHELL.before)
|
|
269
333
|
|
|
@@ -272,9 +336,8 @@ If no:
|
|
|
272
336
|
output = "...(truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
|
|
273
337
|
|
|
274
338
|
try:
|
|
275
|
-
|
|
276
|
-
output +=
|
|
277
|
-
|
|
339
|
+
exit_status = get_status()
|
|
340
|
+
output += exit_status
|
|
278
341
|
except ValueError as e:
|
|
279
342
|
console.print(output)
|
|
280
343
|
traceback.print_exc()
|
|
@@ -286,11 +349,6 @@ If no:
|
|
|
286
349
|
return output, 0
|
|
287
350
|
|
|
288
351
|
|
|
289
|
-
class ReadImage(BaseModel):
|
|
290
|
-
file_path: str
|
|
291
|
-
type: Literal["ReadImage"] = "ReadImage"
|
|
292
|
-
|
|
293
|
-
|
|
294
352
|
def serve_image_in_bg(file_path: str, client_uuid: str, name: str) -> None:
|
|
295
353
|
if not client_uuid:
|
|
296
354
|
client_uuid = str(uuid.uuid4())
|
|
@@ -323,25 +381,17 @@ T = TypeVar("T")
|
|
|
323
381
|
def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
|
|
324
382
|
def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
|
|
325
383
|
global BASH_STATE
|
|
326
|
-
if BASH_STATE == "
|
|
384
|
+
if BASH_STATE == "pending":
|
|
327
385
|
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
328
|
-
|
|
329
|
-
raise ValueError(
|
|
330
|
-
"A command is already running that hasn't exited. NOTE: You can't run multiple shell sessions, likely the previous program is in infinite loop. Please kill the previous program by sending ctrl+c first."
|
|
331
|
-
)
|
|
386
|
+
|
|
332
387
|
return func(*args, **kwargs)
|
|
333
388
|
|
|
334
389
|
return wrapper
|
|
335
390
|
|
|
336
391
|
|
|
337
|
-
@ensure_no_previous_output
|
|
338
392
|
def read_image_from_shell(file_path: str) -> ImageData:
|
|
339
393
|
if not os.path.isabs(file_path):
|
|
340
|
-
|
|
341
|
-
SHELL.expect(PROMPT)
|
|
342
|
-
assert isinstance(SHELL.before, str)
|
|
343
|
-
current_dir = render_terminal_output(SHELL.before).strip()
|
|
344
|
-
file_path = os.path.join(current_dir, file_path)
|
|
394
|
+
file_path = os.path.join(CWD, file_path)
|
|
345
395
|
|
|
346
396
|
if not os.path.exists(file_path):
|
|
347
397
|
raise ValueError(f"File {file_path} does not exist")
|
|
@@ -353,22 +403,69 @@ def read_image_from_shell(file_path: str) -> ImageData:
|
|
|
353
403
|
return ImageData(dataurl=f"data:{image_type};base64,{image_b64}")
|
|
354
404
|
|
|
355
405
|
|
|
356
|
-
@ensure_no_previous_output
|
|
357
406
|
def write_file(writefile: Writefile) -> str:
|
|
358
407
|
if not os.path.isabs(writefile.file_path):
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
408
|
+
return "Failure: file_path should be absolute path"
|
|
409
|
+
else:
|
|
410
|
+
path_ = writefile.file_path
|
|
411
|
+
|
|
412
|
+
path = Path(path_)
|
|
413
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
414
|
+
|
|
365
415
|
try:
|
|
366
|
-
with open(
|
|
416
|
+
with path.open("w") as f:
|
|
367
417
|
f.write(writefile.file_content)
|
|
368
418
|
except OSError as e:
|
|
369
|
-
console.print(f"Error: {e}", style="red")
|
|
370
419
|
return f"Error: {e}"
|
|
371
|
-
console.print(f"File written to {
|
|
420
|
+
console.print(f"File written to {path_}")
|
|
421
|
+
return "Success"
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def find_least_edit_distance_substring(content: str, find_str: str) -> str:
|
|
425
|
+
content_lines = content.split("\n")
|
|
426
|
+
find_lines = find_str.split("\n")
|
|
427
|
+
# Slide window and find one with sum of edit distance least
|
|
428
|
+
min_edit_distance = float("inf")
|
|
429
|
+
min_edit_distance_lines = []
|
|
430
|
+
for i in range(len(content_lines) - len(find_lines) + 1):
|
|
431
|
+
edit_distance_sum = 0
|
|
432
|
+
for j in range(len(find_lines)):
|
|
433
|
+
edit_distance_sum += edit_distance(content_lines[i + j], find_lines[j])
|
|
434
|
+
if edit_distance_sum < min_edit_distance:
|
|
435
|
+
min_edit_distance = edit_distance_sum
|
|
436
|
+
min_edit_distance_lines = content_lines[i : i + len(find_lines)]
|
|
437
|
+
return "\n".join(min_edit_distance_lines)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def file_edit(file_edit: FileEditFindReplace) -> str:
|
|
441
|
+
if not os.path.isabs(file_edit.file_path):
|
|
442
|
+
path_ = os.path.join(CWD, file_edit.file_path)
|
|
443
|
+
else:
|
|
444
|
+
path_ = file_edit.file_path
|
|
445
|
+
|
|
446
|
+
out_string = "\n".join("> " + line for line in file_edit.find_lines.split("\n"))
|
|
447
|
+
in_string = "\n".join(
|
|
448
|
+
"< " + line for line in file_edit.replace_with_lines.split("\n")
|
|
449
|
+
)
|
|
450
|
+
console.log(f"Editing file: {path_}\n---\n{out_string}\n---\n{in_string}\n---")
|
|
451
|
+
try:
|
|
452
|
+
with open(path_) as f:
|
|
453
|
+
content = f.read()
|
|
454
|
+
# First find counts
|
|
455
|
+
count = content.count(file_edit.find_lines)
|
|
456
|
+
|
|
457
|
+
if count == 0:
|
|
458
|
+
closest_match = find_least_edit_distance_substring(
|
|
459
|
+
content, file_edit.find_lines
|
|
460
|
+
)
|
|
461
|
+
return f"Error: no match found for the provided `find_lines` in the file. Closest match:\n---\n{closest_match}\n---\nFile not edited"
|
|
462
|
+
|
|
463
|
+
content = content.replace(file_edit.find_lines, file_edit.replace_with_lines)
|
|
464
|
+
with open(path_, "w") as f:
|
|
465
|
+
f.write(content)
|
|
466
|
+
except OSError as e:
|
|
467
|
+
return f"Error: {e}"
|
|
468
|
+
console.print(f"File written to {path_}")
|
|
372
469
|
return "Success"
|
|
373
470
|
|
|
374
471
|
|
|
@@ -396,16 +493,37 @@ def take_help_of_ai_assistant(
|
|
|
396
493
|
|
|
397
494
|
def which_tool(args: str) -> BaseModel:
|
|
398
495
|
adapter = TypeAdapter[
|
|
399
|
-
Confirmation
|
|
400
|
-
|
|
496
|
+
Confirmation
|
|
497
|
+
| BashCommand
|
|
498
|
+
| BashInteraction
|
|
499
|
+
| ResetShell
|
|
500
|
+
| Writefile
|
|
501
|
+
| FileEditFindReplace
|
|
502
|
+
| AIAssistant
|
|
503
|
+
| DoneFlag
|
|
504
|
+
| ReadImage
|
|
505
|
+
](
|
|
506
|
+
Confirmation
|
|
507
|
+
| BashCommand
|
|
508
|
+
| BashInteraction
|
|
509
|
+
| ResetShell
|
|
510
|
+
| Writefile
|
|
511
|
+
| FileEditFindReplace
|
|
512
|
+
| AIAssistant
|
|
513
|
+
| DoneFlag
|
|
514
|
+
| ReadImage
|
|
515
|
+
)
|
|
401
516
|
return adapter.validate_python(json.loads(args))
|
|
402
517
|
|
|
403
518
|
|
|
404
519
|
def get_tool_output(
|
|
405
520
|
args: dict[object, object]
|
|
406
521
|
| Confirmation
|
|
407
|
-
|
|
|
522
|
+
| BashCommand
|
|
523
|
+
| BashInteraction
|
|
524
|
+
| ResetShell
|
|
408
525
|
| Writefile
|
|
526
|
+
| FileEditFindReplace
|
|
409
527
|
| AIAssistant
|
|
410
528
|
| DoneFlag
|
|
411
529
|
| ReadImage,
|
|
@@ -416,8 +534,26 @@ def get_tool_output(
|
|
|
416
534
|
) -> tuple[str | ImageData | DoneFlag, float]:
|
|
417
535
|
if isinstance(args, dict):
|
|
418
536
|
adapter = TypeAdapter[
|
|
419
|
-
Confirmation
|
|
420
|
-
|
|
537
|
+
Confirmation
|
|
538
|
+
| BashCommand
|
|
539
|
+
| BashInteraction
|
|
540
|
+
| ResetShell
|
|
541
|
+
| Writefile
|
|
542
|
+
| FileEditFindReplace
|
|
543
|
+
| AIAssistant
|
|
544
|
+
| DoneFlag
|
|
545
|
+
| ReadImage
|
|
546
|
+
](
|
|
547
|
+
Confirmation
|
|
548
|
+
| BashCommand
|
|
549
|
+
| BashInteraction
|
|
550
|
+
| ResetShell
|
|
551
|
+
| Writefile
|
|
552
|
+
| FileEditFindReplace
|
|
553
|
+
| AIAssistant
|
|
554
|
+
| DoneFlag
|
|
555
|
+
| ReadImage
|
|
556
|
+
)
|
|
421
557
|
arg = adapter.validate_python(args)
|
|
422
558
|
else:
|
|
423
559
|
arg = args
|
|
@@ -425,12 +561,15 @@ def get_tool_output(
|
|
|
425
561
|
if isinstance(arg, Confirmation):
|
|
426
562
|
console.print("Calling ask confirmation tool")
|
|
427
563
|
output = ask_confirmation(arg), 0.0
|
|
428
|
-
elif isinstance(arg,
|
|
564
|
+
elif isinstance(arg, (BashCommand | BashInteraction)):
|
|
429
565
|
console.print("Calling execute bash tool")
|
|
430
566
|
output = execute_bash(enc, arg, max_tokens)
|
|
431
567
|
elif isinstance(arg, Writefile):
|
|
432
568
|
console.print("Calling write file tool")
|
|
433
569
|
output = write_file(arg), 0
|
|
570
|
+
elif isinstance(arg, FileEditFindReplace):
|
|
571
|
+
console.print("Calling file edit tool")
|
|
572
|
+
output = file_edit(arg), 0.0
|
|
434
573
|
elif isinstance(arg, DoneFlag):
|
|
435
574
|
console.print("Calling mark finish tool")
|
|
436
575
|
output = mark_finish(arg), 0.0
|
|
@@ -440,6 +579,9 @@ def get_tool_output(
|
|
|
440
579
|
elif isinstance(arg, ReadImage):
|
|
441
580
|
console.print("Calling read image tool")
|
|
442
581
|
output = read_image_from_shell(arg.file_path), 0.0
|
|
582
|
+
elif isinstance(arg, ResetShell):
|
|
583
|
+
console.print("Calling reset shell tool")
|
|
584
|
+
output = reset_shell(), 0.0
|
|
443
585
|
else:
|
|
444
586
|
raise ValueError(f"Unknown tool: {arg}")
|
|
445
587
|
|
|
@@ -449,44 +591,6 @@ def get_tool_output(
|
|
|
449
591
|
|
|
450
592
|
History = list[ChatCompletionMessageParam]
|
|
451
593
|
|
|
452
|
-
|
|
453
|
-
def get_is_waiting_user_input(
|
|
454
|
-
model: Models, cost_data: CostData
|
|
455
|
-
) -> Callable[[str], tuple[BASH_CLF_OUTPUT, float]]:
|
|
456
|
-
enc = tiktoken.encoding_for_model(model if not model.startswith("o1") else "gpt-4o")
|
|
457
|
-
system_prompt = """You need to classify if a bash program is waiting for user input based on its stdout, or if it won't exit. You'll be given the output of any program.
|
|
458
|
-
Return `waiting_for_input` if the program is waiting for INTERACTIVE input only, Return 'running' if it's waiting for external resources or just waiting to finish.
|
|
459
|
-
Return `wont_exit` if the program won't exit, for example if it's a server.
|
|
460
|
-
Return `running` otherwise.
|
|
461
|
-
"""
|
|
462
|
-
history: History = [{"role": "system", "content": system_prompt}]
|
|
463
|
-
client = OpenAI()
|
|
464
|
-
|
|
465
|
-
class ExpectedOutput(BaseModel):
|
|
466
|
-
output_classified: BASH_CLF_OUTPUT
|
|
467
|
-
|
|
468
|
-
def is_waiting_user_input(output: str) -> tuple[BASH_CLF_OUTPUT, float]:
|
|
469
|
-
# Send only last 30 lines
|
|
470
|
-
output = "\n".join(output.split("\n")[-30:])
|
|
471
|
-
# Send only max last 200 tokens
|
|
472
|
-
output = enc.decode(enc.encode(output)[-200:])
|
|
473
|
-
|
|
474
|
-
history.append({"role": "user", "content": output})
|
|
475
|
-
response = client.beta.chat.completions.parse(
|
|
476
|
-
model=model, messages=history, response_format=ExpectedOutput
|
|
477
|
-
)
|
|
478
|
-
parsed = response.choices[0].message.parsed
|
|
479
|
-
if parsed is None:
|
|
480
|
-
raise ValueError("No parsed output")
|
|
481
|
-
cost = (
|
|
482
|
-
get_input_cost(cost_data, enc, history)[0]
|
|
483
|
-
+ get_output_cost(cost_data, enc, response.choices[0].message)[0]
|
|
484
|
-
)
|
|
485
|
-
return parsed.output_classified, cost
|
|
486
|
-
|
|
487
|
-
return is_waiting_user_input
|
|
488
|
-
|
|
489
|
-
|
|
490
594
|
default_enc = tiktoken.encoding_for_model("gpt-4o")
|
|
491
595
|
default_model: Models = "gpt-4o-2024-08-06"
|
|
492
596
|
default_cost = CostData(cost_per_1m_input_tokens=0.15, cost_per_1m_output_tokens=0.6)
|
|
@@ -494,7 +598,7 @@ curr_cost = 0.0
|
|
|
494
598
|
|
|
495
599
|
|
|
496
600
|
class Mdata(BaseModel):
|
|
497
|
-
data:
|
|
601
|
+
data: BashCommand | BashInteraction | Writefile | ResetShell | FileEditFindReplace
|
|
498
602
|
|
|
499
603
|
|
|
500
604
|
execution_lock = threading.Lock()
|
|
@@ -509,7 +613,7 @@ def execute_user_input() -> None:
|
|
|
509
613
|
console.log(
|
|
510
614
|
execute_bash(
|
|
511
615
|
default_enc,
|
|
512
|
-
|
|
616
|
+
BashInteraction(
|
|
513
617
|
send_ascii=[ord(x) for x in user_input] + [ord("\n")]
|
|
514
618
|
),
|
|
515
619
|
max_tokens=None,
|
|
@@ -528,6 +632,11 @@ async def register_client(server_url: str, client_uuid: str = "") -> None:
|
|
|
528
632
|
|
|
529
633
|
# Create the WebSocket connection
|
|
530
634
|
async with websockets.connect(f"{server_url}/{client_uuid}") as websocket:
|
|
635
|
+
server_version = str(await websocket.recv())
|
|
636
|
+
print(f"Server version: {server_version}")
|
|
637
|
+
client_version = importlib.metadata.version("wcgw")
|
|
638
|
+
await websocket.send(client_version)
|
|
639
|
+
|
|
531
640
|
print(
|
|
532
641
|
f"Connected. Share this user id with the chatbot: {client_uuid} \nLink: https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access"
|
|
533
642
|
)
|
|
@@ -559,8 +668,15 @@ run = Typer(pretty_exceptions_show_locals=False, no_args_is_help=True)
|
|
|
559
668
|
|
|
560
669
|
@run.command()
|
|
561
670
|
def app(
|
|
562
|
-
server_url: str = "wss://wcgw.arcfu.com/register",
|
|
671
|
+
server_url: str = "wss://wcgw.arcfu.com/v1/register",
|
|
672
|
+
client_uuid: Optional[str] = None,
|
|
673
|
+
version: bool = typer.Option(False, "--version", "-v"),
|
|
563
674
|
) -> None:
|
|
675
|
+
if version:
|
|
676
|
+
version_ = importlib.metadata.version("wcgw")
|
|
677
|
+
print(f"wcgw version: {version_}")
|
|
678
|
+
exit()
|
|
679
|
+
|
|
564
680
|
thread1 = threading.Thread(target=execute_user_input)
|
|
565
681
|
thread2 = threading.Thread(
|
|
566
682
|
target=asyncio.run, args=(register_client(server_url, client_uuid or ""),)
|
wcgw/relay/serve.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
from importlib import metadata
|
|
4
|
+
import semantic_version # type: ignore[import-untyped]
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Callable, Coroutine, DefaultDict, Literal, Optional, Sequence
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
import fastapi
|
|
10
|
+
from fastapi import Response, WebSocket, WebSocketDisconnect
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
import uvicorn
|
|
13
|
+
from fastapi.staticfiles import StaticFiles
|
|
14
|
+
|
|
15
|
+
from dotenv import load_dotenv
|
|
16
|
+
|
|
17
|
+
from ..types_ import (
|
|
18
|
+
BashCommand,
|
|
19
|
+
BashInteraction,
|
|
20
|
+
FileEditFindReplace,
|
|
21
|
+
ResetShell,
|
|
22
|
+
Writefile,
|
|
23
|
+
Specials,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Mdata(BaseModel):
|
|
28
|
+
data: BashCommand | BashInteraction | Writefile | ResetShell | FileEditFindReplace
|
|
29
|
+
user_id: UUID
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
app = fastapi.FastAPI()
|
|
33
|
+
|
|
34
|
+
clients: dict[UUID, Callable[[Mdata], Coroutine[None, None, None]]] = {}
|
|
35
|
+
websockets: dict[UUID, WebSocket] = {}
|
|
36
|
+
gpts: dict[UUID, Callable[[str], None]] = {}
|
|
37
|
+
|
|
38
|
+
images: DefaultDict[UUID, dict[str, dict[str, Any]]] = DefaultDict(dict)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.websocket("/register_serve_image/{uuid}")
|
|
42
|
+
async def register_serve_image(websocket: WebSocket, uuid: UUID) -> None:
|
|
43
|
+
raise Exception("Disabled")
|
|
44
|
+
await websocket.accept()
|
|
45
|
+
received_data = await websocket.receive_json()
|
|
46
|
+
name = received_data["name"]
|
|
47
|
+
image_b64 = received_data["image_b64"]
|
|
48
|
+
image_bytes = base64.b64decode(image_b64)
|
|
49
|
+
images[uuid][name] = {
|
|
50
|
+
"content": image_bytes,
|
|
51
|
+
"media_type": received_data["media_type"],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.get("/get_image/{uuid}/{name}")
|
|
56
|
+
async def get_image(uuid: UUID, name: str) -> fastapi.responses.Response:
|
|
57
|
+
return fastapi.responses.Response(
|
|
58
|
+
content=images[uuid][name]["content"],
|
|
59
|
+
media_type=images[uuid][name]["media_type"],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.websocket("/register/{uuid}")
|
|
64
|
+
async def register_websocket_deprecated(websocket: WebSocket, uuid: UUID) -> None:
|
|
65
|
+
await websocket.accept()
|
|
66
|
+
await websocket.send_text(
|
|
67
|
+
"Outdated client used. Deprecated api is being used. Upgrade the wcgw app."
|
|
68
|
+
)
|
|
69
|
+
await websocket.close(
|
|
70
|
+
reason="This endpoint is deprecated. Please use /v1/register/{uuid}", code=1002
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
CLIENT_VERSION_MINIMUM = "1.0.0"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.websocket("/v1/register/{uuid}")
|
|
78
|
+
async def register_websocket(websocket: WebSocket, uuid: UUID) -> None:
|
|
79
|
+
await websocket.accept()
|
|
80
|
+
|
|
81
|
+
# send server version
|
|
82
|
+
version = metadata.version("wcgw")
|
|
83
|
+
await websocket.send_text(version)
|
|
84
|
+
|
|
85
|
+
# receive client version
|
|
86
|
+
client_version = await websocket.receive_text()
|
|
87
|
+
sem_version_client = semantic_version.Version.coerce(client_version)
|
|
88
|
+
sem_version_server = semantic_version.Version.coerce(CLIENT_VERSION_MINIMUM)
|
|
89
|
+
if sem_version_client < sem_version_server:
|
|
90
|
+
await websocket.send_text(
|
|
91
|
+
f"Client version {client_version} is outdated. Please upgrade to {CLIENT_VERSION_MINIMUM} or higher."
|
|
92
|
+
)
|
|
93
|
+
await websocket.close(
|
|
94
|
+
reason="Client version outdated. Please upgrade to the latest version.",
|
|
95
|
+
code=1002,
|
|
96
|
+
)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Register the callback for this client UUID
|
|
100
|
+
async def send_data_callback(data: Mdata) -> None:
|
|
101
|
+
await websocket.send_text(data.model_dump_json())
|
|
102
|
+
|
|
103
|
+
clients[uuid] = send_data_callback
|
|
104
|
+
websockets[uuid] = websocket
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
while True:
|
|
108
|
+
received_data = await websocket.receive_text()
|
|
109
|
+
if uuid not in gpts:
|
|
110
|
+
raise fastapi.HTTPException(status_code=400, detail="No call made")
|
|
111
|
+
gpts[uuid](received_data)
|
|
112
|
+
except WebSocketDisconnect:
|
|
113
|
+
# Remove the client if the WebSocket is disconnected
|
|
114
|
+
del clients[uuid]
|
|
115
|
+
del websockets[uuid]
|
|
116
|
+
print(f"Client {uuid} disconnected")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.post("/write_file")
|
|
120
|
+
async def write_file_deprecated(write_file_data: Writefile, user_id: UUID) -> Response:
|
|
121
|
+
return Response(
|
|
122
|
+
content="This version of the API is deprecated. Please upgrade your client.",
|
|
123
|
+
status_code=400,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class WritefileWithUUID(Writefile):
|
|
128
|
+
user_id: UUID
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.post("/v1/write_file")
|
|
132
|
+
async def write_file(write_file_data: WritefileWithUUID) -> str:
|
|
133
|
+
user_id = write_file_data.user_id
|
|
134
|
+
if user_id not in clients:
|
|
135
|
+
return "Failure: id not found, ask the user to check it."
|
|
136
|
+
|
|
137
|
+
results: Optional[str] = None
|
|
138
|
+
|
|
139
|
+
def put_results(result: str) -> None:
|
|
140
|
+
nonlocal results
|
|
141
|
+
results = result
|
|
142
|
+
|
|
143
|
+
gpts[user_id] = put_results
|
|
144
|
+
|
|
145
|
+
await clients[user_id](Mdata(data=write_file_data, user_id=user_id))
|
|
146
|
+
|
|
147
|
+
start_time = time.time()
|
|
148
|
+
while time.time() - start_time < 30:
|
|
149
|
+
if results is not None:
|
|
150
|
+
return results
|
|
151
|
+
await asyncio.sleep(0.1)
|
|
152
|
+
|
|
153
|
+
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class FileEditFindReplaceWithUUID(FileEditFindReplace):
|
|
157
|
+
user_id: UUID
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@app.post("/v1/file_edit_find_replace")
|
|
161
|
+
async def file_edit_find_replace(
|
|
162
|
+
file_edit_find_replace: FileEditFindReplaceWithUUID,
|
|
163
|
+
) -> str:
|
|
164
|
+
user_id = file_edit_find_replace.user_id
|
|
165
|
+
if user_id not in clients:
|
|
166
|
+
return "Failure: id not found, ask the user to check it."
|
|
167
|
+
|
|
168
|
+
results: Optional[str] = None
|
|
169
|
+
|
|
170
|
+
def put_results(result: str) -> None:
|
|
171
|
+
nonlocal results
|
|
172
|
+
results = result
|
|
173
|
+
|
|
174
|
+
gpts[user_id] = put_results
|
|
175
|
+
|
|
176
|
+
await clients[user_id](
|
|
177
|
+
Mdata(
|
|
178
|
+
data=file_edit_find_replace,
|
|
179
|
+
user_id=user_id,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
start_time = time.time()
|
|
184
|
+
while time.time() - start_time < 30:
|
|
185
|
+
if results is not None:
|
|
186
|
+
return results
|
|
187
|
+
await asyncio.sleep(0.1)
|
|
188
|
+
|
|
189
|
+
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ResetShellWithUUID(ResetShell):
|
|
193
|
+
user_id: UUID
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@app.post("/v1/reset_shell")
|
|
197
|
+
async def reset_shell(reset_shell: ResetShellWithUUID) -> str:
|
|
198
|
+
user_id = reset_shell.user_id
|
|
199
|
+
if user_id not in clients:
|
|
200
|
+
return "Failure: id not found, ask the user to check it."
|
|
201
|
+
|
|
202
|
+
results: Optional[str] = None
|
|
203
|
+
|
|
204
|
+
def put_results(result: str) -> None:
|
|
205
|
+
nonlocal results
|
|
206
|
+
results = result
|
|
207
|
+
|
|
208
|
+
gpts[user_id] = put_results
|
|
209
|
+
|
|
210
|
+
await clients[user_id](Mdata(data=reset_shell, user_id=user_id))
|
|
211
|
+
|
|
212
|
+
start_time = time.time()
|
|
213
|
+
while time.time() - start_time < 30:
|
|
214
|
+
if results is not None:
|
|
215
|
+
return results
|
|
216
|
+
await asyncio.sleep(0.1)
|
|
217
|
+
|
|
218
|
+
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.post("/execute_bash")
|
|
222
|
+
async def execute_bash_deprecated(excute_bash_data: Any, user_id: UUID) -> Response:
|
|
223
|
+
return Response(
|
|
224
|
+
content="This version of the API is deprecated. Please upgrade your client.",
|
|
225
|
+
status_code=400,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class CommandWithUUID(BaseModel):
|
|
230
|
+
command: str
|
|
231
|
+
user_id: UUID
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@app.post("/v1/bash_command")
|
|
235
|
+
async def bash_command(command: CommandWithUUID) -> str:
|
|
236
|
+
user_id = command.user_id
|
|
237
|
+
if user_id not in clients:
|
|
238
|
+
return "Failure: id not found, ask the user to check it."
|
|
239
|
+
|
|
240
|
+
results: Optional[str] = None
|
|
241
|
+
|
|
242
|
+
def put_results(result: str) -> None:
|
|
243
|
+
nonlocal results
|
|
244
|
+
results = result
|
|
245
|
+
|
|
246
|
+
gpts[user_id] = put_results
|
|
247
|
+
|
|
248
|
+
await clients[user_id](
|
|
249
|
+
Mdata(data=BashCommand(command=command.command), user_id=user_id)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
start_time = time.time()
|
|
253
|
+
while time.time() - start_time < 30:
|
|
254
|
+
if results is not None:
|
|
255
|
+
return results
|
|
256
|
+
await asyncio.sleep(0.1)
|
|
257
|
+
|
|
258
|
+
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class BashInteractionWithUUID(BashInteraction):
|
|
262
|
+
user_id: UUID
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@app.post("/v1/bash_interaction")
|
|
266
|
+
async def bash_interaction(bash_interaction: BashInteractionWithUUID) -> str:
|
|
267
|
+
user_id = bash_interaction.user_id
|
|
268
|
+
if user_id not in clients:
|
|
269
|
+
return "Failure: id not found, ask the user to check it."
|
|
270
|
+
|
|
271
|
+
results: Optional[str] = None
|
|
272
|
+
|
|
273
|
+
def put_results(result: str) -> None:
|
|
274
|
+
nonlocal results
|
|
275
|
+
results = result
|
|
276
|
+
|
|
277
|
+
gpts[user_id] = put_results
|
|
278
|
+
|
|
279
|
+
await clients[user_id](
|
|
280
|
+
Mdata(
|
|
281
|
+
data=bash_interaction,
|
|
282
|
+
user_id=user_id,
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
start_time = time.time()
|
|
287
|
+
while time.time() - start_time < 30:
|
|
288
|
+
if results is not None:
|
|
289
|
+
return results
|
|
290
|
+
await asyncio.sleep(0.1)
|
|
291
|
+
|
|
292
|
+
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def run() -> None:
|
|
299
|
+
load_dotenv()
|
|
300
|
+
|
|
301
|
+
uvicorn_thread = threading.Thread(
|
|
302
|
+
target=uvicorn.run,
|
|
303
|
+
args=(app,),
|
|
304
|
+
kwargs={
|
|
305
|
+
"host": "0.0.0.0",
|
|
306
|
+
"port": 8000,
|
|
307
|
+
"log_level": "info",
|
|
308
|
+
"access_log": True,
|
|
309
|
+
},
|
|
310
|
+
)
|
|
311
|
+
uvicorn_thread.start()
|
|
312
|
+
uvicorn_thread.join()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
run()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Privacy Policy
|
|
2
|
+
I do not collect, store, or share any personal data.
|
|
3
|
+
The data from your terminal is not stored anywhere and it's not logged or collected in any form.
|
|
4
|
+
There is a relay webserver for connecting your terminal to chatgpt the source code for which is open at https://github.com/rusiaaman/wcgw/tree/main/src/relay that you can run on your own.
|
|
5
|
+
Other than the relay webserver there is no further involvement of my servers or services.
|
|
6
|
+
Feel free to me contact at info@arcfu.com for questions on privacy or anything else.
|
|
7
|
+
|
wcgw/types_.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Literal, Optional, Sequence
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BashCommand(BaseModel):
|
|
6
|
+
command: str
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Specials = Literal[
|
|
10
|
+
"Key-up", "Key-down", "Key-left", "Key-right", "Enter", "Ctrl-c", "Ctrl-d", "Ctrl-z"
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BashInteraction(BaseModel):
|
|
15
|
+
send_text: Optional[str] = None
|
|
16
|
+
send_specials: Optional[Sequence[Specials]] = None
|
|
17
|
+
send_ascii: Optional[Sequence[int]] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReadImage(BaseModel):
|
|
21
|
+
file_path: str
|
|
22
|
+
type: Literal["ReadImage"] = "ReadImage"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Writefile(BaseModel):
|
|
26
|
+
file_path: str
|
|
27
|
+
file_content: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FileEditFindReplace(BaseModel):
|
|
31
|
+
file_path: str
|
|
32
|
+
find_lines: str
|
|
33
|
+
replace_with_lines: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ResetShell(BaseModel):
|
|
37
|
+
should_reset: Literal[True] = True
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: What could go wrong giving full shell access to chatgpt?
|
|
5
5
|
Project-URL: Homepage, https://github.com/rusiaaman/wcgw
|
|
6
6
|
Author-email: Aman Rusia <gapypi@arcfu.com>
|
|
7
7
|
Requires-Python: <3.13,>=3.10
|
|
8
8
|
Requires-Dist: fastapi>=0.115.0
|
|
9
9
|
Requires-Dist: mypy>=1.11.2
|
|
10
|
+
Requires-Dist: nltk>=3.9.1
|
|
10
11
|
Requires-Dist: openai>=1.46.0
|
|
11
12
|
Requires-Dist: petname>=2.6
|
|
12
13
|
Requires-Dist: pexpect>=4.9.0
|
|
@@ -14,6 +15,7 @@ Requires-Dist: pydantic>=2.9.2
|
|
|
14
15
|
Requires-Dist: pyte>=0.8.2
|
|
15
16
|
Requires-Dist: python-dotenv>=1.0.1
|
|
16
17
|
Requires-Dist: rich>=13.8.1
|
|
18
|
+
Requires-Dist: semantic-version>=2.10.0
|
|
17
19
|
Requires-Dist: shell>=1.0.1
|
|
18
20
|
Requires-Dist: tiktoken==0.7.0
|
|
19
21
|
Requires-Dist: toml>=0.10.2
|
|
@@ -32,7 +34,8 @@ A custom gpt on chatgpt web app to interact with your local shell.
|
|
|
32
34
|
### 🚀 Highlights
|
|
33
35
|
- ⚡ **Full Shell Access**: No restrictions, complete control.
|
|
34
36
|
- ⚡ **Create, Execute, Iterate**: Ask the gpt to keep running compiler checks till all errors are fixed, or ask it to keep checking for the status of a long running command till it's done.
|
|
35
|
-
- ⚡ **Interactive Command Handling**:
|
|
37
|
+
- ⚡ **Interactive Command Handling**: Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
|
|
38
|
+
- ⚡ **REPL support**: [beta] Supports python/node and other REPL execution.
|
|
36
39
|
|
|
37
40
|
### 🪜 Steps:
|
|
38
41
|
1. Run the [cli client](https://github.com/rusiaaman/wcgw?tab=readme-ov-file#client) in any directory of choice.
|
|
@@ -98,12 +101,12 @@ https://github.com/rusiaaman/wcgw/blob/main/gpt_instructions.txt
|
|
|
98
101
|
https://github.com/rusiaaman/wcgw/blob/main/gpt_action_json_schema.json
|
|
99
102
|
|
|
100
103
|
Run the server
|
|
101
|
-
`gunicorn --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:443 src.relay.serve:app --certfile fullchain.pem --keyfile privkey.pem`
|
|
104
|
+
`gunicorn --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:443 src.wcgw.relay.serve:app --certfile fullchain.pem --keyfile privkey.pem`
|
|
102
105
|
|
|
103
106
|
If you don't have public ip and domain name, you can use `ngrok` or similar services to get a https address to the api.
|
|
104
107
|
|
|
105
108
|
The specify the server url in the `wcgw` command like so
|
|
106
|
-
`wcgw --server-url https://your-url/register`
|
|
109
|
+
`wcgw --server-url https://your-url/v1/register`
|
|
107
110
|
|
|
108
111
|
# [Optional] Local shell access with openai API key
|
|
109
112
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
wcgw/__init__.py,sha256=VMi3gCAN_4_Ft8v5dY74u6bt5X-H1QhFJqTMZRd4fvk,76
|
|
2
|
+
wcgw/types_.py,sha256=qpMRh1y136GkjIONIFowLy56v5p0OcdNqEH2mTmHPqU,756
|
|
3
|
+
wcgw/client/__main__.py,sha256=ngI_vBcLAv7fJgmS4w4U7tuWtalGB8c7W5qebuT6Z6o,30
|
|
4
|
+
wcgw/client/basic.py,sha256=2rY5pKm9dBBRuku0uIBUeLWjNc3iXjTSK9CmfuTxoS8,16196
|
|
5
|
+
wcgw/client/claude.py,sha256=Bp45-UMBIJd-4tzX618nu-SpRbVtkTb1Es6c_gW6xy0,14861
|
|
6
|
+
wcgw/client/common.py,sha256=grH-yV_4tnTQZ29xExn4YicGLxEq98z-HkEZwH0ReSg,1410
|
|
7
|
+
wcgw/client/openai_adapters.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
wcgw/client/openai_utils.py,sha256=YNwCsA-Wqq7jWrxP0rfQmBTb1dI0s7dWXzQqyTzOZT4,2629
|
|
9
|
+
wcgw/client/tools.py,sha256=Ggm5QrSQiyv3Bnme-O9eo_8dXFwS1srCAoow-pODDSE,21212
|
|
10
|
+
wcgw/relay/serve.py,sha256=e3oo4trAWmEKJWcLOsdZCICyEXUCqrI-71DaFnPKI70,8641
|
|
11
|
+
wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
|
|
12
|
+
wcgw-1.0.1.dist-info/METADATA,sha256=O2BPohrX_Eo6RRuWfY18Cz8MX6qTlgSnExa52Nd8Ggg,5222
|
|
13
|
+
wcgw-1.0.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
14
|
+
wcgw-1.0.1.dist-info/entry_points.txt,sha256=WlIB825-Vm9ZtNzgENQsbHj4DRMkbpVR7uSkQyBlaPA,93
|
|
15
|
+
wcgw-1.0.1.dist-info/RECORD,,
|
wcgw/__main__.py
DELETED
wcgw-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
wcgw/__init__.py,sha256=okSsOWpTKDjEQzgOin3Kdpx4Mc3MFX1RunjopHQSIWE,62
|
|
2
|
-
wcgw/__main__.py,sha256=MjJnFwfYzA1rW47xuSP1EVsi53DTHeEGqESkQwsELFQ,34
|
|
3
|
-
wcgw/basic.py,sha256=aTos3c0URl-ufgXfQ1bkg-5oFCR_SxG_VI5qckBtex0,16426
|
|
4
|
-
wcgw/claude.py,sha256=Bp45-UMBIJd-4tzX618nu-SpRbVtkTb1Es6c_gW6xy0,14861
|
|
5
|
-
wcgw/common.py,sha256=grH-yV_4tnTQZ29xExn4YicGLxEq98z-HkEZwH0ReSg,1410
|
|
6
|
-
wcgw/openai_adapters.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
wcgw/openai_utils.py,sha256=YNwCsA-Wqq7jWrxP0rfQmBTb1dI0s7dWXzQqyTzOZT4,2629
|
|
8
|
-
wcgw/tools.py,sha256=UdSU6lAbOGNdG2wiM5x8YTosBDlphiMEo7MHtMjGvRk,18618
|
|
9
|
-
wcgw-0.2.0.dist-info/METADATA,sha256=X4vyv9Oaq8JtD301hk8jObC3bHggqtyWcxNvUssG-I4,5076
|
|
10
|
-
wcgw-0.2.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
11
|
-
wcgw-0.2.0.dist-info/entry_points.txt,sha256=T-IH7w6Vc650hr8xksC8kJfbJR4uwN8HDudejwDwrNM,59
|
|
12
|
-
wcgw-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|