wcgw 1.3.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.
- wcgw/__init__.py +1 -0
- wcgw/client/__main__.py +2 -2
- wcgw/client/anthropic_client.py +83 -38
- wcgw/client/computer_use.py +416 -0
- wcgw/client/mcp_server/Readme.md +73 -0
- wcgw/client/mcp_server/__init__.py +11 -0
- wcgw/client/mcp_server/server.py +283 -0
- wcgw/client/openai_client.py +3 -3
- wcgw/client/sys_utils.py +40 -0
- wcgw/client/tools.py +184 -88
- wcgw/relay/serve.py +5 -12
- wcgw/types_.py +42 -6
- {wcgw-1.3.0.dist-info → wcgw-1.5.0.dist-info}/METADATA +75 -24
- wcgw-1.5.0.dist-info/RECORD +22 -0
- {wcgw-1.3.0.dist-info → wcgw-1.5.0.dist-info}/entry_points.txt +1 -0
- wcgw-1.3.0.dist-info/RECORD +0 -17
- {wcgw-1.3.0.dist-info → wcgw-1.5.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import importlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import traceback
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from mcp.server.models import InitializationOptions
|
|
10
|
+
import mcp.types as types
|
|
11
|
+
from mcp.types import Tool as ToolParam
|
|
12
|
+
from mcp.server import NotificationOptions, Server
|
|
13
|
+
from pydantic import AnyUrl, ValidationError
|
|
14
|
+
import mcp.server.stdio
|
|
15
|
+
from ..tools import DoneFlag, get_tool_output, which_tool_name, default_enc
|
|
16
|
+
from ...types_ import (
|
|
17
|
+
BashCommand,
|
|
18
|
+
BashInteraction,
|
|
19
|
+
CreateFileNew,
|
|
20
|
+
FileEdit,
|
|
21
|
+
Keyboard,
|
|
22
|
+
Mouse,
|
|
23
|
+
ReadFile,
|
|
24
|
+
ReadImage,
|
|
25
|
+
ResetShell,
|
|
26
|
+
Initialize,
|
|
27
|
+
ScreenShot,
|
|
28
|
+
GetScreenInfo,
|
|
29
|
+
)
|
|
30
|
+
from ..computer_use import Computer
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
server = Server("wcgw")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@server.list_resources() # type: ignore
|
|
37
|
+
async def handle_list_resources() -> list[types.Resource]:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@server.read_resource() # type: ignore
|
|
42
|
+
async def handle_read_resource(uri: AnyUrl) -> str:
|
|
43
|
+
raise ValueError("No resources available")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@server.list_prompts() # type: ignore
|
|
47
|
+
async def handle_list_prompts() -> list[types.Prompt]:
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@server.get_prompt() # type: ignore
|
|
52
|
+
async def handle_get_prompt(
|
|
53
|
+
name: str, arguments: dict[str, str] | None
|
|
54
|
+
) -> types.GetPromptResult:
|
|
55
|
+
return types.GetPromptResult(messages=[])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@server.list_tools() # type: ignore
|
|
59
|
+
async def handle_list_tools() -> list[types.Tool]:
|
|
60
|
+
"""
|
|
61
|
+
List available tools.
|
|
62
|
+
Each tool specifies its arguments using JSON Schema validation.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
with open(
|
|
66
|
+
os.path.join(
|
|
67
|
+
os.path.dirname(os.path.dirname(__file__)), "diff-instructions.txt"
|
|
68
|
+
)
|
|
69
|
+
) as f:
|
|
70
|
+
diffinstructions = f.read()
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
ToolParam(
|
|
74
|
+
inputSchema=Initialize.model_json_schema(),
|
|
75
|
+
name="Initialize",
|
|
76
|
+
description="""
|
|
77
|
+
- Always call this at the start of the conversation before anything else.
|
|
78
|
+
""",
|
|
79
|
+
),
|
|
80
|
+
ToolParam(
|
|
81
|
+
inputSchema=BashCommand.model_json_schema(),
|
|
82
|
+
name="BashCommand",
|
|
83
|
+
description="""
|
|
84
|
+
- Execute a bash command. This is stateful (beware with subsequent calls).
|
|
85
|
+
- Do not use interactive commands like nano. Prefer writing simpler commands.
|
|
86
|
+
- Status of the command and the current working directory will always be returned at the end.
|
|
87
|
+
- Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
|
|
88
|
+
- The first line might be `(...truncated)` if the output is too long.
|
|
89
|
+
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
|
|
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 "&".
|
|
92
|
+
""",
|
|
93
|
+
),
|
|
94
|
+
ToolParam(
|
|
95
|
+
inputSchema=BashInteraction.model_json_schema(),
|
|
96
|
+
name="BashInteraction",
|
|
97
|
+
description="""
|
|
98
|
+
- Interact with running program using this tool
|
|
99
|
+
- Special keys like arrows, interrupts, enter, etc.
|
|
100
|
+
- Send text input to the running program.
|
|
101
|
+
- Send send_specials=["Enter"] to recheck status of a running program.
|
|
102
|
+
- Only one of send_text, send_specials, send_ascii should be provided.
|
|
103
|
+
""",
|
|
104
|
+
),
|
|
105
|
+
ToolParam(
|
|
106
|
+
inputSchema=ReadFile.model_json_schema(),
|
|
107
|
+
name="ReadFile",
|
|
108
|
+
description="""
|
|
109
|
+
- Read full file content
|
|
110
|
+
- Provide absolute file path only
|
|
111
|
+
""",
|
|
112
|
+
),
|
|
113
|
+
ToolParam(
|
|
114
|
+
inputSchema=CreateFileNew.model_json_schema(),
|
|
115
|
+
name="CreateFileNew",
|
|
116
|
+
description="""
|
|
117
|
+
- Write content to a new file. Provide file path and content. Use this instead of BashCommand for writing new files.
|
|
118
|
+
- Provide absolute file path only.
|
|
119
|
+
- For editing existing files, use FileEdit instead of this tool.
|
|
120
|
+
""",
|
|
121
|
+
),
|
|
122
|
+
ToolParam(
|
|
123
|
+
inputSchema=ReadImage.model_json_schema(),
|
|
124
|
+
name="ReadImage",
|
|
125
|
+
description="Read an image from the shell.",
|
|
126
|
+
),
|
|
127
|
+
ToolParam(
|
|
128
|
+
inputSchema=ResetShell.model_json_schema(),
|
|
129
|
+
name="ResetShell",
|
|
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.",
|
|
131
|
+
),
|
|
132
|
+
ToolParam(
|
|
133
|
+
inputSchema=FileEdit.model_json_schema(),
|
|
134
|
+
name="FileEdit",
|
|
135
|
+
description="""
|
|
136
|
+
- Use absolute file path only.
|
|
137
|
+
- Use SEARCH/REPLACE blocks to edit the file.
|
|
138
|
+
"""
|
|
139
|
+
+ diffinstructions,
|
|
140
|
+
),
|
|
141
|
+
ToolParam(
|
|
142
|
+
inputSchema=ReadImage.model_json_schema(),
|
|
143
|
+
name="ReadImage",
|
|
144
|
+
description="""
|
|
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.
|
|
186
|
+
""",
|
|
187
|
+
),
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@server.call_tool() # type: ignore
|
|
192
|
+
async def handle_call_tool(
|
|
193
|
+
name: str, arguments: dict[str, Any] | None
|
|
194
|
+
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
|
195
|
+
if not arguments:
|
|
196
|
+
raise ValueError("Missing arguments")
|
|
197
|
+
|
|
198
|
+
tool_type = which_tool_name(name)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
tool_call = tool_type(**arguments)
|
|
202
|
+
except ValidationError:
|
|
203
|
+
|
|
204
|
+
def try_json(x: str) -> Any:
|
|
205
|
+
try:
|
|
206
|
+
return json.loads(x)
|
|
207
|
+
except json.JSONDecodeError:
|
|
208
|
+
return x
|
|
209
|
+
|
|
210
|
+
tool_call = tool_type(**{k: try_json(v) for k, v in arguments.items()})
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
output_or_dones, _ = get_tool_output(
|
|
214
|
+
tool_call, default_enc, 0.0, lambda x, y: ("", 0), 8000
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
output_or_dones = [f"GOT EXCEPTION while calling tool. Error: {e}"]
|
|
219
|
+
tb = traceback.format_exc()
|
|
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.
|
|
230
|
+
|
|
231
|
+
Instructions:
|
|
232
|
+
|
|
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.
|
|
237
|
+
|
|
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.
|
|
241
|
+
|
|
242
|
+
Always write production ready, syntactically correct code.
|
|
243
|
+
"""
|
|
244
|
+
|
|
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
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def main() -> None:
|
|
259
|
+
version = importlib.metadata.version("wcgw")
|
|
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
|
wcgw/client/openai_client.py
CHANGED
|
@@ -27,7 +27,6 @@ from ..types_ import (
|
|
|
27
27
|
FileEdit,
|
|
28
28
|
ReadImage,
|
|
29
29
|
ReadFile,
|
|
30
|
-
Writefile,
|
|
31
30
|
ResetShell,
|
|
32
31
|
)
|
|
33
32
|
|
|
@@ -177,6 +176,7 @@ def loop(
|
|
|
177
176
|
- The first line might be `(...truncated)` if the output is too long.
|
|
178
177
|
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
|
|
179
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 "&".
|
|
180
180
|
""",
|
|
181
181
|
),
|
|
182
182
|
openai.pydantic_function_tool(
|
|
@@ -199,7 +199,6 @@ def loop(
|
|
|
199
199
|
CreateFileNew,
|
|
200
200
|
description="""
|
|
201
201
|
- Write content to a new file. Provide file path and content. Use this instead of BashCommand for writing new files.
|
|
202
|
-
- This doesn't create any directories, please create directories using `mkdir -p` BashCommand.
|
|
203
202
|
- Provide absolute file path only.
|
|
204
203
|
- For editing existing files, use FileEdit instead of this tool.""",
|
|
205
204
|
),
|
|
@@ -337,13 +336,14 @@ System information:
|
|
|
337
336
|
for tool_call_id, toolcallargs in tool_call_args_by_id.items():
|
|
338
337
|
for toolindex, tool_args in toolcallargs.items():
|
|
339
338
|
try:
|
|
340
|
-
|
|
339
|
+
output_or_dones, cost_ = get_tool_output(
|
|
341
340
|
json.loads(tool_args),
|
|
342
341
|
enc,
|
|
343
342
|
limit - cost,
|
|
344
343
|
loop,
|
|
345
344
|
max_tokens=8000,
|
|
346
345
|
)
|
|
346
|
+
output_or_done = output_or_dones[0]
|
|
347
347
|
except Exception as e:
|
|
348
348
|
output_or_done = (
|
|
349
349
|
f"GOT EXCEPTION while calling tool. Error: {e}"
|
wcgw/client/sys_utils.py
ADDED
|
@@ -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
|