wcgw 2.5.0__py3-none-any.whl → 2.6.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/client/mcp_server/server.py +34 -29
- wcgw/client/openai_client.py +17 -29
- wcgw/client/repo_ops/display_tree.py +127 -0
- wcgw/client/repo_ops/path_prob.py +58 -0
- wcgw/client/repo_ops/paths_model.vocab +20000 -0
- wcgw/client/repo_ops/paths_tokens.model +80042 -0
- wcgw/client/repo_ops/repo_context.py +148 -0
- wcgw/client/tools.py +70 -41
- wcgw/types_.py +3 -1
- {wcgw-2.5.0.dist-info → wcgw-2.6.1.dist-info}/METADATA +17 -54
- {wcgw-2.5.0.dist-info → wcgw-2.6.1.dist-info}/RECORD +14 -9
- wcgw-2.6.1.dist-info/licenses/LICENSE +213 -0
- wcgw-2.5.0.dist-info/licenses/LICENSE +0 -243
- {wcgw-2.5.0.dist-info → wcgw-2.6.1.dist-info}/WHEEL +0 -0
- {wcgw-2.5.0.dist-info → wcgw-2.6.1.dist-info}/entry_points.txt +0 -0
wcgw/client/mcp_server/server.py
CHANGED
|
@@ -1,34 +1,33 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import importlib
|
|
3
2
|
import json
|
|
4
3
|
import os
|
|
5
|
-
import sys
|
|
6
|
-
import traceback
|
|
7
4
|
from typing import Any
|
|
8
5
|
|
|
9
|
-
from
|
|
6
|
+
from pydantic import AnyUrl, ValidationError
|
|
7
|
+
|
|
8
|
+
import mcp_wcgw.server.stdio
|
|
10
9
|
import mcp_wcgw.types as types
|
|
11
|
-
from mcp_wcgw.types import Tool as ToolParam
|
|
12
10
|
from mcp_wcgw.server import NotificationOptions, Server
|
|
13
|
-
from
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
from ..tools import DoneFlag, get_tool_output, which_tool_name, default_enc
|
|
11
|
+
from mcp_wcgw.server.models import InitializationOptions
|
|
12
|
+
from mcp_wcgw.types import Tool as ToolParam
|
|
13
|
+
|
|
17
14
|
from ...types_ import (
|
|
18
15
|
BashCommand,
|
|
19
16
|
BashInteraction,
|
|
20
|
-
WriteIfEmpty,
|
|
21
17
|
FileEdit,
|
|
18
|
+
GetScreenInfo,
|
|
19
|
+
Initialize,
|
|
22
20
|
Keyboard,
|
|
23
21
|
Mouse,
|
|
24
22
|
ReadFiles,
|
|
25
23
|
ReadImage,
|
|
26
24
|
ResetShell,
|
|
27
|
-
Initialize,
|
|
28
25
|
ScreenShot,
|
|
29
|
-
|
|
26
|
+
WriteIfEmpty,
|
|
30
27
|
)
|
|
28
|
+
from .. import tools
|
|
31
29
|
from ..computer_use import SLEEP_TIME_MAX_S
|
|
30
|
+
from ..tools import DoneFlag, default_enc, get_tool_output, which_tool_name
|
|
32
31
|
|
|
33
32
|
COMPUTER_USE_ON_DOCKER_ENABLED = False
|
|
34
33
|
|
|
@@ -77,6 +76,12 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
77
76
|
name="Initialize",
|
|
78
77
|
description="""
|
|
79
78
|
- Always call this at the start of the conversation before using any of the shell tools from wcgw.
|
|
79
|
+
- This will reset the shell.
|
|
80
|
+
- Use `any_workspace_path` to initialize the shell in the appropriate project directory.
|
|
81
|
+
- If the user has mentioned a workspace or project root, use it to set `any_workspace_path`.
|
|
82
|
+
- If the user has mentioned a folder or file with unclear project root, use the file or folder as `any_workspace_path`.
|
|
83
|
+
- If user has mentioned any files use `initial_files_to_read` to read, use absolute paths only.
|
|
84
|
+
- If `any_workspace_path` is provided, a tree structure of the workspace will be shown.
|
|
80
85
|
""",
|
|
81
86
|
),
|
|
82
87
|
ToolParam(
|
|
@@ -236,24 +241,24 @@ async def handle_call_tool(
|
|
|
236
241
|
if isinstance(output_or_done, str):
|
|
237
242
|
if issubclass(tool_type, Initialize):
|
|
238
243
|
output_or_done += """
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
---
|
|
245
|
+
You're an expert software engineer with shell and code knowledge.
|
|
246
|
+
|
|
247
|
+
Instructions:
|
|
248
|
+
|
|
249
|
+
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
250
|
+
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
251
|
+
- Always read relevant files before editing.
|
|
252
|
+
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
253
|
+
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
254
|
+
- Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using shell tools.
|
|
255
|
+
- Do not use Ctrl-c or Ctrl-z or interrupt commands without asking the user, because often the program don't show any update but they still are running.
|
|
256
|
+
- Do not use echo to write multi-line files, always use FileEdit tool to update a code.
|
|
243
257
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
249
|
-
- Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using shell tools.
|
|
250
|
-
- Do not use Ctrl-c or Ctrl-z or interrupt commands without asking the user, because often the program don't show any update but they still are running.
|
|
251
|
-
- Do not use echo to write multi-line files, always use FileEdit tool to update a code.
|
|
252
|
-
|
|
253
|
-
Additional instructions:
|
|
254
|
-
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.
|
|
255
|
-
|
|
256
|
-
Always write production ready, syntactically correct code.
|
|
258
|
+
Additional instructions:
|
|
259
|
+
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.
|
|
260
|
+
|
|
261
|
+
Always write production ready, syntactically correct code.
|
|
257
262
|
"""
|
|
258
263
|
|
|
259
264
|
content.append(types.TextContent(type="text", text=output_or_done))
|
wcgw/client/openai_client.py
CHANGED
|
@@ -1,58 +1,46 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import json
|
|
3
3
|
import mimetypes
|
|
4
|
-
|
|
5
|
-
import
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
6
7
|
import traceback
|
|
7
|
-
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import DefaultDict, Optional, cast
|
|
11
|
+
|
|
8
12
|
import openai
|
|
13
|
+
import petname # type: ignore[import-untyped]
|
|
14
|
+
import rich
|
|
15
|
+
import tokenizers # type: ignore[import-untyped]
|
|
16
|
+
from dotenv import load_dotenv
|
|
9
17
|
from openai import OpenAI
|
|
10
18
|
from openai.types.chat import (
|
|
19
|
+
ChatCompletionContentPartParam,
|
|
11
20
|
ChatCompletionMessageParam,
|
|
12
|
-
ChatCompletionAssistantMessageParam,
|
|
13
21
|
ChatCompletionUserMessageParam,
|
|
14
|
-
ChatCompletionContentPartParam,
|
|
15
|
-
ChatCompletionMessage,
|
|
16
|
-
ParsedChatCompletionMessage,
|
|
17
22
|
)
|
|
18
|
-
import
|
|
19
|
-
import petname # type: ignore[import-untyped]
|
|
20
|
-
import tokenizers # type: ignore[import-untyped]
|
|
23
|
+
from pydantic import BaseModel
|
|
21
24
|
from typer import Typer
|
|
22
|
-
import uuid
|
|
23
25
|
|
|
24
26
|
from ..types_ import (
|
|
25
27
|
BashCommand,
|
|
26
28
|
BashInteraction,
|
|
27
|
-
WriteIfEmpty,
|
|
28
29
|
FileEdit,
|
|
29
|
-
ReadImage,
|
|
30
30
|
ReadFiles,
|
|
31
|
+
ReadImage,
|
|
31
32
|
ResetShell,
|
|
33
|
+
WriteIfEmpty,
|
|
32
34
|
)
|
|
33
|
-
|
|
34
|
-
from .common import Models, discard_input
|
|
35
|
-
from .common import CostData, History
|
|
35
|
+
from .common import CostData, History, Models, discard_input
|
|
36
36
|
from .openai_utils import get_input_cost, get_output_cost
|
|
37
|
-
from .tools import ImageData
|
|
38
|
-
|
|
39
37
|
from .tools import (
|
|
40
38
|
DoneFlag,
|
|
39
|
+
ImageData,
|
|
41
40
|
get_tool_output,
|
|
42
41
|
which_tool,
|
|
43
42
|
)
|
|
44
43
|
|
|
45
|
-
from urllib import parse
|
|
46
|
-
import subprocess
|
|
47
|
-
import os
|
|
48
|
-
import tempfile
|
|
49
|
-
|
|
50
|
-
import toml
|
|
51
|
-
from pydantic import BaseModel
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
from dotenv import load_dotenv
|
|
55
|
-
|
|
56
44
|
|
|
57
45
|
class Config(BaseModel):
|
|
58
46
|
model: Models
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Set
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DirectoryTree:
|
|
7
|
+
def __init__(self, root: Path, max_files: int = 10):
|
|
8
|
+
"""
|
|
9
|
+
Initialize the DirectoryTree with a root path and maximum number of files to display
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
root_path: The root directory path to start from
|
|
13
|
+
max_files: Maximum number of files to display in unexpanded directories
|
|
14
|
+
"""
|
|
15
|
+
self.root = root
|
|
16
|
+
self.max_files = max_files
|
|
17
|
+
self.expanded_files: Set[Path] = set()
|
|
18
|
+
self.expanded_dirs = set[Path]()
|
|
19
|
+
|
|
20
|
+
if not self.root.exists():
|
|
21
|
+
raise ValueError(f"Root path {root} does not exist")
|
|
22
|
+
|
|
23
|
+
if not self.root.is_dir():
|
|
24
|
+
raise ValueError(f"Root path {root} is not a directory")
|
|
25
|
+
|
|
26
|
+
def expand(self, rel_path: str) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Expand a specific file in the tree
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
rel_path: Relative path from root to the file to expand
|
|
32
|
+
"""
|
|
33
|
+
abs_path = self.root / rel_path
|
|
34
|
+
|
|
35
|
+
if not abs_path.exists():
|
|
36
|
+
raise ValueError(f"Path {rel_path} does not exist")
|
|
37
|
+
|
|
38
|
+
if not abs_path.is_file():
|
|
39
|
+
raise ValueError(f"Path {rel_path} is not a file")
|
|
40
|
+
|
|
41
|
+
if not str(abs_path).startswith(str(self.root)):
|
|
42
|
+
raise ValueError(f"Path {rel_path} is outside root directory")
|
|
43
|
+
|
|
44
|
+
self.expanded_files.add(abs_path)
|
|
45
|
+
|
|
46
|
+
# Add all parent directories to expanded dirs
|
|
47
|
+
current = abs_path.parent
|
|
48
|
+
while str(current) >= str(self.root):
|
|
49
|
+
if current not in self.expanded_dirs:
|
|
50
|
+
self.expanded_dirs.add(current)
|
|
51
|
+
if current == current.parent:
|
|
52
|
+
break
|
|
53
|
+
current = current.parent
|
|
54
|
+
|
|
55
|
+
def _list_directory(self, dir_path: Path) -> List[Path]:
|
|
56
|
+
"""List contents of a directory, sorted with directories first"""
|
|
57
|
+
contents = list(dir_path.iterdir())
|
|
58
|
+
return sorted(contents, key=lambda x: (not x.is_dir(), x.name.lower()))
|
|
59
|
+
|
|
60
|
+
def _count_hidden_items(
|
|
61
|
+
self, dir_path: Path, shown_items: List[Path]
|
|
62
|
+
) -> tuple[int, int]:
|
|
63
|
+
"""Count hidden files and directories in a directory"""
|
|
64
|
+
all_items = set(self._list_directory(dir_path))
|
|
65
|
+
shown_items_set = set(shown_items)
|
|
66
|
+
hidden_items = all_items - shown_items_set
|
|
67
|
+
|
|
68
|
+
hidden_files = sum(1 for p in hidden_items if p.is_file())
|
|
69
|
+
hidden_dirs = sum(1 for p in hidden_items if p.is_dir())
|
|
70
|
+
|
|
71
|
+
return hidden_files, hidden_dirs
|
|
72
|
+
|
|
73
|
+
def display(self) -> str:
|
|
74
|
+
"""Display the directory tree with expanded state"""
|
|
75
|
+
writer = io.StringIO()
|
|
76
|
+
|
|
77
|
+
def _display_recursive(
|
|
78
|
+
current_path: Path, indent: int = 0, depth: int = 0
|
|
79
|
+
) -> None:
|
|
80
|
+
# Print current directory name
|
|
81
|
+
if current_path == self.root:
|
|
82
|
+
writer.write(f"{current_path}\n")
|
|
83
|
+
else:
|
|
84
|
+
writer.write(f"{' ' * indent}{current_path.name}\n")
|
|
85
|
+
|
|
86
|
+
# Don't recurse beyond depth 1 unless path contains expanded files
|
|
87
|
+
if depth > 0 and current_path not in self.expanded_dirs:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Get directory contents
|
|
91
|
+
contents = self._list_directory(current_path)
|
|
92
|
+
shown_items = []
|
|
93
|
+
|
|
94
|
+
for item in contents:
|
|
95
|
+
# Show items only if:
|
|
96
|
+
# 1. They are expanded files
|
|
97
|
+
# 2. They are parents of expanded items
|
|
98
|
+
should_show = item in self.expanded_files or item in self.expanded_dirs
|
|
99
|
+
|
|
100
|
+
if should_show:
|
|
101
|
+
shown_items.append(item)
|
|
102
|
+
if item.is_dir():
|
|
103
|
+
_display_recursive(item, indent + 2, depth + 1)
|
|
104
|
+
else:
|
|
105
|
+
writer.write(f"{' ' * (indent + 2)}{item.name}\n")
|
|
106
|
+
|
|
107
|
+
# Show hidden items count if any items were hidden
|
|
108
|
+
hidden_files, hidden_dirs = self._count_hidden_items(
|
|
109
|
+
current_path, shown_items
|
|
110
|
+
)
|
|
111
|
+
if hidden_files > 0 or hidden_dirs > 0:
|
|
112
|
+
hidden_msg = []
|
|
113
|
+
if hidden_dirs > 0:
|
|
114
|
+
hidden_msg.append(
|
|
115
|
+
f"{hidden_dirs} director{'ies' if hidden_dirs != 1 else 'y'}"
|
|
116
|
+
)
|
|
117
|
+
if hidden_files > 0:
|
|
118
|
+
hidden_msg.append(
|
|
119
|
+
f"{hidden_files} file{'s' if hidden_files != 1 else ''}"
|
|
120
|
+
)
|
|
121
|
+
writer.write(
|
|
122
|
+
f"{' ' * (indent + 2)}... {' and '.join(hidden_msg)} hidden\n"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
_display_recursive(self.root, depth=0)
|
|
126
|
+
|
|
127
|
+
return writer.getvalue()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Dict, List, Tuple
|
|
2
|
+
|
|
3
|
+
import tokenizers # type: ignore[import-untyped]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FastPathAnalyzer:
|
|
7
|
+
def __init__(self, model_path: str, vocab_path: str) -> None:
|
|
8
|
+
"""Initialize with vocabulary."""
|
|
9
|
+
# Load vocabulary and probabilities
|
|
10
|
+
self.vocab_probs: Dict[str, float] = {}
|
|
11
|
+
with open(vocab_path, "r") as f:
|
|
12
|
+
for line in f:
|
|
13
|
+
parts = line.strip().split()
|
|
14
|
+
if len(parts) == 2:
|
|
15
|
+
token, prob = parts
|
|
16
|
+
try:
|
|
17
|
+
self.vocab_probs[token] = float(prob)
|
|
18
|
+
except ValueError:
|
|
19
|
+
continue
|
|
20
|
+
|
|
21
|
+
self.encoder = tokenizers.Tokenizer.from_file(model_path)
|
|
22
|
+
|
|
23
|
+
def tokenize_batch(self, texts: List[str]) -> List[List[str]]:
|
|
24
|
+
"""Tokenize multiple texts at once."""
|
|
25
|
+
encodings = self.encoder.encode_batch(texts)
|
|
26
|
+
return [encoding.tokens for encoding in encodings]
|
|
27
|
+
|
|
28
|
+
def detokenize(self, tokens: List[str]) -> str:
|
|
29
|
+
"""Convert tokens back to text, handling special tokens."""
|
|
30
|
+
return self.encoder.decode(tokens) # type: ignore[no-any-return]
|
|
31
|
+
|
|
32
|
+
def calculate_path_probabilities_batch(
|
|
33
|
+
self, paths: List[str]
|
|
34
|
+
) -> List[Tuple[float, List[str], List[str]]]:
|
|
35
|
+
"""Calculate log probability for multiple paths at once."""
|
|
36
|
+
# Batch tokenize all paths
|
|
37
|
+
all_tokens = self.tokenize_batch(paths)
|
|
38
|
+
|
|
39
|
+
results = []
|
|
40
|
+
for tokens in all_tokens:
|
|
41
|
+
# Calculate sum of log probabilities for each path
|
|
42
|
+
log_prob_sum = 0.0
|
|
43
|
+
unknown_tokens = []
|
|
44
|
+
for token in tokens:
|
|
45
|
+
if token in self.vocab_probs:
|
|
46
|
+
log_prob_sum += self.vocab_probs[token]
|
|
47
|
+
else:
|
|
48
|
+
unknown_tokens.append(token)
|
|
49
|
+
|
|
50
|
+
results.append((log_prob_sum, tokens, unknown_tokens))
|
|
51
|
+
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
def calculate_path_probability(
|
|
55
|
+
self, path: str
|
|
56
|
+
) -> Tuple[float, List[str], List[str]]:
|
|
57
|
+
"""Calculate log probability for a single path."""
|
|
58
|
+
return self.calculate_path_probabilities_batch([path])[0]
|