auto-coder 0.1.316__py3-none-any.whl → 0.1.317__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 auto-coder might be problematic. Click here for more details.

Files changed (41) hide show
  1. {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/RECORD +41 -20
  3. autocoder/auto_coder_runner.py +1 -2
  4. autocoder/common/__init__.py +3 -0
  5. autocoder/common/auto_coder_lang.py +24 -0
  6. autocoder/common/code_auto_merge_editblock.py +2 -42
  7. autocoder/common/git_utils.py +2 -2
  8. autocoder/common/token_cost_caculate.py +103 -42
  9. autocoder/common/v2/__init__.py +0 -0
  10. autocoder/common/v2/code_auto_generate.py +199 -0
  11. autocoder/common/v2/code_auto_generate_diff.py +361 -0
  12. autocoder/common/v2/code_auto_generate_editblock.py +380 -0
  13. autocoder/common/v2/code_auto_generate_strict_diff.py +269 -0
  14. autocoder/common/v2/code_auto_merge.py +211 -0
  15. autocoder/common/v2/code_auto_merge_diff.py +354 -0
  16. autocoder/common/v2/code_auto_merge_editblock.py +523 -0
  17. autocoder/common/v2/code_auto_merge_strict_diff.py +259 -0
  18. autocoder/common/v2/code_diff_manager.py +266 -0
  19. autocoder/common/v2/code_editblock_manager.py +275 -0
  20. autocoder/common/v2/code_manager.py +238 -0
  21. autocoder/common/v2/code_strict_diff_manager.py +241 -0
  22. autocoder/dispacher/actions/action.py +16 -0
  23. autocoder/dispacher/actions/plugins/action_regex_project.py +6 -0
  24. autocoder/events/event_manager_singleton.py +2 -2
  25. autocoder/helper/__init__.py +0 -0
  26. autocoder/helper/project_creator.py +570 -0
  27. autocoder/linters/linter_factory.py +44 -25
  28. autocoder/linters/models.py +220 -0
  29. autocoder/linters/python_linter.py +1 -7
  30. autocoder/linters/reactjs_linter.py +580 -0
  31. autocoder/linters/shadow_linter.py +390 -0
  32. autocoder/linters/vue_linter.py +576 -0
  33. autocoder/memory/active_context_manager.py +0 -4
  34. autocoder/memory/active_package.py +12 -12
  35. autocoder/shadows/__init__.py +0 -0
  36. autocoder/shadows/shadow_manager.py +235 -0
  37. autocoder/version.py +1 -1
  38. {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/LICENSE +0 -0
  39. {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/WHEEL +0 -0
  40. {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/entry_points.txt +0 -0
  41. {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,523 @@
1
+ import os
2
+ from byzerllm.utils.client import code_utils
3
+ from autocoder.common import AutoCoderArgs, git_utils, SourceCodeList, SourceCode
4
+ from autocoder.common.action_yml_file_manager import ActionYmlFileManager
5
+ from autocoder.common.text import TextSimilarity
6
+ from autocoder.memory.active_context_manager import ActiveContextManager
7
+ import pydantic
8
+ import byzerllm
9
+
10
+ import hashlib
11
+ import subprocess
12
+ import tempfile
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.syntax import Syntax
16
+ import json
17
+ from typing import Union, List, Tuple, Dict
18
+ from autocoder.common.types import CodeGenerateResult, MergeCodeWithoutEffect
19
+ from autocoder.common.code_modification_ranker import CodeModificationRanker
20
+ from autocoder.common import files as FileUtils
21
+ from autocoder.common.printer import Printer
22
+ from autocoder.shadows.shadow_manager import ShadowManager
23
+
24
+ class PathAndCode(pydantic.BaseModel):
25
+ path: str
26
+ content: str
27
+
28
+
29
+ class CodeAutoMergeEditBlock:
30
+ def __init__(
31
+ self,
32
+ llm: byzerllm.ByzerLLM,
33
+ args: AutoCoderArgs,
34
+ fence_0: str = "```",
35
+ fence_1: str = "```",
36
+ ):
37
+ self.llm = llm
38
+ self.args = args
39
+ self.fence_0 = fence_0
40
+ self.fence_1 = fence_1
41
+ self.printer = Printer()
42
+
43
+ def run_pylint(self, code: str) -> tuple[bool, str]:
44
+ with tempfile.NamedTemporaryFile(
45
+ mode="w", suffix=".py", delete=False
46
+ ) as temp_file:
47
+ temp_file.write(code)
48
+ temp_file_path = temp_file.name
49
+
50
+ try:
51
+ result = subprocess.run(
52
+ [
53
+ "pylint",
54
+ "--disable=all",
55
+ "--enable=E0001,W0311,W0312",
56
+ temp_file_path,
57
+ ],
58
+ capture_output=True,
59
+ text=True,
60
+ check=False,
61
+ )
62
+ os.unlink(temp_file_path)
63
+ if result.returncode != 0:
64
+ error_message = result.stdout.strip() or result.stderr.strip()
65
+ self.printer.print_in_terminal("pylint_check_failed", error_message=error_message)
66
+ return False, error_message
67
+ return True, ""
68
+ except subprocess.CalledProcessError as e:
69
+ error_message = f"Error running pylint: {str(e)}"
70
+ self.printer.print_in_terminal("pylint_error", error_message=error_message)
71
+ os.unlink(temp_file_path)
72
+ return False, error_message
73
+
74
+ def parse_whole_text(self, text: str) -> List[PathAndCode]:
75
+ '''
76
+ 从文本中抽取如下格式代码(two_line_mode):
77
+
78
+ ```python
79
+ ##File: /project/path/src/autocoder/index/index.py
80
+ <<<<<<< SEARCH
81
+ =======
82
+ >>>>>>> REPLACE
83
+ ```
84
+
85
+ 或者 (one_line_mode)
86
+
87
+ ```python:/project/path/src/autocoder/index/index.py
88
+ <<<<<<< SEARCH
89
+ =======
90
+ >>>>>>> REPLACE
91
+ ```
92
+
93
+ '''
94
+ HEAD = "<<<<<<< SEARCH"
95
+ DIVIDER = "======="
96
+ UPDATED = ">>>>>>> REPLACE"
97
+ lines = text.split("\n")
98
+ lines_len = len(lines)
99
+ start_marker_count = 0
100
+ block = []
101
+ path_and_code_list = []
102
+ # two_line_mode or one_line_mode
103
+ current_editblock_mode = "two_line_mode"
104
+ current_editblock_path = None
105
+
106
+ def guard(index):
107
+ return index + 1 < lines_len
108
+
109
+ def start_marker(line, index):
110
+ nonlocal current_editblock_mode
111
+ nonlocal current_editblock_path
112
+ if (
113
+ line.startswith(self.fence_0)
114
+ and guard(index)
115
+ and ":" in line
116
+ and lines[index + 1].startswith(HEAD)
117
+ ):
118
+
119
+ current_editblock_mode = "one_line_mode"
120
+ current_editblock_path = line.split(":", 1)[1].strip()
121
+ return True
122
+
123
+ if (
124
+ line.startswith(self.fence_0)
125
+ and guard(index)
126
+ and lines[index + 1].startswith("##File:")
127
+ ):
128
+ current_editblock_mode = "two_line_mode"
129
+ current_editblock_path = None
130
+ return True
131
+
132
+ return False
133
+
134
+ def end_marker(line, index):
135
+ return line.startswith(self.fence_1) and UPDATED in lines[index - 1]
136
+
137
+ for index, line in enumerate(lines):
138
+ if start_marker(line, index) and start_marker_count == 0:
139
+ start_marker_count += 1
140
+ elif end_marker(line, index) and start_marker_count == 1:
141
+ start_marker_count -= 1
142
+ if block:
143
+ if current_editblock_mode == "two_line_mode":
144
+ path = block[0].split(":", 1)[1].strip()
145
+ content = "\n".join(block[1:])
146
+ else:
147
+ path = current_editblock_path
148
+ content = "\n".join(block)
149
+ block = []
150
+ path_and_code_list.append(
151
+ PathAndCode(path=path, content=content))
152
+ elif start_marker_count > 0:
153
+ block.append(line)
154
+
155
+ return path_and_code_list
156
+
157
+ def merge_code(self, generate_result: CodeGenerateResult, force_skip_git: bool = False):
158
+ result = self.choose_best_choice(generate_result)
159
+ self._merge_code(result.contents[0], force_skip_git)
160
+ return result
161
+
162
+ def choose_best_choice(self, generate_result: CodeGenerateResult) -> CodeGenerateResult:
163
+ if len(generate_result.contents) == 1:
164
+ return generate_result
165
+
166
+ merge_results = []
167
+ for content,conversations in zip(generate_result.contents,generate_result.conversations):
168
+ merge_result = self._merge_code_without_effect(content)
169
+ merge_results.append(merge_result)
170
+
171
+ # If all merge results are None, return first one
172
+ if all(len(result.failed_blocks) != 0 for result in merge_results):
173
+ self.printer.print_in_terminal("all_merge_results_failed")
174
+ return CodeGenerateResult(contents=[generate_result.contents[0]], conversations=[generate_result.conversations[0]])
175
+
176
+ # If only one merge result is not None, return that one
177
+ not_none_indices = [i for i, result in enumerate(merge_results) if len(result.failed_blocks) == 0]
178
+ if len(not_none_indices) == 1:
179
+ idx = not_none_indices[0]
180
+ self.printer.print_in_terminal("only_one_merge_result_success")
181
+ return CodeGenerateResult(contents=[generate_result.contents[idx]], conversations=[generate_result.conversations[idx]])
182
+
183
+ # 最后,如果有多个,那么根据质量排序再返回
184
+ ranker = CodeModificationRanker(self.llm, self.args)
185
+ ranked_result = ranker.rank_modifications(generate_result,merge_results)
186
+
187
+ ## 得到的结果,再做一次合并,第一个通过的返回 , 返回做合并有点重复低效,未来修改。
188
+ for content,conversations in zip(ranked_result.contents,ranked_result.conversations):
189
+ merge_result = self._merge_code_without_effect(content)
190
+ if not merge_result.failed_blocks:
191
+ return CodeGenerateResult(contents=[content], conversations=[conversations])
192
+
193
+ # 最后保底,但实际不会出现
194
+ return CodeGenerateResult(contents=[ranked_result.contents[0]], conversations=[ranked_result.conversations[0]])
195
+
196
+ @byzerllm.prompt()
197
+ def git_require_msg(self, source_dir: str, error: str) -> str:
198
+ """
199
+ auto_merge only works for git repositories.
200
+
201
+ Try to use git init in the source directory.
202
+
203
+ ```shell
204
+ cd {{ source_dir }}
205
+ git init .
206
+ ```
207
+
208
+ Then try to run auto-coder again.
209
+ Error: {{ error }}
210
+ """
211
+
212
+ def get_edits(self, content: str):
213
+ edits = self.parse_whole_text(content)
214
+ HEAD = "<<<<<<< SEARCH"
215
+ DIVIDER = "======="
216
+ UPDATED = ">>>>>>> REPLACE"
217
+ result = []
218
+ for edit in edits:
219
+ heads = []
220
+ updates = []
221
+ c = edit.content
222
+ in_head = False
223
+ in_updated = False
224
+ for line in c.splitlines():
225
+ if line.strip() == HEAD:
226
+ in_head = True
227
+ continue
228
+ if line.strip() == DIVIDER:
229
+ in_head = False
230
+ in_updated = True
231
+ continue
232
+ if line.strip() == UPDATED:
233
+ in_head = False
234
+ in_updated = False
235
+ continue
236
+ if in_head:
237
+ heads.append(line)
238
+ if in_updated:
239
+ updates.append(line)
240
+ result.append((edit.path, "\n".join(heads), "\n".join(updates)))
241
+ return result
242
+
243
+ def get_source_code_list_from_shadow_files(self, shadow_files: Dict[str, str]) -> SourceCodeList:
244
+ """
245
+ 将影子文件转换为SourceCodeList对象
246
+
247
+ 参数:
248
+ shadow_files (Dict[str, str]): 映射 {影子文件路径: 内容}
249
+
250
+ 返回:
251
+ SourceCodeList: 包含原始路径和内容的SourceCodeList对象
252
+ """
253
+ sources = []
254
+ shadow_manager = ShadowManager(self.args.source_dir,event_file_id=self.args.event_file)
255
+ for shadow_path, content in shadow_files.items():
256
+ # 将影子路径转换回原始文件路径
257
+ file_path = shadow_manager.from_shadow_path(shadow_path)
258
+ # 创建SourceCode对象并添加到sources列表
259
+ source = SourceCode(module_name=file_path, source_code=content)
260
+ sources.append(source)
261
+
262
+ return SourceCodeList(sources)
263
+
264
+ def _merge_code_without_effect(self, content: str) -> MergeCodeWithoutEffect:
265
+ """Merge code without any side effects like git operations, linting or file writing.
266
+ Returns a tuple of:
267
+ - list of (file_path, new_content) tuples for successfully merged blocks
268
+ - list of (file_path, head, update) tuples for failed to merge blocks"""
269
+ codes = self.get_edits(content)
270
+ file_content_mapping = {}
271
+ failed_blocks = []
272
+
273
+ for block in codes:
274
+ file_path, head, update = block
275
+ if not os.path.exists(file_path):
276
+ file_content_mapping[file_path] = update
277
+ else:
278
+ if file_path not in file_content_mapping:
279
+ file_content_mapping[file_path] = FileUtils.read_file(file_path)
280
+ existing_content = file_content_mapping[file_path]
281
+
282
+ # First try exact match
283
+ new_content = (
284
+ existing_content.replace(head, update, 1)
285
+ if head
286
+ else existing_content + "\n" + update
287
+ )
288
+
289
+ # If exact match fails, try similarity match
290
+ if new_content == existing_content and head:
291
+ similarity, best_window = TextSimilarity(
292
+ head, existing_content
293
+ ).get_best_matching_window()
294
+ if similarity > self.args.editblock_similarity:
295
+ new_content = existing_content.replace(
296
+ best_window, update, 1
297
+ )
298
+
299
+ if new_content != existing_content:
300
+ file_content_mapping[file_path] = new_content
301
+ else:
302
+ failed_blocks.append((file_path, head, update))
303
+
304
+ return MergeCodeWithoutEffect(
305
+ success_blocks=[(path, content)
306
+ for path, content in file_content_mapping.items()],
307
+ failed_blocks=failed_blocks
308
+ )
309
+
310
+
311
+ def _merge_code(self, content: str, force_skip_git: bool = False):
312
+ file_content = FileUtils.read_file(self.args.file)
313
+ md5 = hashlib.md5(file_content.encode("utf-8")).hexdigest()
314
+ file_name = os.path.basename(self.args.file)
315
+
316
+ codes = self.get_edits(content)
317
+ changes_to_make = []
318
+ changes_made = False
319
+ unmerged_blocks = []
320
+ merged_blocks = []
321
+
322
+ # First, check if there are any changes to be made
323
+ file_content_mapping = {}
324
+ for block in codes:
325
+ file_path, head, update = block
326
+ if not os.path.exists(file_path):
327
+ changes_to_make.append((file_path, None, update))
328
+ file_content_mapping[file_path] = update
329
+ merged_blocks.append((file_path, "", update, 1))
330
+ changes_made = True
331
+ else:
332
+ if file_path not in file_content_mapping:
333
+ file_content_mapping[file_path] = FileUtils.read_file(file_path)
334
+ existing_content = file_content_mapping[file_path]
335
+ new_content = (
336
+ existing_content.replace(head, update, 1)
337
+ if head
338
+ else existing_content + "\n" + update
339
+ )
340
+ if new_content != existing_content:
341
+ changes_to_make.append(
342
+ (file_path, existing_content, new_content))
343
+ file_content_mapping[file_path] = new_content
344
+ merged_blocks.append((file_path, head, update, 1))
345
+ changes_made = True
346
+ else:
347
+ # If the SEARCH BLOCK is not found exactly, then try to use
348
+ # the similarity ratio to find the best matching block
349
+ similarity, best_window = TextSimilarity(
350
+ head, existing_content
351
+ ).get_best_matching_window()
352
+ if similarity > self.args.editblock_similarity:
353
+ new_content = existing_content.replace(
354
+ best_window, update, 1)
355
+ if new_content != existing_content:
356
+ changes_to_make.append(
357
+ (file_path, existing_content, new_content)
358
+ )
359
+ file_content_mapping[file_path] = new_content
360
+ merged_blocks.append(
361
+ (file_path, head, update, similarity))
362
+ changes_made = True
363
+ else:
364
+ unmerged_blocks.append(
365
+ (file_path, head, update, similarity))
366
+
367
+ if unmerged_blocks:
368
+ if self.args.request_id and not self.args.skip_events:
369
+ # collect unmerged blocks
370
+ event_data = []
371
+ for file_path, head, update, similarity in unmerged_blocks:
372
+ event_data.append(
373
+ {
374
+ "file_path": file_path,
375
+ "head": head,
376
+ "update": update,
377
+ "similarity": similarity,
378
+ }
379
+ )
380
+ return
381
+
382
+ self.printer.print_in_terminal("unmerged_blocks_warning", num_blocks=len(unmerged_blocks))
383
+ self._print_unmerged_blocks(unmerged_blocks)
384
+ return
385
+
386
+ # lint check
387
+ for file_path, new_content in file_content_mapping.items():
388
+ if file_path.endswith(".py"):
389
+ pylint_passed, error_message = self.run_pylint(new_content)
390
+ if not pylint_passed:
391
+ self.printer.print_in_terminal("pylint_file_check_failed",
392
+ file_path=file_path,
393
+ error_message=error_message)
394
+
395
+ if changes_made and not force_skip_git and not self.args.skip_commit:
396
+ try:
397
+ git_utils.commit_changes(
398
+ self.args.source_dir, f"auto_coder_pre_{file_name}_{md5}"
399
+ )
400
+ except Exception as e:
401
+ self.printer.print_str_in_terminal(
402
+ self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
403
+ style="red"
404
+ )
405
+ return
406
+
407
+ # Now, apply the changes
408
+ for file_path, new_content in file_content_mapping.items():
409
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
410
+ with open(file_path, "w") as f:
411
+ f.write(new_content)
412
+
413
+ if self.args.request_id and not self.args.skip_events:
414
+ # collect modified files
415
+ event_data = []
416
+ for code in merged_blocks:
417
+ file_path, head, update, similarity = code
418
+ event_data.append(
419
+ {
420
+ "file_path": file_path,
421
+ "head": head,
422
+ "update": update,
423
+ "similarity": similarity,
424
+ }
425
+ )
426
+
427
+
428
+ if changes_made:
429
+ if not force_skip_git and not self.args.skip_commit:
430
+ try:
431
+ commit_result = git_utils.commit_changes(
432
+ self.args.source_dir,
433
+ f"{self.args.query}\nauto_coder_{file_name}",
434
+ )
435
+
436
+ action_yml_file_manager = ActionYmlFileManager(self.args.source_dir)
437
+ action_file_name = os.path.basename(self.args.file)
438
+ add_updated_urls = []
439
+ commit_result.changed_files
440
+ for file in commit_result.changed_files:
441
+ add_updated_urls.append(os.path.join(self.args.source_dir, file))
442
+
443
+ self.args.add_updated_urls = add_updated_urls
444
+ update_yaml_success = action_yml_file_manager.update_yaml_field(action_file_name, "add_updated_urls", add_updated_urls)
445
+ if not update_yaml_success:
446
+ self.printer.print_in_terminal("yaml_save_error", style="red", yaml_file=action_file_name)
447
+
448
+ if self.args.enable_active_context:
449
+ active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
450
+ task_id = active_context_manager.process_changes(self.args)
451
+ self.printer.print_in_terminal("active_context_background_task",
452
+ style="blue",
453
+ task_id=task_id)
454
+ git_utils.print_commit_info(commit_result=commit_result)
455
+ except Exception as e:
456
+ self.printer.print_str_in_terminal(
457
+ self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
458
+ style="red"
459
+ )
460
+ else:
461
+ self.print_merged_blocks(merged_blocks)
462
+
463
+ self.printer.print_in_terminal("merge_success",
464
+ num_files=len(file_content_mapping.keys()),
465
+ num_changes=len(changes_to_make),
466
+ total_blocks=len(codes))
467
+
468
+ else:
469
+ self.printer.print_in_terminal("no_changes_made")
470
+
471
+ def _print_unmerged_blocks(self, unmerged_blocks: List[tuple]):
472
+ self.printer.print_in_terminal("unmerged_blocks_title", style="bold red")
473
+ for file_path, head, update, similarity in unmerged_blocks:
474
+ self.printer.print_str_in_terminal(
475
+ f"\n{self.printer.get_message_from_key_with_format('unmerged_file_path',file_path=file_path)}",
476
+ style="bold blue"
477
+ )
478
+ self.printer.print_str_in_terminal(
479
+ f"\n{self.printer.get_message_from_key_with_format('unmerged_search_block',similarity=similarity)}",
480
+ style="bold green"
481
+ )
482
+ syntax = Syntax(head, "python", theme="monokai", line_numbers=True)
483
+ self.printer.console.print(Panel(syntax, expand=False))
484
+ self.printer.print_in_terminal("unmerged_replace_block", style="bold yellow")
485
+ syntax = Syntax(update, "python", theme="monokai", line_numbers=True)
486
+ self.printer.console.print(Panel(syntax, expand=False))
487
+ self.printer.print_in_terminal("unmerged_blocks_total", num_blocks=len(unmerged_blocks), style="bold red")
488
+
489
+
490
+ def print_merged_blocks(self, merged_blocks: List[tuple]):
491
+ """Print search/replace blocks for user review using rich library"""
492
+ from rich.syntax import Syntax
493
+ from rich.panel import Panel
494
+
495
+ # Group blocks by file path
496
+ file_blocks = {}
497
+ for file_path, head, update, similarity in merged_blocks:
498
+ if file_path not in file_blocks:
499
+ file_blocks[file_path] = []
500
+ file_blocks[file_path].append((head, update, similarity))
501
+
502
+ # Generate formatted text for each file
503
+ formatted_text = ""
504
+ for file_path, blocks in file_blocks.items():
505
+ formatted_text += f"##File: {file_path}\n"
506
+ for head, update, similarity in blocks:
507
+ formatted_text += "<<<<<<< SEARCH\n"
508
+ formatted_text += head + "\n"
509
+ formatted_text += "=======\n"
510
+ formatted_text += update + "\n"
511
+ formatted_text += ">>>>>>> REPLACE\n"
512
+ formatted_text += "\n"
513
+
514
+ # Print with rich panel
515
+ self.printer.print_in_terminal("merged_blocks_title", style="bold green")
516
+ self.printer.console.print(
517
+ Panel(
518
+ Syntax(formatted_text, "diff", theme="monokai"),
519
+ title="Merged Changes",
520
+ border_style="green",
521
+ expand=False
522
+ )
523
+ )