wcgw 2.6.2__py3-none-any.whl → 2.7.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/client/anthropic_client.py +64 -51
- wcgw/client/diff-instructions.txt +0 -1
- wcgw/client/file_ops/diff_edit.py +482 -0
- wcgw/client/file_ops/search_replace.py +119 -0
- wcgw/client/mcp_server/server.py +19 -0
- wcgw/client/memory.py +52 -0
- wcgw/client/openai_client.py +42 -18
- wcgw/client/tools.py +76 -172
- wcgw/relay/serve.py +41 -12
- wcgw/types_.py +12 -0
- {wcgw-2.6.2.dist-info → wcgw-2.7.0.dist-info}/METADATA +1 -1
- {wcgw-2.6.2.dist-info → wcgw-2.7.0.dist-info}/RECORD +15 -12
- {wcgw-2.6.2.dist-info → wcgw-2.7.0.dist-info}/WHEEL +0 -0
- {wcgw-2.6.2.dist-info → wcgw-2.7.0.dist-info}/entry_points.txt +0 -0
- {wcgw-2.6.2.dist-info → wcgw-2.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from .diff_edit import FileEditInput, FileEditOutput
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def search_replace_edit(
|
|
8
|
+
lines: list[str], original_content: str, logger: Callable[[str], object]
|
|
9
|
+
) -> tuple[str, str]:
|
|
10
|
+
if not lines:
|
|
11
|
+
raise Exception("Error: No input to search replace edit")
|
|
12
|
+
original_lines = original_content.split("\n")
|
|
13
|
+
n_lines = len(lines)
|
|
14
|
+
i = 0
|
|
15
|
+
search_replace_blocks = list[tuple[list[str], list[str]]]()
|
|
16
|
+
while i < n_lines:
|
|
17
|
+
if re.match(r"^<<<<<<+\s*SEARCH\s*$", lines[i]):
|
|
18
|
+
search_block = []
|
|
19
|
+
i += 1
|
|
20
|
+
while i < n_lines and not re.match(r"^======*\s*$", lines[i]):
|
|
21
|
+
search_block.append(lines[i])
|
|
22
|
+
i += 1
|
|
23
|
+
i += 1
|
|
24
|
+
if not search_block:
|
|
25
|
+
raise Exception("SEARCH block can not be empty")
|
|
26
|
+
replace_block = []
|
|
27
|
+
while i < n_lines and not re.match(r"^>>>>>>+\s*REPLACE\s*$", lines[i]):
|
|
28
|
+
replace_block.append(lines[i])
|
|
29
|
+
i += 1
|
|
30
|
+
i += 1
|
|
31
|
+
|
|
32
|
+
for line in search_block:
|
|
33
|
+
logger("> " + line)
|
|
34
|
+
logger("=======")
|
|
35
|
+
for line in replace_block:
|
|
36
|
+
logger("< " + line)
|
|
37
|
+
logger("\n\n\n\n")
|
|
38
|
+
|
|
39
|
+
search_replace_blocks.append((search_block, replace_block))
|
|
40
|
+
else:
|
|
41
|
+
i += 1
|
|
42
|
+
|
|
43
|
+
if not search_replace_blocks:
|
|
44
|
+
raise Exception(
|
|
45
|
+
"No valid search replace blocks found, ensure your SEARCH/REPLACE blocks are formatted correctly"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
edited_content, comments_ = greedy_context_replace(
|
|
49
|
+
original_lines, [[x] for x in search_replace_blocks], original_lines, set(), 0
|
|
50
|
+
)
|
|
51
|
+
edited_file = "\n".join(edited_content)
|
|
52
|
+
if not comments_:
|
|
53
|
+
comments = "Edited successfully"
|
|
54
|
+
else:
|
|
55
|
+
comments = (
|
|
56
|
+
"Edited successfully. However, following warnings were generated while matching search blocks.\n"
|
|
57
|
+
+ "\n".join(comments_)
|
|
58
|
+
)
|
|
59
|
+
return edited_file, comments
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def greedy_context_replace(
|
|
63
|
+
original_lines: list[str],
|
|
64
|
+
search_replace_blocks: list[list[tuple[list[str], list[str]]]],
|
|
65
|
+
running_lines: list[str],
|
|
66
|
+
running_comments: set[str],
|
|
67
|
+
current_block_offset: int,
|
|
68
|
+
) -> tuple[list[str], set[str]]:
|
|
69
|
+
if current_block_offset >= len(search_replace_blocks):
|
|
70
|
+
return running_lines, running_comments
|
|
71
|
+
current_blocks = search_replace_blocks[current_block_offset]
|
|
72
|
+
|
|
73
|
+
outputs = FileEditInput(running_lines, 0, current_blocks, 0).edit_file()
|
|
74
|
+
best_matches, is_error = FileEditOutput.get_best_match(outputs)
|
|
75
|
+
|
|
76
|
+
if is_error:
|
|
77
|
+
best_matches[0].replace_or_throw(3)
|
|
78
|
+
raise Exception("Shouldn't happen")
|
|
79
|
+
|
|
80
|
+
if len(best_matches) > 1:
|
|
81
|
+
# Duplicate found, try to ground using previous blocks.
|
|
82
|
+
if current_block_offset == 0:
|
|
83
|
+
raise Exception(f"""
|
|
84
|
+
The following block matched more than once:
|
|
85
|
+
---
|
|
86
|
+
```
|
|
87
|
+
{'\n'.join(current_blocks[-1][0])}
|
|
88
|
+
```
|
|
89
|
+
""")
|
|
90
|
+
|
|
91
|
+
else:
|
|
92
|
+
search_replace_blocks = (
|
|
93
|
+
search_replace_blocks[: current_block_offset - 1]
|
|
94
|
+
+ [search_replace_blocks[current_block_offset - 1] + current_blocks]
|
|
95
|
+
+ search_replace_blocks[current_block_offset + 1 :]
|
|
96
|
+
)
|
|
97
|
+
try:
|
|
98
|
+
return greedy_context_replace(
|
|
99
|
+
original_lines, search_replace_blocks, original_lines, set(), 0
|
|
100
|
+
)
|
|
101
|
+
except Exception:
|
|
102
|
+
raise Exception(f"""
|
|
103
|
+
The following block matched more than once:
|
|
104
|
+
---
|
|
105
|
+
```
|
|
106
|
+
{'\n'.join(current_blocks[-1][0])}
|
|
107
|
+
```
|
|
108
|
+
""")
|
|
109
|
+
|
|
110
|
+
best_match = best_matches[0]
|
|
111
|
+
running_lines, comments = best_match.replace_or_throw(3)
|
|
112
|
+
running_comments = running_comments | comments
|
|
113
|
+
return greedy_context_replace(
|
|
114
|
+
original_lines,
|
|
115
|
+
search_replace_blocks,
|
|
116
|
+
running_lines,
|
|
117
|
+
running_comments,
|
|
118
|
+
current_block_offset + 1,
|
|
119
|
+
)
|
wcgw/client/mcp_server/server.py
CHANGED
|
@@ -82,6 +82,7 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
82
82
|
- If the user has mentioned a folder or file with unclear project root, use the file or folder as `any_workspace_path`.
|
|
83
83
|
- If user has mentioned any files use `initial_files_to_read` to read, use absolute paths only.
|
|
84
84
|
- If `any_workspace_path` is provided, a tree structure of the workspace will be shown.
|
|
85
|
+
- Leave `any_workspace_path` as empty if no file or folder is mentioned.
|
|
85
86
|
""",
|
|
86
87
|
),
|
|
87
88
|
ToolParam(
|
|
@@ -152,6 +153,24 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
152
153
|
"""
|
|
153
154
|
+ diffinstructions,
|
|
154
155
|
),
|
|
156
|
+
# ToolParam(
|
|
157
|
+
# inputSchema=KnowledgeTransfer.model_json_schema(),
|
|
158
|
+
# name="KnowledgeTransfer",
|
|
159
|
+
# description="""
|
|
160
|
+
# Write detailed description in order to do a KT, if the user asks for it.
|
|
161
|
+
# Save all information necessary for a person to understand the task and the problems.
|
|
162
|
+
# - `all_user_instructions` should contain all instructions user shared in the conversation.
|
|
163
|
+
# - `current_status_of_the_task` should contain only what is already achieved, not what's remaining.
|
|
164
|
+
# - `all_issues_snippets` should only contain snippets of error, traceback, file snippets, commands, etc., no comments or solutions (important!).
|
|
165
|
+
# - Be very verbose in `all_issues_snippets` providing as much error context as possible.
|
|
166
|
+
# - Provide an id if the user hasn't provided one.
|
|
167
|
+
# - This tool will return a text file path where the information is saved.
|
|
168
|
+
# - After the tool completes succesfully, tell the user the task id and the generate file path. (important!)
|
|
169
|
+
# - Leave arguments as empty string if they aren't relevant.
|
|
170
|
+
# - This tool marks end of your conversation, do not run any further tools after calling this.
|
|
171
|
+
# - Provide absolute file paths only in `relevant_file_paths` containing all relevant files.
|
|
172
|
+
# """,
|
|
173
|
+
# ),
|
|
155
174
|
]
|
|
156
175
|
if COMPUTER_USE_ON_DOCKER_ENABLED:
|
|
157
176
|
tools += [
|
wcgw/client/memory.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from ..types_ import KnowledgeTransfer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_app_dir_xdg() -> str:
|
|
7
|
+
xdg_data_dir = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
|
|
8
|
+
return os.path.join(xdg_data_dir, "wcgw")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_memory(task_memory: KnowledgeTransfer, relevant_files: str) -> str:
|
|
12
|
+
memory_data = f"""# Goal: {task_memory.objective}\n\n
|
|
13
|
+
# Instructions:\n{task_memory.all_user_instructions}\n\n
|
|
14
|
+
# Current Status:\n{task_memory.current_status_of_the_task}\n\n
|
|
15
|
+
# Pending Issues:\n{task_memory.all_issues_snippets}\n\n
|
|
16
|
+
# Build Instructions:\n{task_memory.build_and_development_instructions}\n"""
|
|
17
|
+
|
|
18
|
+
memory_data += "\n# Relevant Files:\n" + relevant_files
|
|
19
|
+
|
|
20
|
+
return memory_data
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def save_memory(task_memory: KnowledgeTransfer, relevant_files: str) -> str:
|
|
24
|
+
app_dir = get_app_dir_xdg()
|
|
25
|
+
memory_dir = os.path.join(app_dir, "memory")
|
|
26
|
+
os.makedirs(memory_dir, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
task_id = task_memory.id
|
|
29
|
+
if not task_id:
|
|
30
|
+
raise Exception("Task id can not be empty")
|
|
31
|
+
memory_data = format_memory(task_memory, relevant_files)
|
|
32
|
+
|
|
33
|
+
memory_file = os.path.join(memory_dir, f"{task_id}.json")
|
|
34
|
+
memory_file_full = os.path.join(memory_dir, f"{task_id}.txt")
|
|
35
|
+
|
|
36
|
+
with open(memory_file_full, "w") as f:
|
|
37
|
+
f.write(memory_data)
|
|
38
|
+
|
|
39
|
+
with open(memory_file, "w") as f:
|
|
40
|
+
f.write(task_memory.model_dump_json())
|
|
41
|
+
|
|
42
|
+
return memory_file_full
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_memory(task_id: str) -> KnowledgeTransfer:
|
|
46
|
+
app_dir = get_app_dir_xdg()
|
|
47
|
+
memory_dir = os.path.join(app_dir, "memory")
|
|
48
|
+
memory_file = os.path.join(memory_dir, f"{task_id}.json")
|
|
49
|
+
|
|
50
|
+
with open(memory_file, "r") as f:
|
|
51
|
+
task_save = KnowledgeTransfer.model_validate_json(f.read())
|
|
52
|
+
return task_save
|
wcgw/client/openai_client.py
CHANGED
|
@@ -27,17 +27,20 @@ from ..types_ import (
|
|
|
27
27
|
BashCommand,
|
|
28
28
|
BashInteraction,
|
|
29
29
|
FileEdit,
|
|
30
|
+
KnowledgeTransfer,
|
|
30
31
|
ReadFiles,
|
|
31
32
|
ReadImage,
|
|
32
33
|
ResetShell,
|
|
33
34
|
WriteIfEmpty,
|
|
34
35
|
)
|
|
35
36
|
from .common import CostData, History, Models, discard_input
|
|
37
|
+
from .memory import load_memory
|
|
36
38
|
from .openai_utils import get_input_cost, get_output_cost
|
|
37
39
|
from .tools import (
|
|
38
40
|
DoneFlag,
|
|
39
41
|
ImageData,
|
|
40
42
|
get_tool_output,
|
|
43
|
+
initialize,
|
|
41
44
|
which_tool,
|
|
42
45
|
)
|
|
43
46
|
|
|
@@ -117,19 +120,24 @@ def loop(
|
|
|
117
120
|
|
|
118
121
|
history: History = []
|
|
119
122
|
waiting_for_assistant = False
|
|
123
|
+
|
|
124
|
+
memory = None
|
|
120
125
|
if resume:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
try:
|
|
127
|
+
memory = load_memory(resume)
|
|
128
|
+
except OSError:
|
|
129
|
+
if resume == "latest":
|
|
130
|
+
resume_path = sorted(Path(".wcgw").iterdir(), key=os.path.getmtime)[-1]
|
|
131
|
+
else:
|
|
132
|
+
resume_path = Path(resume)
|
|
133
|
+
if not resume_path.exists():
|
|
134
|
+
raise FileNotFoundError(f"File {resume} not found")
|
|
135
|
+
with resume_path.open() as f:
|
|
136
|
+
history = json.load(f)
|
|
137
|
+
if len(history) <= 2:
|
|
138
|
+
raise ValueError("Invalid history file")
|
|
139
|
+
first_message = ""
|
|
140
|
+
waiting_for_assistant = history[-1]["role"] != "assistant"
|
|
133
141
|
|
|
134
142
|
my_dir = os.path.dirname(__file__)
|
|
135
143
|
|
|
@@ -202,10 +210,29 @@ def loop(
|
|
|
202
210
|
ResetShell,
|
|
203
211
|
description="Resets the shell. Use only if all interrupts and prompt reset attempts have failed repeatedly.",
|
|
204
212
|
),
|
|
213
|
+
openai.pydantic_function_tool(
|
|
214
|
+
KnowledgeTransfer,
|
|
215
|
+
description="""
|
|
216
|
+
Write detailed description in order to do a KT, if the user asks for it.
|
|
217
|
+
Save all information necessary for a person to understand the task and the problems.
|
|
218
|
+
|
|
219
|
+
- `all_user_instructions` should contain all instructions user shared in the conversation.
|
|
220
|
+
- `current_status_of_the_task` should contain only what is already achieved, not what's remaining.
|
|
221
|
+
- `all_issues_snippets` should only contain snippets of error, traceback, file snippets, commands, etc., no comments or solutions (important!).
|
|
222
|
+
- Be very verbose in `all_issues_snippets` providing as much error context as possible.
|
|
223
|
+
- Provide an id if the user hasn't provided one.
|
|
224
|
+
- This tool will return a text file path where the information is saved.
|
|
225
|
+
- After the tool completes succesfully, tell the user the task id and the generate file path. (important!)
|
|
226
|
+
- Leave arguments as empty string if they aren't relevant.
|
|
227
|
+
- This tool marks end of your conversation, do not run any further tools after calling this.
|
|
228
|
+
- Provide absolute file paths only in `relevant_file_paths` containing all relevant files.
|
|
229
|
+
""",
|
|
230
|
+
),
|
|
205
231
|
]
|
|
206
|
-
uname_sysname = os.uname().sysname
|
|
207
|
-
uname_machine = os.uname().machine
|
|
208
232
|
|
|
233
|
+
initial_info = initialize(
|
|
234
|
+
os.getcwd(), [], resume if (memory and resume) else "", 8000
|
|
235
|
+
)
|
|
209
236
|
system = f"""
|
|
210
237
|
You're an expert software engineer with shell and code knowledge.
|
|
211
238
|
|
|
@@ -217,10 +244,7 @@ Instructions:
|
|
|
217
244
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
218
245
|
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
219
246
|
|
|
220
|
-
|
|
221
|
-
- System: {uname_sysname}
|
|
222
|
-
- Machine: {uname_machine}
|
|
223
|
-
- Current directory: {os.getcwd()}
|
|
247
|
+
{initial_info}
|
|
224
248
|
|
|
225
249
|
"""
|
|
226
250
|
|