wcgw 1.4.0__py3-none-any.whl → 1.5.0__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.

@@ -2,6 +2,7 @@ import asyncio
2
2
  import importlib
3
3
  import json
4
4
  import os
5
+ import sys
5
6
  import traceback
6
7
  from typing import Any
7
8
 
@@ -17,39 +18,44 @@ from ...types_ import (
17
18
  BashInteraction,
18
19
  CreateFileNew,
19
20
  FileEdit,
21
+ Keyboard,
22
+ Mouse,
20
23
  ReadFile,
21
24
  ReadImage,
22
25
  ResetShell,
23
26
  Initialize,
27
+ ScreenShot,
28
+ GetScreenInfo,
24
29
  )
30
+ from ..computer_use import Computer
25
31
 
26
32
 
27
33
  server = Server("wcgw")
28
34
 
29
35
 
30
- @server.list_resources()
36
+ @server.list_resources() # type: ignore
31
37
  async def handle_list_resources() -> list[types.Resource]:
32
38
  return []
33
39
 
34
40
 
35
- @server.read_resource()
41
+ @server.read_resource() # type: ignore
36
42
  async def handle_read_resource(uri: AnyUrl) -> str:
37
43
  raise ValueError("No resources available")
38
44
 
39
45
 
40
- @server.list_prompts()
46
+ @server.list_prompts() # type: ignore
41
47
  async def handle_list_prompts() -> list[types.Prompt]:
42
48
  return []
43
49
 
44
50
 
45
- @server.get_prompt()
51
+ @server.get_prompt() # type: ignore
46
52
  async def handle_get_prompt(
47
53
  name: str, arguments: dict[str, str] | None
48
54
  ) -> types.GetPromptResult:
49
- types.GetPromptResult(messages=[])
55
+ return types.GetPromptResult(messages=[])
50
56
 
51
57
 
52
- @server.list_tools()
58
+ @server.list_tools() # type: ignore
53
59
  async def handle_list_tools() -> list[types.Tool]:
54
60
  """
55
61
  List available tools.
@@ -62,6 +68,7 @@ async def handle_list_tools() -> list[types.Tool]:
62
68
  )
63
69
  ) as f:
64
70
  diffinstructions = f.read()
71
+
65
72
  return [
66
73
  ToolParam(
67
74
  inputSchema=Initialize.model_json_schema(),
@@ -81,6 +88,7 @@ async def handle_list_tools() -> list[types.Tool]:
81
88
  - The first line might be `(...truncated)` if the output is too long.
82
89
  - Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
83
90
  - The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
91
+ - Run long running commands in background using screen instead of "&".
84
92
  """,
85
93
  ),
86
94
  ToolParam(
@@ -107,7 +115,6 @@ async def handle_list_tools() -> list[types.Tool]:
107
115
  name="CreateFileNew",
108
116
  description="""
109
117
  - Write content to a new file. Provide file path and content. Use this instead of BashCommand for writing new files.
110
- - This doesn't create any directories, please create directories using `mkdir -p` BashCommand.
111
118
  - Provide absolute file path only.
112
119
  - For editing existing files, use FileEdit instead of this tool.
113
120
  """,
@@ -120,7 +127,7 @@ async def handle_list_tools() -> list[types.Tool]:
120
127
  ToolParam(
121
128
  inputSchema=ResetShell.model_json_schema(),
122
129
  name="ResetShell",
123
- description="Resets the shell. Use only if all interrupts and prompt reset attempts have failed repeatedly.",
130
+ description="Resets the shell. Use only if all interrupts and prompt reset attempts have failed repeatedly.\nAlso exits the docker environment.\nYou need to call GetScreenInfo again.",
124
131
  ),
125
132
  ToolParam(
126
133
  inputSchema=FileEdit.model_json_schema(),
@@ -136,14 +143,54 @@ async def handle_list_tools() -> list[types.Tool]:
136
143
  name="ReadImage",
137
144
  description="""
138
145
  - Read an image from the shell.
146
+ """,
147
+ ),
148
+ ToolParam(
149
+ inputSchema=GetScreenInfo.model_json_schema(),
150
+ name="GetScreenInfo",
151
+ description="""
152
+ - Get display information of an OS running on docker using image "ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest"
153
+ - If user hasn't provided docker image id, check using `docker ps` and provide the id.
154
+ - Important: call this first in the conversation before ScreenShot, Mouse, and Keyboard tools.
155
+ - Connects shell to the docker environment.
156
+ - Note: once this is called, the shell enters the docker environment. All bash commands will run over there.
157
+ """,
158
+ ),
159
+ ToolParam(
160
+ inputSchema=ScreenShot.model_json_schema(),
161
+ name="ScreenShot",
162
+ description="""
163
+ - Capture screenshot of an OS running on docker using image "ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest"
164
+ - If user hasn't provided docker image id, check using `docker ps` and provide the id.
165
+ - Capture ScreenShot of the current screen for automation.
166
+ """,
167
+ ),
168
+ ToolParam(
169
+ inputSchema=Mouse.model_json_schema(),
170
+ name="Mouse",
171
+ description="""
172
+ - Interact with docker container running image "ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest"
173
+ - If user hasn't provided docker image id, check using `docker ps` and provide the id.
174
+ - Interact with the screen using mouse
175
+ """,
176
+ ),
177
+ ToolParam(
178
+ inputSchema=Keyboard.model_json_schema(),
179
+ name="Keyboard",
180
+ description="""
181
+ - Interact with docker container running image "ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest"
182
+ - If user hasn't provided docker image id, check using `docker ps` and provide the id.
183
+ - Emulate keyboard input to the screen
184
+ - Uses xdootool to send keyboard input, keys like Return, BackSpace, Escape, Page_Up, etc. can be used.
185
+ - Do not use it to interact with Bash tool.
139
186
  """,
140
187
  ),
141
188
  ]
142
189
 
143
190
 
144
- @server.call_tool()
191
+ @server.call_tool() # type: ignore
145
192
  async def handle_call_tool(
146
- name: str, arguments: dict | None
193
+ name: str, arguments: dict[str, Any] | None
147
194
  ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
148
195
  if not arguments:
149
196
  raise ValueError("Missing arguments")
@@ -163,60 +210,74 @@ async def handle_call_tool(
163
210
  tool_call = tool_type(**{k: try_json(v) for k, v in arguments.items()})
164
211
 
165
212
  try:
166
- output_or_done, _ = get_tool_output(
213
+ output_or_dones, _ = get_tool_output(
167
214
  tool_call, default_enc, 0.0, lambda x, y: ("", 0), 8000
168
215
  )
169
216
 
170
217
  except Exception as e:
171
- output_or_done = f"GOT EXCEPTION while calling tool. Error: {e}"
218
+ output_or_dones = [f"GOT EXCEPTION while calling tool. Error: {e}"]
172
219
  tb = traceback.format_exc()
173
- print(output_or_done + "\n" + tb)
174
-
175
- assert not isinstance(output_or_done, DoneFlag)
176
- if isinstance(output_or_done, str):
177
- if issubclass(tool_type, Initialize):
178
- output_or_done += """
179
-
180
- You're an expert software engineer with shell and code knowledge.
181
-
182
- Instructions:
183
-
184
- - You should use the provided bash execution, reading and writing file tools to complete objective.
185
- - First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
186
- - Always read relevant files before editing.
187
- - Do not provide code snippets unless asked by the user, instead directly edit the code.
220
+ print(str(output_or_dones[0]) + "\n" + tb)
221
+
222
+ content: list[types.TextContent | types.ImageContent | types.EmbeddedResource] = []
223
+ for output_or_done in output_or_dones:
224
+ assert not isinstance(output_or_done, DoneFlag)
225
+ if isinstance(output_or_done, str):
226
+ if issubclass(tool_type, Initialize):
227
+ output_or_done += """
228
+
229
+ You're an expert software engineer with shell and code knowledge.
188
230
 
231
+ Instructions:
189
232
 
190
- Additional instructions:
191
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost, or to get absolute cwd.
233
+ - You should use the provided bash execution, reading and writing file tools to complete objective.
234
+ - First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
235
+ - Always read relevant files before editing.
236
+ - Do not provide code snippets unless asked by the user, instead directly edit the code.
192
237
 
193
- Always write production ready, syntactically correct code.
194
- """
238
+
239
+ Additional instructions:
240
+ Always run `pwd` if you get any file or directory not found error to make sure you're not lost, or to get absolute cwd.
195
241
 
196
- return [types.TextContent(type="text", text=output_or_done)]
242
+ Always write production ready, syntactically correct code.
243
+ """
197
244
 
198
- return [
199
- types.ImageContent(
200
- type="image",
201
- data=output_or_done.data,
202
- mimeType=output_or_done.media_type,
203
- )
204
- ]
245
+ content.append(types.TextContent(type="text", text=output_or_done))
246
+ else:
247
+ content.append(
248
+ types.ImageContent(
249
+ type="image",
250
+ data=output_or_done.data,
251
+ mimeType=output_or_done.media_type,
252
+ )
253
+ )
254
+
255
+ return content
205
256
 
206
257
 
207
258
  async def main() -> None:
208
259
  version = importlib.metadata.version("wcgw")
209
- # Run the server using stdin/stdout streams
210
- async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
211
- await server.run(
212
- read_stream,
213
- write_stream,
214
- InitializationOptions(
215
- server_name="wcgw",
216
- server_version=version,
217
- capabilities=server.get_capabilities(
218
- notification_options=NotificationOptions(),
219
- experimental_capabilities={},
220
- ),
221
- ),
222
- )
260
+ while True:
261
+ try:
262
+ # Run the server using stdin/stdout streams
263
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
264
+ await server.run(
265
+ read_stream,
266
+ write_stream,
267
+ InitializationOptions(
268
+ server_name="wcgw",
269
+ server_version=version,
270
+ capabilities=server.get_capabilities(
271
+ notification_options=NotificationOptions(),
272
+ experimental_capabilities={},
273
+ ),
274
+ ),
275
+ raise_exceptions=True,
276
+ )
277
+ except BaseException as e:
278
+ print(f"Server encountered an error: {e}", file=sys.stderr)
279
+ print("Stack trace:", file=sys.stderr)
280
+ traceback.print_exc(file=sys.stderr)
281
+ print("Restarting server in 5 seconds...", file=sys.stderr)
282
+ await asyncio.sleep(5)
283
+ continue
@@ -176,6 +176,7 @@ def loop(
176
176
  - The first line might be `(...truncated)` if the output is too long.
177
177
  - Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
178
178
  - The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
179
+ - Run long running commands in background using screen instead of "&".
179
180
  """,
180
181
  ),
181
182
  openai.pydantic_function_tool(
@@ -198,7 +199,6 @@ def loop(
198
199
  CreateFileNew,
199
200
  description="""
200
201
  - Write content to a new file. Provide file path and content. Use this instead of BashCommand for writing new files.
201
- - This doesn't create any directories, please create directories using `mkdir -p` BashCommand.
202
202
  - Provide absolute file path only.
203
203
  - For editing existing files, use FileEdit instead of this tool.""",
204
204
  ),
@@ -336,13 +336,14 @@ System information:
336
336
  for tool_call_id, toolcallargs in tool_call_args_by_id.items():
337
337
  for toolindex, tool_args in toolcallargs.items():
338
338
  try:
339
- output_or_done, cost_ = get_tool_output(
339
+ output_or_dones, cost_ = get_tool_output(
340
340
  json.loads(tool_args),
341
341
  enc,
342
342
  limit - cost,
343
343
  loop,
344
344
  max_tokens=8000,
345
345
  )
346
+ output_or_done = output_or_dones[0]
346
347
  except Exception as e:
347
348
  output_or_done = (
348
349
  f"GOT EXCEPTION while calling tool. Error: {e}"
@@ -0,0 +1,40 @@
1
+ import subprocess
2
+
3
+ MAX_RESPONSE_LEN: int = 16000
4
+
5
+
6
+ def maybe_truncate(content: str, truncate_after: int | None = MAX_RESPONSE_LEN) -> str:
7
+ """Truncate content and append a notice if content exceeds the specified length."""
8
+ return (
9
+ content
10
+ if not truncate_after or len(content) <= truncate_after
11
+ else content[:truncate_after] + TRUNCATED_MESSAGE
12
+ )
13
+
14
+
15
+ def command_run(
16
+ cmd: str,
17
+ timeout: float | None = 30.0, # seconds
18
+ truncate_after: int | None = MAX_RESPONSE_LEN,
19
+ text: bool = True,
20
+ ) -> tuple[int, str, str]:
21
+ """Run a shell command synchronously with a timeout."""
22
+ try:
23
+ process = subprocess.Popen(
24
+ cmd,
25
+ shell=True,
26
+ stdout=subprocess.PIPE,
27
+ stderr=subprocess.PIPE,
28
+ text=text,
29
+ )
30
+ stdout, stderr = process.communicate(timeout=timeout)
31
+ return (
32
+ process.returncode or 0,
33
+ maybe_truncate(stdout, truncate_after=truncate_after),
34
+ maybe_truncate(stderr, truncate_after=truncate_after),
35
+ )
36
+ except subprocess.TimeoutExpired as exc:
37
+ process.kill()
38
+ raise TimeoutError(
39
+ f"Command '{cmd}' timed out after {timeout} seconds"
40
+ ) from exc