Skip to content

Commit c65a97b

Browse files
authored
Better line-ending handling and better prompts to direct the llm to the right path immediately (#1079)
1 parent 8f8c148 commit c65a97b

File tree

5 files changed

+72
-48
lines changed

5 files changed

+72
-48
lines changed

patchwork/common/tools/bash_tool.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

99
class BashTool(Tool, tool_name="bash"):
10-
def __init__(self, path: str):
10+
def __init__(self, path: Path):
1111
super().__init__()
1212
self.path = Path(path)
1313
self.modified_files = []
@@ -16,15 +16,16 @@ def __init__(self, path: str):
1616
def json_schema(self) -> dict:
1717
return {
1818
"name": "bash",
19-
"description": """Run commands in a bash shell
19+
"description": f"""Run commands in a bash shell
2020
2121
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
2222
* You don't have access to the internet via this tool.
2323
* You do have access to a mirror of common linux and python packages via apt and pip.
2424
* State is persistent across command calls and discussions with the user.
2525
* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
2626
* Please avoid commands that may produce a very large amount of output.
27-
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.""",
27+
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
28+
* The working directory is always {self.path}""",
2829
"input_schema": {
2930
"type": "object",
3031
"properties": {"command": {"type": "string", "description": "The bash command to run."}},

patchwork/common/tools/code_edit_tools.py

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,28 @@
55
from typing import Literal
66

77
from patchwork.common.tools.tool import Tool
8+
from patchwork.common.utils.utils import detect_newline
89

910

1011
class CodeEditTool(Tool, tool_name="code_edit_tool"):
11-
def __init__(self, path: str):
12+
def __init__(self, path: Path):
1213
super().__init__()
13-
self.repo_path = Path(path)
14+
self.repo_path = path
1415
self.modified_files = set()
1516

1617
@property
1718
def json_schema(self) -> dict:
1819
return {
1920
"name": "code_edit_tool",
20-
"description": """Custom editing tool for viewing, creating and editing files
21+
"description": f"""\
22+
Custom editing tool for viewing, creating and editing files
2123
2224
* State is persistent across command calls and discussions with the user
2325
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
2426
* The `create` command cannot be used if the specified `path` already exists as a file
2527
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
2628
* The `undo_edit` command will revert the last edit made to the file at `path`
29+
* The working directory is always {self.repo_path}
2730
2831
Notes for using the `str_replace` command:
2932
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
@@ -86,40 +89,39 @@ def execute(
8689
return f"Error: `{'` and `'.join(missing_required)}` parameters must be set and cannot be empty"
8790

8891
try:
92+
abs_path = self.__get_abs_path(path)
8993
if command == "view":
90-
result = self.__view(path, view_range)
94+
result = self.__view(abs_path, view_range)
9195
elif command == "create":
92-
result = self.__create(file_text, path)
96+
result = self.__create(file_text, abs_path)
9397
elif command == "str_replace":
94-
result = self.__str_replace(new_str, old_str, path)
98+
result = self.__str_replace(new_str, old_str, abs_path)
9599
elif command == "insert":
96-
result = self.__insert(insert_line, new_str, path)
100+
result = self.__insert(insert_line, new_str, abs_path)
97101
else:
98102
return f"Error: Unknown action {command}"
99-
100-
if command in {"create", "str_replace", "insert"}:
101-
self.modified_files.update({path.lstrip("/")})
102-
103-
return result
104-
105103
except Exception as e:
106104
return f"Error: {str(e)}"
107105

106+
if command in {"create", "str_replace", "insert"}:
107+
self.modified_files.update({abs_path.relative_to(self.repo_path)})
108+
109+
return result
110+
108111
@property
109112
def tool_records(self):
110-
return dict(modified_files=[{"path": file} for file in self.modified_files])
113+
return dict(modified_files=[{"path": str(file)} for file in self.modified_files])
111114

112115
def __get_abs_path(self, path: str):
113-
abs_path = (self.repo_path / path.lstrip("/")).resolve()
114-
if not abs_path.is_relative_to(self.repo_path.resolve()):
116+
wanted_path = Path(path).resolve()
117+
if wanted_path.is_relative_to(self.repo_path):
118+
return wanted_path
119+
else:
115120
raise ValueError(f"Path {path} contains illegal path traversal")
116121

117-
return abs_path
118-
119-
def __view(self, path, view_range):
120-
abs_path = self.__get_abs_path(path)
122+
def __view(self, abs_path: Path, view_range):
121123
if not abs_path.exists():
122-
return f"Error: Path {path} does not exist"
124+
return f"Error: Path {abs_path} does not exist"
123125

124126
if abs_path.is_file():
125127
with open(abs_path, "r") as f:
@@ -141,38 +143,38 @@ def __view(self, path, view_range):
141143
result.append(f)
142144
return "\n".join(result)
143145

144-
def __create(self, file_text, path):
145-
abs_path = self.__get_abs_path(path)
146+
def __create(self, file_text, abs_path):
146147
if abs_path.exists():
147-
return f"Error: File {path} already exists"
148+
return f"Error: File {abs_path} already exists"
148149
abs_path.parent.mkdir(parents=True, exist_ok=True)
149150
abs_path.write_text(file_text)
150-
return f"File created successfully at: {path}"
151+
return f"File created successfully at: {abs_path}"
151152

152-
def __str_replace(self, new_str, old_str, path):
153-
abs_path = self.__get_abs_path(path)
153+
def __str_replace(self, new_str, old_str, abs_path):
154154
if not abs_path.exists():
155-
return f"Error: File {path} does not exist"
155+
return f"Error: File {abs_path} does not exist"
156156
if not abs_path.is_file():
157-
return f"Error: File {path} is not a file"
157+
return f"Error: File {abs_path} is not a file"
158158
content = abs_path.read_text()
159159
occurrences = content.count(old_str)
160160
if occurrences != 1:
161161
return f"Error: Found {occurrences} occurrences of old_str, expected exactly 1"
162162
new_content = content.replace(old_str, new_str)
163-
with open(abs_path, "w") as f:
164-
f.write(new_content)
163+
newline = detect_newline(abs_path)
164+
with abs_path.open("w", newline=newline) as fp:
165+
fp.write(new_content)
165166
return "Replacement successful"
166167

167-
def __insert(self, insert_line, new_str, path):
168-
abs_path = self.__get_abs_path(path)
168+
def __insert(self, insert_line, new_str, abs_path):
169169
if not abs_path.is_file():
170-
return f"Error: File {path} does not exist or is not a file"
170+
return f"Error: File {abs_path} does not exist or is not a file"
171171

172172
lines = abs_path.read_text().splitlines(keepends=True)
173173
if insert_line is None or insert_line < 1 or insert_line > len(lines):
174174
return f"Error: Invalid insert line {insert_line}"
175175

176176
lines.insert(insert_line, new_str + "\n")
177-
abs_path.write_text("".join(lines))
177+
newline = detect_newline(abs_path)
178+
with abs_path.open("w", newline=newline) as fp:
179+
fp.write("".join(lines))
178180
return "Insert successful"

patchwork/common/utils/utils.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,31 @@
99
import tiktoken
1010
from chardet.universaldetector import UniversalDetector
1111
from git import Head, Repo
12-
from typing_extensions import Any, Callable
12+
from typing_extensions import Any, Callable, Iterable, Counter
1313

1414
from patchwork.common.utils.dependency import chromadb
1515
from patchwork.logger import logger
1616
from patchwork.managed_files import HOME_FOLDER
1717

1818
_CLEANUP_FILES: set[Path] = set()
19+
_NEWLINES = {"\n", "\r\n", "\r"}
20+
21+
def detect_newline(path: str | Path) -> str | None:
22+
with open(path, "r", newline="") as f:
23+
lines = f.read().splitlines(keepends=True)
24+
if len(lines) < 1:
25+
return None
26+
27+
counter = Counter(_NEWLINES)
28+
for line in lines:
29+
newline_len = 0
30+
newline = "\n"
31+
for possible_newline in _NEWLINES:
32+
if line.endswith(possible_newline) and len(possible_newline) > newline_len:
33+
newline_len = len(possible_newline)
34+
newline = possible_newline
35+
counter[newline] += 1
36+
return counter.most_common(1)[0][0]
1937

2038

2139
def _cleanup_files():

patchwork/steps/FixIssue/FixIssue.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,21 @@
2020

2121
class _ResolveIssue(AnalyzeImplementStrategy):
2222
def __init__(self, repo_path: str, llm_client: LlmClient, issue_description: Any, **kwargs):
23-
self.tool_set = Tool.get_tools(path=repo_path)
23+
path = Path(repo_path).resolve()
24+
self.tool_set = Tool.get_tools(path=path)
2425
super().__init__(
2526
llm_client=llm_client,
2627
initial_template_data=dict(issue=issue_description),
27-
analysis_prompt_template="""<uploaded_files>
28-
.
28+
analysis_prompt_template=f"""\
29+
<uploaded_files>
30+
{path}
2931
</uploaded_files>
30-
I've uploaded a code repository in the current working directory (not in /tmp/inputs).
32+
I've uploaded a code repository in the current working directory.
3133
3234
Consider the following issue:
3335
3436
<issue_description>
35-
{{issue}}
37+
{{{{issue}}}}
3638
</issue_description>
3739
3840
Let's first explore and analyze the repository to understand where the issue is located.
@@ -49,15 +51,16 @@ def __init__(self, repo_path: str, llm_client: LlmClient, issue_description: Any
4951
<error_reproduction>The error reproduction script and its output</error_reproduction>
5052
<changes_needed>Description of the specific changes needed</changes_needed>
5153
</analysis>""",
52-
implementation_prompt_template="""<uploaded_files>
53-
.
54+
implementation_prompt_template=f"""\
55+
<uploaded_files>
56+
{path}
5457
</uploaded_files>
5558
I've uploaded a code repository in the current working directory (not in /tmp/inputs).
5659
5760
Based on our previous analysis:
5861
5962
<previous_analysis>
60-
{{analysis_results}}
63+
{{{{analysis_results}}}}
6164
</previous_analysis>
6265
6366
Let's implement the necessary changes:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "patchwork-cli"
3-
version = "0.0.81"
3+
version = "0.0.82"
44
description = ""
55
authors = ["patched.codes"]
66
license = "AGPL"

0 commit comments

Comments
 (0)