jarvis-ai-assistant 0.1.87__py3-none-any.whl → 0.1.89__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 jarvis-ai-assistant might be problematic. Click here for more details.

jarvis/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Jarvis AI Assistant"""
2
2
 
3
- __version__ = "0.1.87"
3
+ __version__ = "0.1.89"
@@ -270,8 +270,7 @@ class CodeBase:
270
270
 
271
271
  except Exception as e:
272
272
  PrettyOutput.print(f"处理文件失败 {file_path}: {str(e)}",
273
- output_type=OutputType.ERROR,
274
- traceback=True)
273
+ output_type=OutputType.ERROR)
275
274
  return None
276
275
 
277
276
  def build_index(self):
@@ -0,0 +1,67 @@
1
+ import os
2
+ from typing import List
3
+ import yaml
4
+ import time
5
+ from jarvis.utils import OutputType, PrettyOutput
6
+ from jarvis.models.registry import PlatformRegistry
7
+ from .model_utils import call_model_with_retry
8
+
9
+ def has_uncommitted_files() -> bool:
10
+ """判断代码库是否有未提交的文件"""
11
+ # 获取未暂存的修改
12
+ unstaged = os.popen("git diff --name-only").read()
13
+ # 获取已暂存但未提交的修改
14
+ staged = os.popen("git diff --cached --name-only").read()
15
+ # 获取未跟踪的文件
16
+ untracked = os.popen("git ls-files --others --exclude-standard").read()
17
+
18
+ return bool(unstaged or staged or untracked)
19
+
20
+ def generate_commit_message(git_diff: str, feature: str) -> str:
21
+ """根据git diff和功能描述生成commit信息"""
22
+ prompt = f"""你是一个经验丰富的程序员,请根据以下代码变更和功能描述生成简洁明了的commit信息:
23
+
24
+ 功能描述:
25
+ {feature}
26
+
27
+ 代码变更:
28
+ Git Diff:
29
+ {git_diff}
30
+
31
+ 请遵循以下规则:
32
+ 1. 使用英文编写
33
+ 2. 采用常规的commit message格式:<type>(<scope>): <subject>
34
+ 3. 保持简洁,不超过50个字符
35
+ 4. 准确描述代码变更的主要内容
36
+ 5. 优先考虑功能描述和git diff中的变更内容
37
+ """
38
+
39
+ model = PlatformRegistry().get_global_platform_registry().get_codegen_platform()
40
+ model.set_suppress_output(True)
41
+ success, response = call_model_with_retry(model, prompt)
42
+ if not success:
43
+ return "Update code changes"
44
+
45
+ return response.strip().split("\n")[0]
46
+
47
+ def save_edit_record(record_dir: str, commit_message: str, git_diff: str) -> None:
48
+ """保存代码修改记录"""
49
+ # 获取下一个序号
50
+ existing_records = [f for f in os.listdir(record_dir) if f.endswith('.yaml')]
51
+ next_num = 1
52
+ if existing_records:
53
+ last_num = max(int(f[:4]) for f in existing_records)
54
+ next_num = last_num + 1
55
+
56
+ # 创建记录文件
57
+ record = {
58
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
59
+ "commit_message": commit_message,
60
+ "git_diff": git_diff
61
+ }
62
+
63
+ record_path = os.path.join(record_dir, f"{next_num:04d}.yaml")
64
+ with open(record_path, "w", encoding="utf-8") as f:
65
+ yaml.safe_dump(record, f, allow_unicode=True)
66
+
67
+ PrettyOutput.print(f"已保存修改记录: {record_path}", OutputType.SUCCESS)
@@ -14,6 +14,8 @@ from prompt_toolkit.completion import WordCompleter, Completer, Completion
14
14
  from prompt_toolkit.formatted_text import FormattedText
15
15
  from prompt_toolkit.styles import Style
16
16
  import fnmatch
17
+ from .patch_handler import PatchHandler
18
+ from .git_utils import has_uncommitted_files, generate_commit_message, save_edit_record
17
19
 
18
20
  # 全局锁对象
19
21
  index_lock = threading.Lock()
@@ -21,13 +23,18 @@ index_lock = threading.Lock()
21
23
  class JarvisCoder:
22
24
  def __init__(self, root_dir: str, language: str):
23
25
  """初始化代码修改工具"""
24
-
26
+ self.root_dir = root_dir
27
+ self.language = language
28
+ self._init_directories()
29
+ self._init_codebase()
30
+
31
+ def _init_directories(self):
32
+ """初始化目录"""
25
33
  self.max_context_length = get_max_context_length()
26
34
 
27
-
28
- self.root_dir = find_git_root(root_dir)
35
+ self.root_dir = find_git_root(self.root_dir)
29
36
  if not self.root_dir:
30
- self.root_dir = root_dir
37
+ self.root_dir = self.root_dir
31
38
 
32
39
  PrettyOutput.print(f"Git根目录: {self.root_dir}", OutputType.INFO)
33
40
 
@@ -57,13 +64,22 @@ class JarvisCoder:
57
64
  # 2.2 提交
58
65
  os.system(f"git commit -m 'Initial commit'")
59
66
 
67
+ PrettyOutput.print("代码库有未提交的文件,提交一次", OutputType.INFO)
68
+ os.system(f"git add .")
69
+ os.system(f"git commit -m 'commit before code edit'")
60
70
  # 3. 查看代码库是否有未提交的文件,如果有,提交一次
61
71
  if self._has_uncommitted_files():
72
+ PrettyOutput.print("代码库有未提交的文件,提交一次", OutputType.INFO)
73
+ os.system(f"git add .")
74
+ git_diff = os.popen("git diff --cached").read()
75
+ commit_message = generate_commit_message(git_diff, "Pre-edit commit")
76
+ os.system(f"git commit -m '{commit_message}'")
62
77
  PrettyOutput.print("代码库有未提交的文件,提交一次", OutputType.INFO)
63
78
  os.system(f"git add .")
64
79
  os.system(f"git commit -m 'commit before code edit'")
65
80
 
66
- # 4. 初始化代码库
81
+ def _init_codebase(self):
82
+ """初始化代码库"""
67
83
  self._codebase = CodeBase(self.root_dir)
68
84
 
69
85
  def _new_model(self):
@@ -82,260 +98,11 @@ class JarvisCoder:
82
98
 
83
99
  return bool(unstaged or staged or untracked)
84
100
 
85
- def _call_model_with_retry(self, model: BasePlatform, prompt: str, max_retries: int = 3, initial_delay: float = 1.0) -> Tuple[bool, str]:
86
- """调用模型并支持重试
87
-
88
- Args:
89
- prompt: 提示词
90
- max_retries: 最大重试次数
91
- initial_delay: 初始延迟时间(秒)
92
-
93
- Returns:
94
- Tuple[bool, str]: (是否成功, 响应内容)
95
- """
96
- delay = initial_delay
97
- for attempt in range(max_retries):
98
- try:
99
- response = model.chat(prompt)
100
- return True, response
101
- except Exception as e:
102
- if attempt == max_retries - 1: # 最后一次尝试
103
- PrettyOutput.print(f"调用模型失败: {str(e)}", OutputType.ERROR)
104
- return False, str(e)
105
-
106
- PrettyOutput.print(f"调用模型失败,{delay}秒后重试: {str(e)}", OutputType.WARNING)
107
- time.sleep(delay)
108
- delay *= 2 # 指数退避
109
-
110
- def _remake_patch(self, prompt: str) -> List[str]:
111
- success, response = self._call_model_with_retry(self.main_model, prompt, max_retries=5) # 增加重试次数
112
- if not success:
113
- return []
114
-
115
- try:
116
- patches = re.findall(r'<PATCH>.*?</PATCH>', response, re.DOTALL)
117
- return [patch.replace('<PATCH>', '').replace('</PATCH>', '').strip()
118
- for patch in patches if patch.strip()]
119
- except Exception as e:
120
- PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.WARNING)
121
- return []
122
-
123
- def _make_patch(self, related_files: List[Dict], feature: str) -> List[str]:
124
- """生成修改方案"""
125
- prompt = """你是一个资深程序员,请根据需求描述,修改文件内容。
126
-
127
- 修改格式说明:
128
- 1. 每个修改块格式如下:
129
- <PATCH>
130
- > path/to/file
131
- def old_function():
132
- print("old code")
133
- return False
134
- =======
135
- def old_function():
136
- print("new code")
137
- return True
138
- </PATCH>
139
-
140
- 2. 如果是新文件或者替换整个文件内容,格式如下:
141
- <PATCH>
142
- > src/new_module.py
143
- =======
144
- from typing import List
145
-
146
- def new_function():
147
- return "This is a new file"
148
- </PATCH>
149
-
150
- 3. 如果要删除文件中的某一段,格式如下:
151
- <PATCH>
152
- > path/to/file
153
- # 这是要删除的注释
154
- deprecated_code = True
155
- if deprecated_code:
156
- print("old feature")
157
- =======
158
- </PATCH>
159
-
160
- 4. 如果要修改导入语句,格式如下:
161
- <PATCH>
162
- > src/main.py
163
- from old_module import old_class
164
- =======
165
- from new_module import new_class
166
- </PATCH>
167
-
168
- 5. 如果要修改类定义,格式如下:
169
- <PATCH>
170
- > src/models.py
171
- class OldModel:
172
- def __init__(self):
173
- self.value = 0
174
- =======
175
- class OldModel:
176
- def __init__(self):
177
- self.value = 1
178
- self.name = "new"
179
- </PATCH>
180
-
181
- 文件列表如下:
182
- """
183
- for i, file in enumerate(related_files):
184
- if len(prompt) > self.max_context_length:
185
- PrettyOutput.print(f'避免上下文超限,丢弃低相关度文件:{file["file_path"]}', OutputType.WARNING)
186
- continue
187
- prompt += f"""{i}. {file["file_path"]}\n"""
188
- prompt += f"""文件内容:\n"""
189
- prompt += f"<FILE_CONTENT>\n"
190
- prompt += f'{file["file_content"]}\n'
191
- prompt += f"</FILE_CONTENT>\n"
192
-
193
- prompt += f"\n需求描述: {feature}\n"
194
- prompt += """
195
- 注意事项:
196
- 1、仅输出补丁内容,不要输出任何其他内容,每个补丁必须用<PATCH>和</PATCH>标记
197
- 2、如果在大段代码中有零星修改,生成多个补丁
198
- 3、要替换的内容,一定要与文件内容完全一致,不要有任何多余或者缺失的内容
199
- 4、每个patch不超过20行,超出20行,请生成多个patch
200
- """
201
-
202
- success, response = self._call_model_with_retry(self.main_model, prompt)
203
- if not success:
204
- return []
205
-
206
- try:
207
- # 使用正则表达式匹配每个patch块
208
- patches = re.findall(r'<PATCH>.*?</PATCH>', response, re.DOTALL)
209
- return [patch.replace('<PATCH>', '').replace('</PATCH>', '').strip()
210
- for patch in patches if patch.strip()]
211
- except Exception as e:
212
- PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.WARNING)
213
- return []
214
-
215
- def _apply_patch(self, related_files: List[Dict], patches: List[str]) -> Tuple[bool, str]:
216
- """应用补丁"""
217
- error_info = []
218
- modified_files = set()
219
-
220
- # 创建文件内容映射
221
- file_map = {file["file_path"]: file["file_content"] for file in related_files}
222
- temp_map = file_map.copy() # 创建临时映射用于尝试应用
223
-
224
- # 尝试应用所有补丁
225
- for i, patch in enumerate(patches):
226
- PrettyOutput.print(f"正在应用补丁 {i+1}/{len(patches)}", OutputType.INFO)
227
-
228
- try:
229
- # 解析补丁
230
- lines = patch.split("\n")
231
- if not lines:
232
- continue
233
-
234
- # 获取文件路径
235
- file_path_match = re.search(r'> (.*)', lines[0])
236
- if not file_path_match:
237
- error_info.append(f"无法解析文件路径: {lines[0]}")
238
- return False, "\n".join(error_info)
239
-
240
- file_path = file_path_match.group(1).strip()
241
-
242
- # 解析补丁内容
243
- patch_content = "\n".join(lines[1:])
244
- parts = patch_content.split("=======")
245
-
246
- if len(parts) != 2:
247
- error_info.append(f"补丁格式错误: {file_path}")
248
- return False, "\n".join(error_info)
249
-
250
- old_content = parts[0]
251
- new_content = parts[1].split("</PATCH>")[0]
252
-
253
- # 处理新文件
254
- if not old_content:
255
- temp_map[file_path] = new_content
256
- modified_files.add(file_path)
257
- continue
258
-
259
- # 处理文件修改
260
- if file_path not in temp_map:
261
- error_info.append(f"文件不存在: {file_path}")
262
- return False, "\n".join(error_info)
263
-
264
- current_content = temp_map[file_path]
265
-
266
- # 查找并替换代码块
267
- if old_content not in current_content:
268
- error_info.append(
269
- f"补丁应用失败: {file_path}\n"
270
- f"原因: 未找到要替换的代码\n"
271
- f"期望找到的代码:\n{old_content}\n"
272
- f"实际文件内容:\n{current_content[:200]}..." # 只显示前200个字符
273
- )
274
- return False, "\n".join(error_info)
275
-
276
- # 应用更改
277
- temp_map[file_path] = current_content.replace(old_content, new_content)
278
- modified_files.add(file_path)
279
-
280
- except Exception as e:
281
- error_info.append(f"处理补丁时发生错误: {str(e)}")
282
- return False, "\n".join(error_info)
283
-
284
- # 所有补丁都应用成功,更新实际文件
285
- for file_path in modified_files:
286
- try:
287
- dir_path = os.path.dirname(file_path)
288
- if dir_path and not os.path.exists(dir_path):
289
- os.makedirs(dir_path, exist_ok=True)
290
-
291
- with open(file_path, "w", encoding="utf-8") as f:
292
- f.write(temp_map[file_path])
293
-
294
- PrettyOutput.print(f"成功修改文件: {file_path}", OutputType.SUCCESS)
295
-
296
- except Exception as e:
297
- error_info.append(f"写入文件失败 {file_path}: {str(e)}")
298
- return False, "\n".join(error_info)
299
-
300
- return True, ""
301
-
302
- def _save_edit_record(self, commit_message: str, git_diff: str) -> None:
303
- """保存代码修改记录
304
-
305
- Args:
306
- commit_message: 提交信息
307
- git_diff: git diff --cached的输出
308
- """
309
-
310
- # 获取下一个序号
311
- existing_records = [f for f in os.listdir(self.record_dir) if f.endswith('.yaml')]
312
- next_num = 1
313
- if existing_records:
314
- last_num = max(int(f[:4]) for f in existing_records)
315
- next_num = last_num + 1
316
-
317
- # 创建记录文件
318
- record = {
319
- "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
320
- "commit_message": commit_message,
321
- "git_diff": git_diff
322
- }
323
-
324
- record_path = os.path.join(self.record_dir, f"{next_num:04d}.yaml")
325
- with open(record_path, "w", encoding="utf-8") as f:
326
- yaml.safe_dump(record, f, allow_unicode=True)
327
-
328
- PrettyOutput.print(f"已保存修改记录: {record_path}", OutputType.SUCCESS)
329
-
330
-
331
-
332
-
333
101
  def _prepare_execution(self) -> None:
334
102
  """准备执行环境"""
335
103
  self.main_model = self._new_model()
336
104
  self._codebase.generate_codebase()
337
105
 
338
-
339
106
  def _load_related_files(self, feature: str) -> List[Dict]:
340
107
  """加载相关文件内容"""
341
108
  ret = []
@@ -352,135 +119,6 @@ class OldModel:
352
119
  ret.append({"file_path": file, "file_content": content})
353
120
  return ret
354
121
 
355
- def _handle_patch_application(self, related_files: List[Dict], patches: List[str], feature: str) -> Dict[str, Any]:
356
- """处理补丁应用流程"""
357
- while True:
358
- PrettyOutput.print(f"生成{len(patches)}个补丁", OutputType.INFO)
359
-
360
- if not patches:
361
- retry_prompt = f"""未生成补丁,请重新生成补丁"""
362
- patches = self._remake_patch(retry_prompt)
363
- continue
364
-
365
- success, error_info = self._apply_patch(related_files, patches)
366
-
367
- if success:
368
- user_confirm = input("是否确认修改?(y/n)")
369
- if user_confirm.lower() == "y":
370
- self._finalize_changes(feature)
371
- return {
372
- "success": True,
373
- "stdout": f"已完成功能开发{feature}",
374
- "stderr": "",
375
- "error": None
376
- }
377
- else:
378
- self._revert_changes()
379
-
380
- # 让用户输入调整意见
381
- user_feedback = get_multiline_input("""
382
- 请提供修改建议,帮助生成更好的补丁:
383
- 1. 修改的位置是否正确?
384
- 2. 修改的内容是否合适?
385
- 3. 是否有遗漏的修改?
386
- 4. 其他调整建议?
387
-
388
- 请输入调整意见(直接回车跳过):""")
389
-
390
- if not user_feedback:
391
- return {
392
- "success": False,
393
- "stdout": "",
394
- "stderr": "修改被用户取消,文件未发生任何变化",
395
- "error": UserWarning("用户取消修改")
396
- }
397
-
398
- retry_prompt = f"""补丁被用户拒绝,请根据用户意见重新生成补丁:
399
-
400
- 用户意见:
401
- {user_feedback}
402
-
403
- 请重新生成补丁,确保:
404
- 1. 按照用户意见调整修改内容
405
- 2. 准确定位要修改的代码位置
406
- 3. 正确处理代码缩进
407
- 4. 考虑代码上下文
408
- """
409
- patches = self._remake_patch(retry_prompt)
410
- continue
411
- else:
412
- PrettyOutput.print(f"补丁应用失败: {error_info}", OutputType.WARNING)
413
-
414
- # 让用户输入补充信息
415
- user_info = get_multiline_input("""
416
- 补丁应用失败。请提供更多信息来帮助修复问题:
417
- 1. 是否需要调整代码位置?
418
- 2. 是否有特殊的格式要求?
419
- 3. 是否需要考虑其他文件的依赖?
420
- 4. 其他补充说明?
421
-
422
- 请输入补充信息(直接回车跳过):""")
423
-
424
- retry_prompt = f"""补丁应用失败,请根据以下信息重新生成补丁:
425
-
426
- 错误信息:
427
- {error_info}
428
-
429
- 用户补充信息:
430
- {user_info if user_info else "用户未提供补充信息"}
431
-
432
- 请确保:
433
- 1. 准确定位要修改的代码位置
434
- 2. 正确处理代码缩进
435
- 3. 考虑代码上下文
436
- 4. 对新文件不要包含原始内容
437
- """
438
- patches = self._remake_patch(retry_prompt)
439
-
440
-
441
-
442
-
443
- def _generate_commit_message(self, git_diff: str, feature: str) -> str:
444
- """根据git diff和功能描述生成commit信息
445
-
446
- Args:
447
- git_diff: git diff --cached的输出
448
- feature: 用户的功能描述
449
-
450
- Returns:
451
- str: 生成的commit信息
452
- """
453
-
454
- # 生成提示词
455
- prompt = f"""你是一个经验丰富的程序员,请根据以下代码变更和功能描述生成简洁明了的commit信息:
456
-
457
- 功能描述:
458
- {feature}
459
-
460
- 代码变更:
461
- """
462
- # 添加git diff内容
463
- prompt += f"Git Diff:\n{git_diff}\n\n"
464
-
465
- prompt += """
466
- 请遵循以下规则:
467
- 1. 使用英文编写
468
- 2. 采用常规的commit message格式:<type>(<scope>): <subject>
469
- 3. 保持简洁,不超过50个字符
470
- 4. 准确描述代码变更的主要内容
471
- 5. 优先考虑功能描述和git diff中的变更内容
472
- """
473
-
474
- # 使用normal模型生成commit信息
475
- model = PlatformRegistry().get_global_platform_registry().get_codegen_platform()
476
- model.set_suppress_output(True)
477
- success, response = self._call_model_with_retry(model, prompt)
478
- if not success:
479
- return "Update code changes"
480
-
481
- # 清理响应内容
482
- return response.strip().split("\n")[0]
483
-
484
122
  def _finalize_changes(self, feature: str) -> None:
485
123
  """完成修改并提交"""
486
124
  PrettyOutput.print("修改确认成功,提交修改", OutputType.INFO)
@@ -492,7 +130,7 @@ class OldModel:
492
130
  git_diff = os.popen("git diff --cached").read()
493
131
 
494
132
  # 自动生成commit信息,传入feature
495
- commit_message = self._generate_commit_message(git_diff, feature)
133
+ commit_message = generate_commit_message(git_diff, feature)
496
134
 
497
135
  # 显示并确认commit信息
498
136
  PrettyOutput.print(f"自动生成的commit信息: {commit_message}", OutputType.INFO)
@@ -503,7 +141,7 @@ class OldModel:
503
141
 
504
142
  # 不需要再次 git add,因为已经添加过了
505
143
  os.system(f"git commit -m '{commit_message}'")
506
- self._save_edit_record(commit_message, git_diff)
144
+ save_edit_record(self.record_dir, commit_message, git_diff)
507
145
 
508
146
  def _revert_changes(self) -> None:
509
147
  """回退所有修改"""
@@ -519,26 +157,36 @@ class OldModel:
519
157
 
520
158
  Returns:
521
159
  Dict[str, Any]: 包含执行结果的字典
522
- - success: 是否成功
523
- - stdout: 标准输出信息
524
- - stderr: 错误信息
525
- - error: 错误对象(如果有)
526
160
  """
527
161
  try:
528
162
  self._prepare_execution()
529
163
  related_files = self._load_related_files(feature)
530
- patches = self._make_patch(related_files, feature)
531
- return self._handle_patch_application(related_files, patches, feature)
164
+
165
+ patch_handler = PatchHandler(self.main_model)
166
+ if patch_handler.handle_patch_application(related_files, feature):
167
+ self._finalize_changes(feature)
168
+ return {
169
+ "success": True,
170
+ "stdout": "代码修改成功",
171
+ "stderr": "",
172
+ }
173
+ else:
174
+ self._revert_changes()
175
+ return {
176
+ "success": False,
177
+ "stdout": "",
178
+ "stderr": "代码修改失败,请修改需求后重试",
179
+ }
532
180
 
533
181
  except Exception as e:
182
+ self._revert_changes()
534
183
  return {
535
184
  "success": False,
536
185
  "stdout": "",
537
- "stderr": f"执行失败: {str(e)}",
186
+ "stderr": f"执行失败: {str(e)},请修改需求后重试",
538
187
  "error": e
539
188
  }
540
189
 
541
-
542
190
  def main():
543
191
  """命令行入口"""
544
192
  import argparse
@@ -550,7 +198,6 @@ def main():
550
198
  parser.add_argument('-l', '--language', help='编程语言', default="python")
551
199
  args = parser.parse_args()
552
200
 
553
-
554
201
  tool = JarvisCoder(args.dir, args.language)
555
202
 
556
203
  # 循环处理需求
@@ -569,16 +216,21 @@ def main():
569
216
  if result["success"]:
570
217
  PrettyOutput.print(result["stdout"], OutputType.SUCCESS)
571
218
  else:
572
- if result["stderr"]:
219
+ if result.get("stderr"):
573
220
  PrettyOutput.print(result["stderr"], OutputType.WARNING)
574
- if result["error"]:
575
- PrettyOutput.print(f"错误类型: {type(result['error']).__name__}", OutputType.WARNING)
221
+ if result.get("error"): # 使用 get() 方法避免 KeyError
222
+ error = result["error"]
223
+ PrettyOutput.print(f"错误类型: {type(error).__name__}", OutputType.WARNING)
224
+ PrettyOutput.print(f"错误信息: {str(error)}", OutputType.WARNING)
225
+ # 提示用户可以继续输入
226
+ PrettyOutput.print("\n您可以修改需求后重试", OutputType.INFO)
576
227
 
577
228
  except KeyboardInterrupt:
578
229
  print("\n用户中断执行")
579
230
  break
580
231
  except Exception as e:
581
232
  PrettyOutput.print(f"执行出错: {str(e)}", OutputType.ERROR)
233
+ PrettyOutput.print("\n您可以修改需求后重试", OutputType.INFO)
582
234
  continue
583
235
 
584
236
  return 0
@@ -703,4 +355,4 @@ def get_multiline_input(prompt_text: str, root_dir: str = None) -> str:
703
355
  except EOFError:
704
356
  pass
705
357
 
706
- return "\n".join(lines)
358
+ return "\n".join(lines)
@@ -0,0 +1,30 @@
1
+ from typing import Tuple
2
+ import time
3
+ from jarvis.models.base import BasePlatform
4
+ from jarvis.utils import OutputType, PrettyOutput
5
+
6
+ def call_model_with_retry(model: BasePlatform, prompt: str, max_retries: int = 3, initial_delay: float = 1.0) -> Tuple[bool, str]:
7
+ """调用模型并支持重试
8
+
9
+ Args:
10
+ model: 模型实例
11
+ prompt: 提示词
12
+ max_retries: 最大重试次数
13
+ initial_delay: 初始延迟时间(秒)
14
+
15
+ Returns:
16
+ Tuple[bool, str]: (是否成功, 响应内容)
17
+ """
18
+ delay = initial_delay
19
+ for attempt in range(max_retries):
20
+ try:
21
+ response = model.chat(prompt)
22
+ return True, response
23
+ except Exception as e:
24
+ if attempt == max_retries - 1: # 最后一次尝试
25
+ PrettyOutput.print(f"调用模型失败: {str(e)}", OutputType.ERROR)
26
+ return False, str(e)
27
+
28
+ PrettyOutput.print(f"调用模型失败,{delay}秒后重试: {str(e)}", OutputType.WARNING)
29
+ time.sleep(delay)
30
+ delay *= 2 # 指数退避
@@ -0,0 +1,390 @@
1
+ import re
2
+ import os
3
+ from typing import List, Tuple, Dict
4
+ from jarvis.utils import OutputType, PrettyOutput
5
+ from .model_utils import call_model_with_retry
6
+
7
+ class PatchHandler:
8
+ def __init__(self, model):
9
+ self.model = model
10
+
11
+ def _extract_patches(self, response: str) -> List[Tuple[str, str, str]]:
12
+ """从响应中提取补丁
13
+
14
+ Args:
15
+ response: 模型响应内容
16
+
17
+ Returns:
18
+ List[Tuple[str, str, str]]: 补丁列表,每个补丁是 (格式, 文件路径, 补丁内容) 的元组
19
+ """
20
+ patches = []
21
+
22
+ # 匹配两种格式的补丁
23
+ fmt1_patches = re.finditer(r'<PATCH_FMT1>\n?(.*?)\n?</PATCH_FMT1>', response, re.DOTALL)
24
+ fmt2_patches = re.finditer(r'<PATCH_FMT2>\n?(.*?)\n?</PATCH_FMT2>', response, re.DOTALL)
25
+
26
+ # 处理 FMT1 格式的补丁
27
+ for match in fmt1_patches:
28
+ patch_content = match.group(1)
29
+ if not patch_content:
30
+ continue
31
+
32
+ # 提取文件路径和补丁内容
33
+ lines = patch_content.split('\n')
34
+ file_path_match = re.search(r'> (.*)', lines[0])
35
+ if not file_path_match:
36
+ continue
37
+
38
+ file_path = file_path_match.group(1).strip()
39
+ patch_content = '\n'.join(lines[1:])
40
+ patches.append(("FMT1", file_path, patch_content))
41
+
42
+ # 处理 FMT2 格式的补丁
43
+ for match in fmt2_patches:
44
+ patch_content = match.group(1)
45
+ if not patch_content:
46
+ continue
47
+
48
+ # 提取文件路径和补丁内容
49
+ lines = patch_content.split('\n')
50
+ file_path_match = re.search(r'> (.*)', lines[0])
51
+ if not file_path_match:
52
+ continue
53
+
54
+ file_path = file_path_match.group(1).strip()
55
+ patch_content = '\n'.join(lines[1:])
56
+ patches.append(("FMT2", file_path, patch_content))
57
+
58
+ return patches
59
+
60
+ def make_patch(self, related_files: List[Dict], feature: str) -> List[Tuple[str, str, str]]:
61
+ """生成修改方案"""
62
+ prompt = """你是一个资深程序员,请根据需求描述,修改文件内容。
63
+
64
+ 修改格式说明:
65
+ 1. 第一种格式 - 完整代码块替换:
66
+ <PATCH_FMT1>
67
+ > path/to/file
68
+ old_content
69
+ @@@@@@
70
+ new_content
71
+ </PATCH_FMT1>
72
+
73
+ 例:
74
+ <PATCH_FMT1>
75
+ > src/main.py
76
+ def old_function():
77
+ print("old code")
78
+ return False
79
+ @@@@@@
80
+ def old_function():
81
+ print("new code")
82
+ return True
83
+ </PATCH_FMT1>
84
+
85
+ 2. 第二种格式 - 通过首尾行定位要修改的代码范围:
86
+ <PATCH_FMT2>
87
+ > path/to/file
88
+ start_line_content
89
+ end_line_content
90
+ new_content
91
+ ...
92
+ </PATCH_FMT2>
93
+
94
+ 例:
95
+ <PATCH_FMT2>
96
+ > src/main.py
97
+ def old_function():
98
+ return False
99
+ def new_function():
100
+ print("new code")
101
+ return True
102
+ </PATCH_FMT2>
103
+
104
+ 例子中 `def old_function():` 是首行内容,`return False` 是尾行内容,第三行开始是新的代码内容,将替换第一行到最后一行之间的所有内容
105
+
106
+ 注意事项:
107
+ 1、仅输出补丁内容,不要输出任何其他内容
108
+ 2、如果在大段代码中有零星修改,生成多个补丁
109
+ 3、要替换的内容,一定要与文件内容完全一致,不要有任何多余或者缺失的内容
110
+ 4、每个patch不超过20行,超出20行,请生成多个patch
111
+ 5、务必保留原始文件的缩进和格式
112
+ 6、优先使用第二种格式(PATCH_FMT2),因为它更准确地定位要修改的代码范围
113
+ 7、第二种格式(PATCH_FMT2)的前两行必须完全匹配文件中要修改的代码块的首尾行
114
+ 8、如果第二种格式无法准确定位到要修改的代码(比如有重复的行),请使用第一种格式(PATCH_FMT1)
115
+ """
116
+ # 添加文件内容到提示
117
+ for i, file in enumerate(related_files):
118
+ prompt += f"""\n{i}. {file["file_path"]}\n"""
119
+ prompt += f"""文件内容:\n"""
120
+ prompt += f"<FILE_CONTENT>\n"
121
+ prompt += f'{file["file_content"]}\n'
122
+ prompt += f"</FILE_CONTENT>\n"
123
+
124
+ prompt += f"\n需求描述: {feature}\n"
125
+
126
+ # 调用模型生成补丁
127
+ success, response = call_model_with_retry(self.model, prompt)
128
+ if not success:
129
+ PrettyOutput.print("生成补丁失败", OutputType.ERROR)
130
+ return []
131
+
132
+ try:
133
+ patches = self._extract_patches(response)
134
+
135
+ if not patches:
136
+ PrettyOutput.print("未生成任何有效补丁", OutputType.WARNING)
137
+ return []
138
+
139
+ PrettyOutput.print(f"生成了 {len(patches)} 个补丁", OutputType.SUCCESS)
140
+ return patches
141
+
142
+ except Exception as e:
143
+ PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.WARNING)
144
+ return []
145
+
146
+ def apply_patch(self, related_files: List[Dict], patches: List[Tuple[str, str, str]]) -> Tuple[bool, str]:
147
+ """应用补丁
148
+
149
+ Args:
150
+ related_files: 相关文件列表
151
+ patches: 补丁列表,每个补丁是 (格式, 文件路径, 补丁内容) 的元组
152
+
153
+ Returns:
154
+ Tuple[bool, str]: (是否成功, 错误信息)
155
+ """
156
+ error_info = []
157
+ modified_files = set()
158
+
159
+ # 创建文件内容映射
160
+ file_map = {file["file_path"]: file["file_content"] for file in related_files}
161
+ temp_map = file_map.copy() # 创建临时映射用于尝试应用
162
+
163
+ # 尝试应用所有补丁
164
+ for i, (fmt, file_path, patch_content) in enumerate(patches):
165
+ PrettyOutput.print(f"正在应用补丁 {i+1}/{len(patches)}", OutputType.INFO)
166
+
167
+ try:
168
+ # 处理文件修改
169
+ if file_path not in temp_map:
170
+ error_info.append(f"文件不存在: {file_path}")
171
+ return False, "\n".join(error_info)
172
+
173
+ current_content = temp_map[file_path]
174
+
175
+ if fmt == "FMT1": # 完整代码块替换格式
176
+ parts = patch_content.split("@@@@@@")
177
+ if len(parts) != 2:
178
+ error_info.append(f"FMT1补丁格式错误: {file_path},缺少分隔符")
179
+ return False, "\n".join(error_info)
180
+
181
+ old_content, new_content = parts
182
+
183
+ # 处理新文件
184
+ if not old_content:
185
+ temp_map[file_path] = new_content
186
+ modified_files.add(file_path)
187
+ continue
188
+
189
+ # 查找并替换代码块
190
+ if old_content not in current_content:
191
+ error_info.append(
192
+ f"补丁应用失败: {file_path}\n"
193
+ f"原因: 未找到要替换的代码\n"
194
+ f"期望找到的代码:\n{old_content}\n"
195
+ f"实际文件内容:\n{current_content[:200]}..."
196
+ )
197
+ return False, "\n".join(error_info)
198
+
199
+ # 应用更改
200
+ temp_map[file_path] = current_content.replace(old_content, new_content)
201
+
202
+ else: # FMT2 - 首尾行定位格式
203
+ lines = patch_content.splitlines()
204
+ if len(lines) < 3:
205
+ error_info.append(f"FMT2补丁格式错误: {file_path},行数不足")
206
+ return False, "\n".join(error_info)
207
+
208
+ first_line = lines[0]
209
+ last_line = lines[1]
210
+ new_content = '\n'.join(lines[2:])
211
+
212
+ # 在文件内容中定位要替换的区域
213
+ content_lines = current_content.splitlines()
214
+ start_idx = -1
215
+ end_idx = -1
216
+
217
+ # 查找匹配的起始行和结束行
218
+ for idx, line in enumerate(content_lines):
219
+ if line.rstrip() == first_line.rstrip():
220
+ start_idx = idx
221
+ if start_idx != -1 and line.rstrip() == last_line.rstrip():
222
+ end_idx = idx
223
+ break
224
+
225
+ if start_idx == -1 or end_idx == -1:
226
+ error_info.append(
227
+ f"补丁应用失败: {file_path}\n"
228
+ f"原因: 未找到匹配的代码范围\n"
229
+ f"起始行: {first_line}\n"
230
+ f"结束行: {last_line}"
231
+ )
232
+ return False, "\n".join(error_info)
233
+
234
+ # 替换内容
235
+ content_lines[start_idx:end_idx + 1] = new_content.splitlines()
236
+ temp_map[file_path] = "\n".join(content_lines)
237
+
238
+ modified_files.add(file_path)
239
+
240
+ except Exception as e:
241
+ error_info.append(f"处理补丁时发生错误: {str(e)}")
242
+ return False, "\n".join(error_info)
243
+
244
+ # 所有补丁都应用成功,更新实际文件
245
+ for file_path in modified_files:
246
+ try:
247
+ dir_path = os.path.dirname(file_path)
248
+ if dir_path and not os.path.exists(dir_path):
249
+ os.makedirs(dir_path, exist_ok=True)
250
+
251
+ with open(file_path, "w", encoding="utf-8") as f:
252
+ f.write(temp_map[file_path])
253
+
254
+ PrettyOutput.print(f"成功修改文件: {file_path}", OutputType.SUCCESS)
255
+
256
+ except Exception as e:
257
+ error_info.append(f"写入文件失败 {file_path}: {str(e)}")
258
+ return False, "\n".join(error_info)
259
+
260
+ return True, ""
261
+
262
+ def handle_patch_feedback(self, error_msg: str, feature: str) -> List[Tuple[str, str, str]]:
263
+ """处理补丁应用失败的反馈
264
+
265
+ Args:
266
+ error_msg: 错误信息
267
+ feature: 功能描述
268
+
269
+ Returns:
270
+ List[Tuple[str, str, str]]: 新的补丁列表
271
+ """
272
+ PrettyOutput.print("补丁应用失败,尝试重新生成", OutputType.WARNING)
273
+
274
+ # 获取用户补充信息
275
+ additional_info = input("\n请输入补充信息(直接回车跳过):")
276
+ PrettyOutput.print(f"开始重新生成补丁", OutputType.INFO)
277
+
278
+ # 构建重试提示
279
+ retry_prompt = f"""补丁应用失败,请根据以下信息重新生成补丁:
280
+
281
+ 错误信息:
282
+ {error_msg}
283
+
284
+ 原始需求:
285
+ {feature}
286
+
287
+ 用户补充信息:
288
+ {additional_info}
289
+
290
+ 请重新生成补丁,确保:
291
+ 1. 代码匹配完全准确
292
+ 2. 保持正确的缩进和格式
293
+ 3. 避免之前的错误
294
+ """
295
+ success, response = call_model_with_retry(self.model, retry_prompt)
296
+ if not success:
297
+ return []
298
+
299
+ try:
300
+ patches = self._extract_patches(response)
301
+ return patches
302
+
303
+ except Exception as e:
304
+ PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.WARNING)
305
+ return []
306
+
307
+ def monitor_patch_result(self, success: bool, error_msg: str) -> bool:
308
+ """监控补丁应用结果
309
+
310
+ Args:
311
+ success: 是否成功
312
+ error_msg: 错误信息
313
+
314
+ Returns:
315
+ bool: 是否继续尝试
316
+ """
317
+ if success:
318
+ PrettyOutput.print("补丁应用成功", OutputType.SUCCESS)
319
+ return False
320
+
321
+ PrettyOutput.print(f"补丁应用失败: {error_msg}", OutputType.ERROR)
322
+
323
+ # 询问是否继续尝试
324
+ retry = input("\n是否重新尝试?(y/n) [y]: ").lower() or "y"
325
+ return retry == "y"
326
+
327
+ def handle_patch_application(self, related_files: List[Dict], feature: str) -> bool:
328
+ """处理补丁应用流程
329
+
330
+ Args:
331
+ related_files: 相关文件列表
332
+ feature: 功能描述
333
+
334
+ Returns:
335
+ bool: 是否成功应用补丁
336
+ """
337
+ max_attempts = 3
338
+ attempt = 0
339
+
340
+ while attempt < max_attempts:
341
+ attempt += 1
342
+
343
+ while True: # 在当前尝试中循环,直到成功或用户放弃
344
+ # 1. 生成补丁
345
+ patches = self.make_patch(related_files, feature)
346
+ if not patches:
347
+ return False
348
+
349
+ # 2. 显示补丁内容
350
+ PrettyOutput.print("\n将要应用以下补丁:", OutputType.INFO)
351
+ for fmt, file_path, patch_content in patches:
352
+ PrettyOutput.print(f"\n文件: {file_path}", OutputType.INFO)
353
+ PrettyOutput.print(f"格式: {fmt}", OutputType.INFO)
354
+ PrettyOutput.print("补丁内容:", OutputType.INFO)
355
+ print(patch_content)
356
+
357
+ # 3. 应用补丁
358
+ success, error_msg = self.apply_patch(related_files, patches)
359
+ if not success:
360
+ # 4. 如果应用失败,询问是否重试
361
+ should_retry = self.monitor_patch_result(success, error_msg)
362
+ if not should_retry:
363
+ break # 退出内层循环,尝试下一次完整的迭代
364
+
365
+ # 5. 处理失败反馈
366
+ patches = self.handle_patch_feedback(error_msg, feature)
367
+ if not patches:
368
+ return False
369
+ continue # 继续当前迭代
370
+
371
+ # 6. 应用成功,让用户确认修改
372
+ PrettyOutput.print("\n补丁已应用,请检查修改效果。", OutputType.SUCCESS)
373
+ confirm = input("\n是否保留这些修改?(y/n) [y]: ").lower() or "y"
374
+ if confirm != "y":
375
+ PrettyOutput.print("用户取消修改,正在回退", OutputType.WARNING)
376
+ os.system("git reset --hard") # 回退所有修改
377
+
378
+ # 询问是否要在当前迭代中重试
379
+ retry = input("\n是否要重新生成补丁?(y/n) [y]: ").lower() or "y"
380
+ if retry != "y":
381
+ break # 退出内层循环,尝试下一次完整的迭代
382
+ continue # 继续当前迭代
383
+
384
+ return True # 用户确认修改,返回成功
385
+
386
+ # 如果内层循环正常退出(非return),继续外层循环
387
+ continue
388
+
389
+ PrettyOutput.print(f"达到最大重试次数 ({max_attempts})", OutputType.WARNING)
390
+ return False
jarvis/main.py CHANGED
@@ -132,7 +132,7 @@ def main():
132
132
  user_input = get_multiline_input("请输入您的任务(输入空行退出):")
133
133
  if not user_input or user_input == "__interrupt__":
134
134
  break
135
- agent.run(user_input, args.files, keep_history=args.keep_history)
135
+ agent.run(user_input, args.files)
136
136
  except Exception as e:
137
137
  PrettyOutput.print(f"错误: {str(e)}", OutputType.ERROR)
138
138
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: jarvis-ai-assistant
3
- Version: 0.1.87
3
+ Version: 0.1.89
4
4
  Summary: Jarvis: An AI assistant that uses tools to interact with the system
5
5
  Home-page: https://github.com/skyfireitdiy/Jarvis
6
6
  Author: skyfire
@@ -1,11 +1,14 @@
1
- jarvis/__init__.py,sha256=Qu_hhtdNbd4dzRrFbg7RZnEUuZ1vjkIa45ZGmskpboY,50
1
+ jarvis/__init__.py,sha256=_3MqMWMf232eVTukktJrSn128MkK9jyds0HMhYHti94,50
2
2
  jarvis/agent.py,sha256=_qh4mSojAgClOEz5pTiRIfRJU-5_3QGzBAU09roCjtk,19095
3
- jarvis/main.py,sha256=ksZkJzqc4oow6wB-7QbGJLejGblrbZtRI3fdciS5DS4,5455
3
+ jarvis/main.py,sha256=72od8757A3bhe0ncE38S7u-YZsAh0y50w9ozMhgqIU8,5423
4
4
  jarvis/utils.py,sha256=Y5zig7AgIzdWHF31qHaMUziezythfjVKjxFRtMzd1m4,10357
5
5
  jarvis/jarvis_codebase/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- jarvis/jarvis_codebase/main.py,sha256=nlh0zkJhJfA8jaENV8wNo9OAXCeEKX1lbGzKOBjkzV4,26518
6
+ jarvis/jarvis_codebase/main.py,sha256=hYdDwREfFW5KPbIbLZ75MFCth6VqrwxSeNfXiQavdzE,26473
7
7
  jarvis/jarvis_coder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- jarvis/jarvis_coder/main.py,sha256=Rdt9w_uGamDHN-jjZXtHdUNGHa0q2fbGe9R9Ay6XXe0,25431
8
+ jarvis/jarvis_coder/git_utils.py,sha256=tJ25kIzglGaPBqu42rZZSsXk0tpOFTiaG8q-bq4CSF0,2343
9
+ jarvis/jarvis_coder/main.py,sha256=5uxexKziA5kMZLNkGPEzz8hKQvarKVflNqRcXZVWnT8,13406
10
+ jarvis/jarvis_coder/model_utils.py,sha256=XXg5ZPlgRsq9K6iJb4vPoZqSiAJbAUIZffmgaLFnLCw,1104
11
+ jarvis/jarvis_coder/patch_handler.py,sha256=U8I19Aq2U3LAU3FE94C3tlFQNtGYA_luwnBU7OnX7pw,15253
9
12
  jarvis/jarvis_rag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
13
  jarvis/jarvis_rag/main.py,sha256=a8TtPVCh5Xd6W1AaRFGeXvU_1hEnHQdoMElxnMuq0ew,24773
11
14
  jarvis/jarvis_smart_shell/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -32,9 +35,9 @@ jarvis/tools/search.py,sha256=c9dXtyICdl8Lm8shNPNyIx9k67uY0rMF8xnIKu2RsnE,8787
32
35
  jarvis/tools/shell.py,sha256=UPKshPyOaUwTngresUw-ot1jHjQIb4wCY5nkJqa38lU,2520
33
36
  jarvis/tools/sub_agent.py,sha256=rEtAmSVY2ZjFOZEKr5m5wpACOQIiM9Zr_3dT92FhXYU,2621
34
37
  jarvis/tools/webpage.py,sha256=d3w3Jcjcu1ESciezTkz3n3Zf-rp_l91PrVoDEZnckOo,2391
35
- jarvis_ai_assistant-0.1.87.dist-info/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
36
- jarvis_ai_assistant-0.1.87.dist-info/METADATA,sha256=1W0ZGe3z8R5w--7LJhwc5KGb_HpLrN-CUkgAOp-d41g,12589
37
- jarvis_ai_assistant-0.1.87.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
38
- jarvis_ai_assistant-0.1.87.dist-info/entry_points.txt,sha256=sdmIO86MrIUepJTGyHs0i_Ho9VGf1q9YRP4RgQvGWcI,280
39
- jarvis_ai_assistant-0.1.87.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
40
- jarvis_ai_assistant-0.1.87.dist-info/RECORD,,
38
+ jarvis_ai_assistant-0.1.89.dist-info/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
39
+ jarvis_ai_assistant-0.1.89.dist-info/METADATA,sha256=aG0WNMCBnK4O5fPKrNiq7Nj3wlsU5mx2xy1tQUSlmAU,12589
40
+ jarvis_ai_assistant-0.1.89.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
41
+ jarvis_ai_assistant-0.1.89.dist-info/entry_points.txt,sha256=sdmIO86MrIUepJTGyHs0i_Ho9VGf1q9YRP4RgQvGWcI,280
42
+ jarvis_ai_assistant-0.1.89.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
43
+ jarvis_ai_assistant-0.1.89.dist-info/RECORD,,