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.

@@ -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
+ )
@@ -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
@@ -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
- if resume == "latest":
122
- resume_path = sorted(Path(".wcgw").iterdir(), key=os.path.getmtime)[-1]
123
- else:
124
- resume_path = Path(resume)
125
- if not resume_path.exists():
126
- raise FileNotFoundError(f"File {resume} not found")
127
- with resume_path.open() as f:
128
- history = json.load(f)
129
- if len(history) <= 2:
130
- raise ValueError("Invalid history file")
131
- first_message = ""
132
- waiting_for_assistant = history[-1]["role"] != "assistant"
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
- System information:
221
- - System: {uname_sysname}
222
- - Machine: {uname_machine}
223
- - Current directory: {os.getcwd()}
247
+ {initial_info}
224
248
 
225
249
  """
226
250