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

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