@@ -85,32 +85,30 @@ def _extract_code_block(llm_response: str) -> str:
85
85
return matches [- 1 ]
86
86
87
87
88
- def _merge_content ( original_content : str , edited_content : str , start : int , end : int ) -> str :
89
- """Merge edited content with original content, preserving content outside the edit range .
88
+ def get_llm_edit ( original_file_section : str , edit_content : str ) -> str :
89
+ """Get edited content from LLM .
90
90
91
91
Args:
92
- original_content: Original file content
93
- edited_content: New content for the specified range
94
- start: Start line (1-indexed)
95
- end: End line (1-indexed or -1 for end of file)
92
+ original_file_section: Original content to edit
93
+ edit_content: Edit specification/instructions
96
94
97
95
Returns:
98
- Merged content
96
+ LLM response with edited content
99
97
"""
100
- original_lines = original_content .split ("\n " )
101
- edited_lines = edited_content .split ("\n " )
102
-
103
- if start == - 1 and end == - 1 : # Append mode
104
- return original_content + "\n " + edited_content
98
+ system_message = COMMANDER_SYSTEM_PROMPT
99
+ human_message = _HUMAN_PROMPT_DRAFT_EDITOR
100
+ prompt = ChatPromptTemplate .from_messages ([system_message , human_message ])
105
101
106
- # Convert to 0-indexed
107
- start_idx = start - 1
108
- end_idx = end - 1 if end != - 1 else len (original_lines )
102
+ llm = ChatAnthropic (
103
+ model = "claude-3-5-sonnet-latest" ,
104
+ temperature = 0 ,
105
+ max_tokens = 5000 ,
106
+ )
109
107
110
- # Merge the content
111
- result_lines = original_lines [: start_idx ] + edited_lines + original_lines [ end_idx + 1 :]
108
+ chain = prompt | llm
109
+ response = chain . invoke ({ "original_file_section" : original_file_section , "edit_content" : edit_content })
112
110
113
- return " \n " . join ( result_lines )
111
+ return response . content
114
112
115
113
116
114
def _validate_edit_boundaries (original_lines : list [str ], modified_lines : list [str ], start_idx : int , end_idx : int ) -> None :
@@ -126,14 +124,108 @@ def _validate_edit_boundaries(original_lines: list[str], modified_lines: list[st
126
124
ValueError: If changes were made outside the specified range
127
125
"""
128
126
# Check lines before start_idx
129
- for i in range (start_idx ):
130
- if i >= len ( original_lines ) or i >= len ( modified_lines ) or original_lines [i ] != modified_lines [i ]:
127
+ for i in range (min ( start_idx , len ( original_lines ), len ( modified_lines )) ):
128
+ if original_lines [i ] != modified_lines [i ]:
131
129
msg = f"Edit modified line { i + 1 } which is before the specified start line { start_idx + 1 } "
132
130
raise ValueError (msg )
133
131
132
+ # Check lines after end_idx
133
+ remaining_lines = len (original_lines ) - (end_idx + 1 )
134
+ if remaining_lines > 0 :
135
+ orig_suffix = original_lines [- remaining_lines :]
136
+ if len (modified_lines ) >= remaining_lines :
137
+ mod_suffix = modified_lines [- remaining_lines :]
138
+ if orig_suffix != mod_suffix :
139
+ msg = f"Edit modified content after the specified end line { end_idx + 1 } "
140
+ raise ValueError (msg )
141
+
142
+
143
+ def extract_file_window (file_content : str , start : int = 1 , end : int = - 1 ) -> tuple [str , int , int ]:
144
+ """Extract a window of content from a file.
145
+
146
+ Args:
147
+ file_content: Content of the file
148
+ start: Start line (1-indexed, default: 1)
149
+ end: End line (1-indexed or -1 for end of file, default: -1)
150
+
151
+ Returns:
152
+ Tuple of (extracted_content, start_idx, end_idx)
153
+ """
154
+ # Split into lines and handle line numbers
155
+ lines = file_content .split ("\n " )
156
+ total_lines = len (lines )
157
+
158
+ # Convert to 0-indexed
159
+ start_idx = start - 1
160
+ end_idx = end - 1 if end != - 1 else total_lines - 1
161
+
162
+ # Get the content window
163
+ window_lines = lines [start_idx : end_idx + 1 ]
164
+ window_content = "\n " .join (window_lines )
165
+
166
+ return window_content , start_idx , end_idx
167
+
168
+
169
+ def apply_semantic_edit (codebase : Codebase , filepath : str , edited_content : str , start : int = 1 , end : int = - 1 ) -> tuple [str , str ]:
170
+ """Apply a semantic edit to a section of content.
171
+
172
+ Args:
173
+ codebase: Codebase object
174
+ filepath: Path to the file to edit
175
+ edited_content: New content for the specified range
176
+ start: Start line (1-indexed, default: 1)
177
+ end: End line (1-indexed or -1 for end of file, default: -1)
178
+
179
+ Returns:
180
+ Tuple of (new_content, diff)
181
+ """
182
+ # Get the original content
183
+ file = codebase .get_file (filepath )
184
+ original_content = file .content
185
+
186
+ # Handle append mode
187
+ if start == - 1 and end == - 1 :
188
+ new_content = original_content + "\n " + edited_content
189
+ diff = generate_diff (original_content , new_content )
190
+ file .edit (new_content )
191
+ codebase .commit ()
192
+ return new_content , diff
193
+
194
+ # Split content into lines
195
+ original_lines = original_content .splitlines ()
196
+ edited_lines = edited_content .splitlines ()
197
+
198
+ # Convert to 0-indexed
199
+ start_idx = start - 1
200
+ end_idx = end - 1 if end != - 1 else len (original_lines ) - 1
201
+
202
+ # Splice together: prefix + edited content + suffix
203
+ new_lines = (
204
+ original_lines [:start_idx ] # Prefix
205
+ + edited_lines # Edited section
206
+ + original_lines [end_idx + 1 :] # Suffix
207
+ )
208
+
209
+ # Preserve original file's newline if it had one
210
+ new_content = "\n " .join (new_lines ) + ("\n " if original_content .endswith ("\n " ) else "" )
211
+ # Validate the edit boundaries
212
+ _validate_edit_boundaries (original_lines , new_lines , start_idx , end_idx )
213
+
214
+ # Apply the edit
215
+ file .edit (new_content )
216
+ codebase .commit ()
217
+ with open (file .path , "w" ) as f :
218
+ f .write (new_content )
219
+
220
+ # Generate diff from the original section to the edited section
221
+ original_section , _ , _ = extract_file_window (original_content , start , end )
222
+ diff = generate_diff (original_section , edited_content )
223
+
224
+ return new_content , diff
225
+
134
226
135
227
def semantic_edit (codebase : Codebase , filepath : str , edit_content : str , start : int = 1 , end : int = - 1 ) -> SemanticEditObservation :
136
- """Edit a file using semantic editing with line range support. This is an internal api and should not be called by the LLM. """
228
+ """Edit a file using semantic editing with line range support."""
137
229
try :
138
230
file = codebase .get_file (filepath )
139
231
except ValueError :
@@ -158,81 +250,29 @@ def semantic_edit(codebase: Codebase, filepath: str, edit_content: str, start: i
158
250
line_count = len (original_lines ),
159
251
)
160
252
161
- # Handle append mode
162
- if start == - 1 and end == - 1 :
163
- try :
164
- file .add_symbol_from_source (edit_content )
165
- codebase .commit ()
166
-
167
- return SemanticEditObservation (
168
- status = "success" ,
169
- filepath = filepath ,
170
- new_content = file .content ,
171
- diff = generate_diff (original_content , file .content ),
172
- )
173
- except Exception as e :
174
- msg = f"Failed to append content: { e !s} "
175
- raise ValueError (msg )
176
-
177
- # For range edits, get the context for the draft editor
178
- total_lines = len (original_lines )
179
- start_idx = start - 1
180
- end_idx = end - 1 if end != - 1 else total_lines
181
-
182
- # Get the context for the edit
183
- context_lines = original_lines [start_idx : end_idx + 1 ]
184
- original_file_section = "\n " .join (context_lines )
253
+ # Extract the window of content to edit
254
+ original_file_section , start_idx , end_idx = extract_file_window (original_content , start , end )
185
255
186
- # =====[ Get the LLM ]=====
187
- system_message = COMMANDER_SYSTEM_PROMPT
188
- human_message = _HUMAN_PROMPT_DRAFT_EDITOR
189
- prompt = ChatPromptTemplate .from_messages ([system_message , human_message ])
190
- llm = ChatAnthropic (
191
- model = "claude-3-5-sonnet-latest" ,
192
- temperature = 0 ,
193
- max_tokens = 5000 ,
194
- )
195
- chain = prompt | llm
196
- response = chain .invoke ({"original_file_section" : original_file_section , "edit_content" : edit_content })
197
-
198
- # Extract code from markdown code block
256
+ # Get edited content from LLM
199
257
try :
200
- modified_segment = _extract_code_block (response . content )
258
+ modified_segment = _extract_code_block (get_llm_edit ( original_file_section , edit_content ) )
201
259
except ValueError as e :
202
260
return SemanticEditObservation (
203
261
status = "error" ,
204
262
error = f"Failed to parse LLM response: { e !s} " ,
205
263
filepath = filepath ,
206
264
)
207
265
208
- # Merge the edited content with the original
209
- new_content = _merge_content (original_content , modified_segment , start , end )
210
- new_lines = new_content .splitlines ()
211
-
212
- # Validate that no changes were made before the start line
266
+ # Apply the semantic edit
213
267
try :
214
- _validate_edit_boundaries ( original_lines , new_lines , start_idx , end_idx )
268
+ new_content , diff = apply_semantic_edit ( codebase , filepath , modified_segment , start , end )
215
269
except ValueError as e :
216
270
return SemanticEditObservation (
217
271
status = "error" ,
218
272
error = str (e ),
219
273
filepath = filepath ,
220
274
)
221
275
222
- # Generate diff
223
- diff = generate_diff (original_content , new_content )
224
-
225
- # Apply the edit
226
- try :
227
- file .edit (new_content )
228
- codebase .commit ()
229
- except Exception as e :
230
- return SemanticEditObservation (
231
- status = "error" ,
232
- error = f"Failed to apply edit: { e !s} " ,
233
- filepath = filepath ,
234
- )
235
-
236
276
return SemanticEditObservation (
237
277
status = "success" ,
238
278
filepath = filepath ,
0 commit comments