wcgw 0.0.5__py3-none-any.whl → 0.0.7__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/basic.py CHANGED
@@ -1,4 +1,6 @@
1
+ import base64
1
2
  import json
3
+ import mimetypes
2
4
  from pathlib import Path
3
5
  import sys
4
6
  import traceback
@@ -8,17 +10,21 @@ from openai import OpenAI
8
10
  from openai.types.chat import (
9
11
  ChatCompletionMessageParam,
10
12
  ChatCompletionAssistantMessageParam,
13
+ ChatCompletionUserMessageParam,
14
+ ChatCompletionContentPartParam,
11
15
  ChatCompletionMessage,
12
16
  ParsedChatCompletionMessage,
13
17
  )
14
18
  import rich
19
+ import petname
15
20
  from typer import Typer
16
21
  import uuid
17
22
 
18
- from .common import Models, discard_input
19
- from .common import CostData, History
23
+ from wcgw.common import Config, text_from_editor
24
+
25
+ from .common import Models
20
26
  from .openai_utils import get_input_cost, get_output_cost
21
- from .tools import ExecuteBash, GetShellOutputLastCommand
27
+ from .tools import ExecuteBash, ReadImage, ImageData
22
28
 
23
29
  from .tools import (
24
30
  BASH_CLF_OUTPUT,
@@ -34,40 +40,14 @@ from .tools import (
34
40
  import tiktoken
35
41
 
36
42
  from urllib import parse
37
- import subprocess
38
43
  import os
39
- import tempfile
40
44
 
41
45
  import toml
42
- from pydantic import BaseModel
43
46
 
44
47
 
45
48
  from dotenv import load_dotenv
46
49
 
47
-
48
- class Config(BaseModel):
49
- model: Models
50
- secondary_model: Models
51
- cost_limit: float
52
- cost_file: dict[Models, CostData]
53
- cost_unit: str = "$"
54
-
55
-
56
- def text_from_editor(console: rich.console.Console) -> str:
57
- # First consume all the input till now
58
- discard_input()
59
- console.print("\n---------------------------------------\n# User message")
60
- data = input()
61
- if data:
62
- return data
63
- editor = os.environ.get("EDITOR", "vim")
64
- with tempfile.NamedTemporaryFile(suffix=".tmp") as tf:
65
- subprocess.run([editor, tf.name], check=True)
66
- with open(tf.name, "r") as f:
67
- data = f.read()
68
- console.print(data)
69
- return data
70
-
50
+ History = list[ChatCompletionMessageParam]
71
51
 
72
52
  def save_history(history: History, session_id: str) -> None:
73
53
  myid = str(history[1]["content"]).replace("/", "_").replace(" ", "_").lower()[:60]
@@ -80,6 +60,38 @@ def save_history(history: History, session_id: str) -> None:
80
60
  json.dump(history, f, indent=3)
81
61
 
82
62
 
63
+ def parse_user_message_special(msg: str) -> ChatCompletionUserMessageParam:
64
+ # Search for lines starting with `%` and treat them as special commands
65
+ parts: list[ChatCompletionContentPartParam] = []
66
+ for line in msg.split("\n"):
67
+ if line.startswith("%"):
68
+ args = line[1:].strip().split(" ")
69
+ command = args[0]
70
+ assert command == 'image'
71
+ image_path = args[1]
72
+ with open(image_path, 'rb') as f:
73
+ image_bytes = f.read()
74
+ image_b64 = base64.b64encode(image_bytes).decode("utf-8")
75
+ image_type = mimetypes.guess_type(image_path)[0]
76
+ dataurl=f'data:{image_type};base64,{image_b64}'
77
+ parts.append({
78
+ 'type': 'image_url',
79
+ 'image_url': {
80
+ 'url': dataurl,
81
+ 'detail': 'auto'
82
+ }
83
+ })
84
+ else:
85
+ if len(parts) > 0 and parts[-1]['type'] == 'text':
86
+ parts[-1]['text'] += '\n' + line
87
+ else:
88
+ parts.append({'type': 'text', 'text': line})
89
+ return {
90
+ 'role': 'user',
91
+ 'content': parts
92
+ }
93
+
94
+
83
95
  app = Typer(pretty_exceptions_show_locals=False)
84
96
 
85
97
 
@@ -94,6 +106,7 @@ def loop(
94
106
  session_id = str(uuid.uuid4())[:6]
95
107
 
96
108
  history: History = []
109
+ waiting_for_assistant = False
97
110
  if resume:
98
111
  if resume == "latest":
99
112
  resume_path = sorted(Path(".wcgw").iterdir(), key=os.path.getmtime)[-1]
@@ -108,6 +121,7 @@ def loop(
108
121
  if history[1]["role"] != "user":
109
122
  raise ValueError("Invalid history file, second message should be user")
110
123
  first_message = ""
124
+ waiting_for_assistant = history[-1]['role'] != 'assistant'
111
125
 
112
126
  my_dir = os.path.dirname(__file__)
113
127
  config_file = os.path.join(my_dir, "..", "..", "config.toml")
@@ -134,19 +148,18 @@ Execute a bash script. Stateful (beware with subsequent calls).
134
148
  Execute commands using `execute_command` attribute.
135
149
  Do not use interactive commands like nano. Prefer writing simpler commands.
136
150
  Last line will always be `(exit <int code>)` except if
137
- the last line is `(waiting for input)` which will be the case if you've run any interactive command (which you shouldn't run) by mistake. You can then send input using `send_ascii` attributes.
151
+ the last line is `(pending)` if the program is still running or waiting for user inputs. You can then send input using `send_ascii` attributes. You get status by sending `send_ascii: [10]`.
138
152
  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.
139
153
  Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
140
154
  The first line might be `(...truncated)` if the output is too long.""",
141
155
  ),
142
- openai.pydantic_function_tool(
143
- GetShellOutputLastCommand,
144
- description="Get output of the last command run in the shell. Use this in case you want to know status of a running program.",
145
- ),
146
156
  openai.pydantic_function_tool(
147
157
  Writefile,
148
158
  description="Write content to a file. Provide file path and content. Use this instead of ExecuteBash for writing files.",
149
159
  ),
160
+ openai.pydantic_function_tool(
161
+ ReadImage, description="Read an image from the shell."
162
+ ),
150
163
  ]
151
164
  uname_sysname = os.uname().sysname
152
165
  uname_machine = os.uname().machine
@@ -165,12 +178,11 @@ System information:
165
178
  - Machine: {uname_machine}
166
179
  """
167
180
 
168
- has_tool_output = False
169
181
  if not history:
170
182
  history = [{"role": "system", "content": system}]
171
183
  else:
172
184
  if history[-1]["role"] == "tool":
173
- has_tool_output = True
185
+ waiting_for_assistant = True
174
186
 
175
187
  client = OpenAI()
176
188
 
@@ -189,16 +201,16 @@ System information:
189
201
  )
190
202
  break
191
203
 
192
- if not has_tool_output:
204
+ if not waiting_for_assistant:
193
205
  if first_message:
194
206
  msg = first_message
195
207
  first_message = ""
196
208
  else:
197
209
  msg = text_from_editor(user_console)
198
210
 
199
- history.append({"role": "user", "content": msg})
211
+ history.append(parse_user_message_special(msg))
200
212
  else:
201
- has_tool_output = False
213
+ waiting_for_assistant = False
202
214
 
203
215
  cost_, input_toks_ = get_input_cost(
204
216
  config.cost_file[config.model], enc, history
@@ -223,6 +235,7 @@ System information:
223
235
  _histories: History = []
224
236
  item: ChatCompletionMessageParam
225
237
  full_response: str = ""
238
+ image_histories: History = []
226
239
  try:
227
240
  for chunk in stream:
228
241
  if chunk.choices[0].finish_reason == "tool_calls":
@@ -236,7 +249,7 @@ System information:
236
249
  "type": "function",
237
250
  "function": {
238
251
  "arguments": tool_args,
239
- "name": "execute_bash",
252
+ "name": type(which_tool(tool_args)).__name__,
240
253
  },
241
254
  }
242
255
  for tool_call_id, toolcallargs in tool_call_args_by_id.items()
@@ -252,7 +265,7 @@ System information:
252
265
  )
253
266
  system_console.print(f"\nTotal cost: {config.cost_unit}{cost:.3f}")
254
267
  output_toks += output_toks_
255
-
268
+
256
269
  _histories.append(item)
257
270
  for tool_call_id, toolcallargs in tool_call_args_by_id.items():
258
271
  for toolindex, tool_args in toolcallargs.items():
@@ -284,13 +297,50 @@ System information:
284
297
  f"\nTotal cost: {config.cost_unit}{cost:.3f}"
285
298
  )
286
299
  return output_or_done.task_output, cost
300
+
287
301
  output = output_or_done
288
302
 
289
- item = {
290
- "role": "tool",
291
- "content": str(output),
292
- "tool_call_id": tool_call_id + str(toolindex),
293
- }
303
+ if isinstance(output, ImageData):
304
+ randomId = petname.Generate(2, "-")
305
+ if not image_histories:
306
+ image_histories.extend([
307
+ {
308
+ 'role': 'assistant',
309
+ 'content': f'Share images with ids: {randomId}'
310
+
311
+ },
312
+ {
313
+ 'role': 'user',
314
+ 'content': [{
315
+ 'type': 'image_url',
316
+ 'image_url': {
317
+ 'url': output.dataurl,
318
+ 'detail': 'auto'
319
+ }
320
+ }]
321
+ }]
322
+ )
323
+ else:
324
+ image_histories[0]['content'] += ', ' + randomId
325
+ image_histories[1]["content"].append({ # type: ignore
326
+ 'type': 'image_url',
327
+ 'image_url': {
328
+ 'url': output.dataurl,
329
+ 'detail': 'auto'
330
+ }
331
+ })
332
+
333
+ item = {
334
+ "role": "tool",
335
+ "content": f'Ask user for image id: {randomId}',
336
+ "tool_call_id": tool_call_id + str(toolindex),
337
+ }
338
+ else:
339
+ item = {
340
+ "role": "tool",
341
+ "content": str(output),
342
+ "tool_call_id": tool_call_id + str(toolindex),
343
+ }
294
344
  cost_, output_toks_ = get_output_cost(
295
345
  config.cost_file[config.model], enc, item
296
346
  )
@@ -298,7 +348,7 @@ System information:
298
348
  output_toks += output_toks_
299
349
 
300
350
  _histories.append(item)
301
- has_tool_output = True
351
+ waiting_for_assistant = True
302
352
  break
303
353
  elif chunk.choices[0].finish_reason:
304
354
  assistant_console.print("")
@@ -327,11 +377,11 @@ System information:
327
377
  assistant_console.print(chunk_str, end="")
328
378
  full_response += chunk_str
329
379
  except KeyboardInterrupt:
330
- has_tool_output = False
380
+ waiting_for_assistant = False
331
381
  input("Interrupted...enter to redo the current turn")
332
382
  else:
333
383
  history.extend(_histories)
334
-
384
+ history.extend(image_histories)
335
385
  save_history(history, session_id)
336
386
 
337
387
  return "Couldn't finish the task", cost
wcgw/common.py CHANGED
@@ -1,9 +1,13 @@
1
+ import os
1
2
  import select
3
+ import subprocess
2
4
  import sys
5
+ import tempfile
3
6
  import termios
4
7
  import tty
5
8
  from typing import Literal
6
9
  from pydantic import BaseModel
10
+ import rich
7
11
 
8
12
 
9
13
  class CostData(BaseModel):
@@ -11,14 +15,6 @@ class CostData(BaseModel):
11
15
  cost_per_1m_output_tokens: float
12
16
 
13
17
 
14
- from openai.types.chat import (
15
- ChatCompletionMessageParam,
16
- ChatCompletionAssistantMessageParam,
17
- ChatCompletionMessage,
18
- ParsedChatCompletionMessage,
19
- )
20
-
21
- History = list[ChatCompletionMessageParam]
22
18
  Models = Literal["gpt-4o-2024-08-06", "gpt-4o-mini"]
23
19
 
24
20
 
@@ -45,3 +41,27 @@ def discard_input() -> None:
45
41
  finally:
46
42
  # Restore old terminal settings
47
43
  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
44
+
45
+
46
+ class Config(BaseModel):
47
+ model: Models
48
+ secondary_model: Models
49
+ cost_limit: float
50
+ cost_file: dict[Models, CostData]
51
+ cost_unit: str = "$"
52
+
53
+
54
+ def text_from_editor(console: rich.console.Console) -> str:
55
+ # First consume all the input till now
56
+ discard_input()
57
+ console.print("\n---------------------------------------\n# User message")
58
+ data = input()
59
+ if data:
60
+ return data
61
+ editor = os.environ.get("EDITOR", "vim")
62
+ with tempfile.NamedTemporaryFile(suffix=".tmp") as tf:
63
+ subprocess.run([editor, tf.name], check=True)
64
+ with open(tf.name, "r") as f:
65
+ data = f.read()
66
+ console.print(data)
67
+ return data
wcgw/openai_utils.py CHANGED
@@ -28,9 +28,19 @@ def get_input_cost(
28
28
  input_tokens = 0
29
29
  for msg in history:
30
30
  content = msg["content"]
31
- if not isinstance(content, str):
31
+ refusal = msg.get("refusal")
32
+ if isinstance(content, list):
33
+ for part in content:
34
+ if 'text' in part:
35
+ input_tokens += len(enc.encode(part['text']))
36
+ elif content is None:
37
+ if refusal is None:
38
+ raise ValueError("Expected content or refusal to be present")
39
+ input_tokens += len(enc.encode(str(refusal)))
40
+ elif not isinstance(content, str):
32
41
  raise ValueError(f"Expected content to be string, got {type(content)}")
33
- input_tokens += len(enc.encode(content))
42
+ else:
43
+ input_tokens += len(enc.encode(content))
34
44
  cost = input_tokens * cost_map.cost_per_1m_input_tokens / 1_000_000
35
45
  return cost, input_tokens
36
46
 
wcgw/tools.py CHANGED
@@ -1,11 +1,14 @@
1
1
  import asyncio
2
+ import base64
2
3
  import json
4
+ import mimetypes
3
5
  import sys
4
6
  import threading
5
7
  import traceback
6
- from typing import Callable, Literal, Optional, ParamSpec, Sequence, TypeVar, TypedDict
8
+ from typing import Callable, Literal, NewType, Optional, ParamSpec, Sequence, TypeVar, TypedDict
7
9
  import uuid
8
10
  from pydantic import BaseModel, TypeAdapter
11
+ from websockets.sync.client import connect as syncconnect
9
12
 
10
13
  import os
11
14
  import tiktoken
@@ -67,7 +70,7 @@ class Writefile(BaseModel):
67
70
 
68
71
  def start_shell():
69
72
  SHELL = pexpect.spawn(
70
- "/bin/bash",
73
+ "/bin/bash --noprofile --norc",
71
74
  env={**os.environ, **{"PS1": "#@@"}},
72
75
  echo=False,
73
76
  encoding="utf-8",
@@ -82,11 +85,26 @@ def start_shell():
82
85
  SHELL = start_shell()
83
86
 
84
87
 
88
+ def _is_int(mystr: str) -> bool:
89
+ try:
90
+ int(mystr)
91
+ return True
92
+ except ValueError:
93
+ return False
94
+
95
+
85
96
  def _get_exit_code() -> int:
86
97
  SHELL.sendline("echo $?")
87
- SHELL.expect("#@@")
88
- assert isinstance(SHELL.before, str)
89
- return int((SHELL.before))
98
+ before = ""
99
+ while not _is_int(before): # Consume all previous output
100
+ SHELL.expect("#@@")
101
+ assert isinstance(SHELL.before, str)
102
+ before = SHELL.before
103
+
104
+ try:
105
+ return int((before))
106
+ except ValueError:
107
+ raise ValueError(f"Malformed output: {before}")
90
108
 
91
109
 
92
110
  Specials = Literal["Key-up", "Key-down", "Key-left", "Key-right", "Enter", "Ctrl-c"]
@@ -97,10 +115,6 @@ class ExecuteBash(BaseModel):
97
115
  send_ascii: Optional[Sequence[int | Specials]] = None
98
116
 
99
117
 
100
- class GetShellOutputLastCommand(BaseModel):
101
- type: Literal["get_output_of_last_command"] = "get_output_of_last_command"
102
-
103
-
104
118
  BASH_CLF_OUTPUT = Literal["running", "waiting_for_input", "wont_exit"]
105
119
  BASH_STATE: BASH_CLF_OUTPUT = "running"
106
120
 
@@ -116,9 +130,9 @@ def get_output_of_last_command(enc: tiktoken.Encoding) -> str:
116
130
  return output
117
131
 
118
132
 
119
- WETTING_INPUT_MESSAGE = """A command is already running waiting for input. NOTE: You can't run multiple shell sessions, likely a previous program hasn't exited.
120
- 1. Get its output using `GetShellOutputLastCommand` OR
121
- 2. Use `send_ascii` to give inputs to the running program, don't use `execute_command` OR
133
+ WAITING_INPUT_MESSAGE = """A command is already running waiting for input. NOTE: You can't run multiple shell sessions, likely a previous program hasn't exited.
134
+ 1. Get its output using `send_ascii: [10]`
135
+ 2. Use `send_ascii` to give inputs to the running program, don't use `execute_command` OR
122
136
  3. kill the previous program by sending ctrl+c first using `send_ascii`"""
123
137
 
124
138
 
@@ -131,7 +145,7 @@ def execute_bash(
131
145
  try:
132
146
  if bash_arg.execute_command:
133
147
  if BASH_STATE == "waiting_for_input":
134
- raise ValueError(WETTING_INPUT_MESSAGE)
148
+ raise ValueError(WAITING_INPUT_MESSAGE)
135
149
  elif BASH_STATE == "wont_exit":
136
150
  raise ValueError(
137
151
  """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.
@@ -192,9 +206,7 @@ def execute_bash(
192
206
  text = "...(truncated)\n" + enc.decode(tokens[-2047:])
193
207
 
194
208
  last_line = (
195
- "(waiting for input)"
196
- if BASH_STATE == "waiting_for_input"
197
- else "(won't exit)"
209
+ "(pending)" if BASH_STATE == "waiting_for_input" else "(won't exit)"
198
210
  )
199
211
  return text + f"\n{last_line}", cost
200
212
  index = SHELL.expect(["#@@", pexpect.TIMEOUT], timeout=wait)
@@ -211,7 +223,9 @@ def execute_bash(
211
223
  exit_code = _get_exit_code()
212
224
  output += f"\n(exit {exit_code})"
213
225
 
214
- except ValueError:
226
+ except ValueError as e:
227
+ console.print(output)
228
+ traceback.print_exc()
215
229
  console.print("Malformed output, restarting shell", style="red")
216
230
  # Malformed output, restart shell
217
231
  SHELL.close(True)
@@ -220,6 +234,35 @@ def execute_bash(
220
234
  return output, 0
221
235
 
222
236
 
237
+ class ReadImage(BaseModel):
238
+ file_path: str
239
+ type: Literal['ReadImage'] = 'ReadImage'
240
+
241
+
242
+ def serve_image_in_bg(file_path: str, client_uuid: str, name: str) -> None:
243
+ if not client_uuid:
244
+ client_uuid = str(uuid.uuid4())
245
+
246
+ server_url = "wss://wcgw.arcfu.com/register_serve_image"
247
+
248
+ with open(file_path, "rb") as image_file:
249
+ image_bytes = image_file.read()
250
+ media_type = mimetypes.guess_type(file_path)[0]
251
+ image_b64 = base64.b64encode(image_bytes).decode("utf-8")
252
+ uu = {"name": name, "image_b64": image_b64, "media_type": media_type}
253
+
254
+ with syncconnect(f"{server_url}/{client_uuid}") as websocket:
255
+ try:
256
+ websocket.send(json.dumps(uu))
257
+ except websockets.ConnectionClosed:
258
+ print(f"Connection closed for UUID: {client_uuid}, retrying")
259
+ serve_image_in_bg(file_path, client_uuid, name)
260
+
261
+ class ImageData(BaseModel):
262
+ dataurl: str
263
+
264
+
265
+
223
266
  Param = ParamSpec("Param")
224
267
 
225
268
  T = TypeVar("T")
@@ -229,7 +272,7 @@ def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
229
272
  def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
230
273
  global BASH_STATE
231
274
  if BASH_STATE == "waiting_for_input":
232
- raise ValueError(WETTING_INPUT_MESSAGE)
275
+ raise ValueError(WAITING_INPUT_MESSAGE)
233
276
  elif BASH_STATE == "wont_exit":
234
277
  raise ValueError(
235
278
  "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."
@@ -238,6 +281,24 @@ def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
238
281
 
239
282
  return wrapper
240
283
 
284
+ @ensure_no_previous_output
285
+ def read_image_from_shell(file_path: str) -> ImageData:
286
+ if not os.path.isabs(file_path):
287
+ SHELL.sendline("pwd")
288
+ SHELL.expect("#@@")
289
+ assert isinstance(SHELL.before, str)
290
+ current_dir = SHELL.before.strip()
291
+ file_path = os.path.join(current_dir, file_path)
292
+
293
+ if not os.path.exists(file_path):
294
+ raise ValueError(f"File {file_path} does not exist")
295
+
296
+ with open(file_path, "rb") as image_file:
297
+ image_bytes = image_file.read()
298
+ image_b64 = base64.b64encode(image_bytes).decode("utf-8")
299
+ image_type = mimetypes.guess_type(file_path)[0]
300
+ return ImageData(dataurl=f'data:{image_type};base64,{image_b64}')
301
+
241
302
 
242
303
  @ensure_no_previous_output
243
304
  def write_file(writefile: Writefile) -> str:
@@ -280,41 +341,24 @@ def take_help_of_ai_assistant(
280
341
  return output, cost
281
342
 
282
343
 
283
- class AddTasks(BaseModel):
284
- task_statement: str
285
-
286
-
287
- def add_task(addtask: AddTasks) -> str:
288
- petname_id = petname.Generate(2, "-")
289
- return petname_id
290
-
291
-
292
- class RemoveTask(BaseModel):
293
- task_id: str
294
-
295
-
296
- def remove_task(removetask: RemoveTask) -> str:
297
- return "removed"
298
-
299
-
300
344
  def which_tool(args: str) -> BaseModel:
301
345
  adapter = TypeAdapter[
302
- Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag
303
- ](Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag)
346
+ Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag | ReadImage
347
+ ](Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag | ReadImage)
304
348
  return adapter.validate_python(json.loads(args))
305
349
 
306
350
 
307
351
  def get_tool_output(
308
- args: dict | Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag,
352
+ args: dict | Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag | ReadImage,
309
353
  enc: tiktoken.Encoding,
310
354
  limit: float,
311
355
  loop_call: Callable[[str, float], tuple[str, float]],
312
356
  is_waiting_user_input: Callable[[str], tuple[BASH_CLF_OUTPUT, float]],
313
- ) -> tuple[str | DoneFlag, float]:
357
+ ) -> tuple[str | ImageData | DoneFlag, float]:
314
358
  if isinstance(args, dict):
315
359
  adapter = TypeAdapter[
316
- Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag
317
- ](Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag)
360
+ Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag | ReadImage
361
+ ](Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag | ReadImage)
318
362
  arg = adapter.validate_python(args)
319
363
  else:
320
364
  arg = args
@@ -334,12 +378,9 @@ def get_tool_output(
334
378
  elif isinstance(arg, AIAssistant):
335
379
  console.print("Calling AI assistant tool")
336
380
  output = take_help_of_ai_assistant(arg, limit, loop_call)
337
- elif isinstance(arg, AddTasks):
338
- console.print("Calling add task tool")
339
- output = add_task(arg), 0
340
- elif isinstance(arg, get_output_of_last_command):
341
- console.print("Calling get output of last program tool")
342
- output = get_output_of_last_command(enc), 0
381
+ elif isinstance(arg, ReadImage):
382
+ console.print("Calling read image tool")
383
+ output = read_image_from_shell(arg.file_path), 0.0
343
384
  else:
344
385
  raise ValueError(f"Unknown tool: {arg}")
345
386
 
@@ -353,9 +394,9 @@ History = list[ChatCompletionMessageParam]
353
394
  def get_is_waiting_user_input(model: Models, cost_data: CostData):
354
395
  enc = tiktoken.encoding_for_model(model if not model.startswith("o1") else "gpt-4o")
355
396
  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.
356
- Return `waiting_for_input` if the program is waiting for INTERACTIVE input only, Return false if it's waiting for external resources or just waiting to finish.
397
+ 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.
357
398
  Return `wont_exit` if the program won't exit, for example if it's a server.
358
- Return `normal` otherwise.
399
+ Return `running` otherwise.
359
400
  """
360
401
  history: History = [{"role": "system", "content": system_prompt}]
361
402
  client = OpenAI()
@@ -402,36 +443,35 @@ def execute_user_input() -> None:
402
443
  while True:
403
444
  discard_input()
404
445
  user_input = input()
405
- if user_input:
406
- with execution_lock:
407
- try:
408
- console.log(
409
- execute_bash(
410
- default_enc,
411
- ExecuteBash(
412
- send_ascii=[ord(x) for x in user_input] + [ord("\n")]
413
- ),
414
- lambda x: ("wont_exit", 0),
415
- )[0]
416
- )
417
- except Exception as e:
418
- traceback.print_exc()
419
- console.log(f"Error: {e}")
420
-
421
-
422
- async def register_client(server_url: str) -> None:
446
+ with execution_lock:
447
+ try:
448
+ console.log(
449
+ execute_bash(
450
+ default_enc,
451
+ ExecuteBash(
452
+ send_ascii=[ord(x) for x in user_input] + [ord("\n")]
453
+ ),
454
+ lambda x: ("waiting_for_input", 0),
455
+ )[0]
456
+ )
457
+ except Exception as e:
458
+ traceback.print_exc()
459
+ console.log(f"Error: {e}")
460
+
461
+
462
+ async def register_client(server_url: str, client_uuid: str = "") -> None:
423
463
  global default_enc, default_model, curr_cost
424
464
  # Generate a unique UUID for this client
425
- client_uuid = str(uuid.uuid4())
426
- print(f"Connecting with UUID: {client_uuid}")
465
+ if not client_uuid:
466
+ client_uuid = str(uuid.uuid4())
427
467
 
428
468
  # Create the WebSocket connection
429
469
  async with websockets.connect(f"{server_url}/{client_uuid}") as websocket:
470
+ print(f"Connected. Share this user id with the chatbot: {client_uuid} \nLink: https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access")
430
471
  try:
431
472
  while True:
432
473
  # Wait to receive data from the server
433
474
  message = await websocket.recv()
434
- print(message, type(message))
435
475
  mdata = Mdata.model_validate_json(message)
436
476
  with execution_lock:
437
477
  # is_waiting_user_input = get_is_waiting_user_input(
@@ -454,8 +494,9 @@ async def register_client(server_url: str) -> None:
454
494
  assert not isinstance(output, DoneFlag)
455
495
  await websocket.send(output)
456
496
 
457
- except websockets.ConnectionClosed:
458
- print(f"Connection closed for UUID: {client_uuid}")
497
+ except (websockets.ConnectionClosed, ConnectionError):
498
+ print(f"Connection closed for UUID: {client_uuid}, retrying")
499
+ await register_client(server_url, client_uuid)
459
500
 
460
501
 
461
502
  def run() -> None:
@@ -463,4 +504,12 @@ def run() -> None:
463
504
  server_url = sys.argv[1]
464
505
  else:
465
506
  server_url = "wss://wcgw.arcfu.com/register"
466
- asyncio.run(register_client(server_url))
507
+
508
+ thread1 = threading.Thread(target=execute_user_input)
509
+ thread2 = threading.Thread(target=asyncio.run, args=(register_client(server_url),))
510
+
511
+ thread1.start()
512
+ thread2.start()
513
+
514
+ thread1.join()
515
+ thread2.join()
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: wcgw
3
- Version: 0.0.5
3
+ Version: 0.0.7
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
- Requires-Python: >=3.7
7
+ Requires-Python: >=3.8
8
+ Requires-Dist: anthropic>=0.36.2
8
9
  Requires-Dist: fastapi>=0.115.0
9
10
  Requires-Dist: mypy>=1.11.2
10
11
  Requires-Dist: openai>=1.46.0
@@ -22,10 +23,18 @@ Requires-Dist: uvicorn>=0.31.0
22
23
  Requires-Dist: websockets>=13.1
23
24
  Description-Content-Type: text/markdown
24
25
 
25
- # Giving full shell access to Chatgpt web (What could go wrong?)
26
- Steps:
27
- 1. First run the client in any directory of choice
28
- 2. Use this custom gpt `https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access` to let it interact with your shell.
26
+ # Shell access to chatgpt.com
27
+
28
+ ### 🚀 Highlights
29
+ - **Full Shell Access**: No restrictions, complete control.
30
+ - ⚡ **Create, Execute, Iterate**: Seamless workflow for development and execution.
31
+ - ⚡ **Interactive Command Handling**: Supports interactive commands with ease.
32
+
33
+
34
+ ### 🪜 Steps:
35
+ 1. Run the [cli client](https://github.com/rusiaaman/wcgw?tab=readme-ov-file#client) in any directory of choice.
36
+ 2. Share the generated id with the GPT: `https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access`
37
+ 3. The custom GPT can now run any command on your cli
29
38
 
30
39
  ## Client
31
40
 
@@ -49,7 +58,7 @@ https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access
49
58
 
50
59
  Add user id the client generated to the first message along with the instructions.
51
60
 
52
- # How does it work?
61
+ # How it works
53
62
  Your commands are relayed through a server I've hosted at https://wcgw.arcfu.com. The code for that is at `src/relay/serve.py`.
54
63
 
55
64
  The user id that you share with chatgpt is added in the request it sents to the relay server which holds a websocket with the terminal client.
@@ -0,0 +1,10 @@
1
+ wcgw/__init__.py,sha256=okSsOWpTKDjEQzgOin3Kdpx4Mc3MFX1RunjopHQSIWE,62
2
+ wcgw/__main__.py,sha256=MjJnFwfYzA1rW47xuSP1EVsi53DTHeEGqESkQwsELFQ,34
3
+ wcgw/basic.py,sha256=o_iyg3Rmqz08LTWzgO7JIA0u_5l6GGaXyJe0zw73v8w,15085
4
+ wcgw/common.py,sha256=gHiP1RVHkvp10jPc1Xzg5DtUqhGbgQ7pTJK7OvUXfZQ,1764
5
+ wcgw/openai_utils.py,sha256=YNwCsA-Wqq7jWrxP0rfQmBTb1dI0s7dWXzQqyTzOZT4,2629
6
+ wcgw/tools.py,sha256=ozJtcuOlMrdGoDmSv4UpolnStdY7Xz8Z7JyrRllKA7s,17088
7
+ wcgw-0.0.7.dist-info/METADATA,sha256=Ei676pDMqOXetAPPmxU5wAXZ6mWRJfLq8eJHyhfKSTY,2022
8
+ wcgw-0.0.7.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
9
+ wcgw-0.0.7.dist-info/entry_points.txt,sha256=T-IH7w6Vc650hr8xksC8kJfbJR4uwN8HDudejwDwrNM,59
10
+ wcgw-0.0.7.dist-info/RECORD,,
wcgw/openai_adapters.py DELETED
File without changes
@@ -1,11 +0,0 @@
1
- wcgw/__init__.py,sha256=okSsOWpTKDjEQzgOin3Kdpx4Mc3MFX1RunjopHQSIWE,62
2
- wcgw/__main__.py,sha256=MjJnFwfYzA1rW47xuSP1EVsi53DTHeEGqESkQwsELFQ,34
3
- wcgw/basic.py,sha256=BiVjIwrtiz93SkUedDXSwtfVMKoV8-zEWeFKBIamVSQ,12372
4
- wcgw/common.py,sha256=jn39zTpaFUO1PWof_z7qBmklaZH5G1blzjlBvez0cg4,1225
5
- wcgw/openai_adapters.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- wcgw/openai_utils.py,sha256=4Hr9S2-WT8xhDdu3b2YVoX1l9AVxwCFdi_GJbQAx7Us,2202
7
- wcgw/tools.py,sha256=IHCMbB8Eg1LFhY0sI3e0CMUuGWXCdU7-168efpTk9eo,15155
8
- wcgw-0.0.5.dist-info/METADATA,sha256=_HvXQu02oMxiFjtBkJ1wJsAMDjFEkMn7nt4c8noa-c0,1669
9
- wcgw-0.0.5.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
10
- wcgw-0.0.5.dist-info/entry_points.txt,sha256=T-IH7w6Vc650hr8xksC8kJfbJR4uwN8HDudejwDwrNM,59
11
- wcgw-0.0.5.dist-info/RECORD,,
File without changes