jarvis-ai-assistant 0.1.97__py3-none-any.whl → 0.1.99__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 +1 -1
- jarvis/agent.py +199 -157
- jarvis/jarvis_code_agent/__init__.py +0 -0
- jarvis/jarvis_code_agent/main.py +203 -0
- jarvis/jarvis_codebase/main.py +412 -284
- jarvis/jarvis_coder/file_select.py +209 -0
- jarvis/jarvis_coder/git_utils.py +81 -19
- jarvis/jarvis_coder/main.py +68 -446
- jarvis/jarvis_coder/patch_handler.py +117 -47
- jarvis/jarvis_coder/plan_generator.py +69 -27
- jarvis/jarvis_platform/main.py +38 -38
- jarvis/jarvis_rag/main.py +189 -189
- jarvis/jarvis_smart_shell/main.py +22 -24
- jarvis/models/base.py +6 -1
- jarvis/models/ollama.py +2 -2
- jarvis/models/registry.py +3 -6
- jarvis/tools/ask_user.py +6 -6
- jarvis/tools/codebase_qa.py +5 -7
- jarvis/tools/create_code_sub_agent.py +55 -0
- jarvis/tools/{sub_agent.py → create_sub_agent.py} +4 -1
- jarvis/tools/execute_code_modification.py +72 -0
- jarvis/tools/{file_ops.py → file_operation.py} +13 -14
- jarvis/tools/find_related_files.py +86 -0
- jarvis/tools/methodology.py +25 -25
- jarvis/tools/rag.py +32 -32
- jarvis/tools/registry.py +72 -36
- jarvis/tools/search.py +1 -1
- jarvis/tools/select_code_files.py +64 -0
- jarvis/utils.py +153 -49
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/METADATA +1 -1
- jarvis_ai_assistant-0.1.99.dist-info/RECORD +52 -0
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/entry_points.txt +2 -1
- jarvis/main.py +0 -155
- jarvis/tools/coder.py +0 -69
- jarvis_ai_assistant-0.1.97.dist-info/RECORD +0 -47
- /jarvis/tools/{shell.py → execute_shell.py} +0 -0
- /jarvis/tools/{generator.py → generate_tool.py} +0 -0
- /jarvis/tools/{webpage.py → read_webpage.py} +0 -0
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/top_level.txt +0 -0
jarvis/jarvis_coder/main.py
CHANGED
|
@@ -3,6 +3,7 @@ import threading
|
|
|
3
3
|
from typing import Dict, Any, List, Optional
|
|
4
4
|
import re
|
|
5
5
|
|
|
6
|
+
from jarvis.jarvis_coder.file_select import select_files
|
|
6
7
|
from jarvis.utils import OutputType, PrettyOutput, find_git_root, get_max_context_length, is_long_context, load_env_from_file, while_success
|
|
7
8
|
from jarvis.models.registry import PlatformRegistry
|
|
8
9
|
from jarvis.jarvis_codebase.main import CodeBase
|
|
@@ -12,7 +13,7 @@ from prompt_toolkit.formatted_text import FormattedText
|
|
|
12
13
|
from prompt_toolkit.styles import Style
|
|
13
14
|
import fnmatch
|
|
14
15
|
from .patch_handler import PatchHandler
|
|
15
|
-
from .git_utils import generate_commit_message, save_edit_record
|
|
16
|
+
from .git_utils import generate_commit_message, init_git_repo, save_edit_record
|
|
16
17
|
from .plan_generator import PlanGenerator
|
|
17
18
|
|
|
18
19
|
# 全局锁对象
|
|
@@ -20,484 +21,120 @@ index_lock = threading.Lock()
|
|
|
20
21
|
|
|
21
22
|
class JarvisCoder:
|
|
22
23
|
def __init__(self, root_dir: str, language: Optional[str] = "python"):
|
|
23
|
-
"""
|
|
24
|
+
"""Initialize code modification tool"""
|
|
24
25
|
self.root_dir = root_dir
|
|
25
26
|
self.language = language
|
|
26
27
|
self._init_directories()
|
|
27
28
|
self._init_codebase()
|
|
28
29
|
|
|
29
30
|
def _init_directories(self):
|
|
30
|
-
"""
|
|
31
|
+
"""Initialize directories"""
|
|
31
32
|
self.max_context_length = get_max_context_length()
|
|
32
|
-
|
|
33
|
-
root_dir = find_git_root(self.root_dir)
|
|
34
|
-
if not root_dir:
|
|
35
|
-
root_dir = self.root_dir
|
|
36
|
-
|
|
37
|
-
self.root_dir = root_dir
|
|
38
|
-
|
|
39
|
-
PrettyOutput.print(f"Git根目录: {self.root_dir}", OutputType.INFO)
|
|
40
|
-
|
|
41
|
-
# 1. 判断代码库路径是否存在,如果不存在,创建
|
|
42
|
-
if not os.path.exists(self.root_dir):
|
|
43
|
-
PrettyOutput.print(
|
|
44
|
-
"Root directory does not exist, creating...", OutputType.INFO)
|
|
45
|
-
os.makedirs(self.root_dir)
|
|
46
|
-
|
|
47
|
-
os.chdir(self.root_dir)
|
|
48
|
-
|
|
49
|
-
# 2. 创建 .jarvis-coder 目录
|
|
50
|
-
self.jarvis_dir = os.path.join(self.root_dir, ".jarvis-coder")
|
|
51
|
-
if not os.path.exists(self.jarvis_dir):
|
|
52
|
-
os.makedirs(self.jarvis_dir)
|
|
53
|
-
|
|
54
|
-
self.record_dir = os.path.join(self.jarvis_dir, "record")
|
|
55
|
-
if not os.path.exists(self.record_dir):
|
|
56
|
-
os.makedirs(self.record_dir)
|
|
57
|
-
|
|
58
|
-
# 3. 处理 .gitignore 文件
|
|
59
|
-
gitignore_path = os.path.join(self.root_dir, ".gitignore")
|
|
60
|
-
gitignore_modified = False
|
|
61
|
-
jarvis_ignore_pattern = ".jarvis-*"
|
|
62
|
-
|
|
63
|
-
# 3.1 如果 .gitignore 不存在,创建它
|
|
64
|
-
if not os.path.exists(gitignore_path):
|
|
65
|
-
PrettyOutput.print("创建 .gitignore 文件", OutputType.INFO)
|
|
66
|
-
with open(gitignore_path, "w", encoding="utf-8") as f:
|
|
67
|
-
f.write(f"{jarvis_ignore_pattern}\n")
|
|
68
|
-
gitignore_modified = True
|
|
69
|
-
else:
|
|
70
|
-
# 3.2 检查是否已经包含 .jarvis-* 模式
|
|
71
|
-
with open(gitignore_path, "r", encoding="utf-8") as f:
|
|
72
|
-
content = f.read()
|
|
73
|
-
|
|
74
|
-
# 检查是否需要添加 .jarvis-* 模式
|
|
75
|
-
if jarvis_ignore_pattern not in content.split("\n"):
|
|
76
|
-
PrettyOutput.print("将 .jarvis-* 添加到 .gitignore", OutputType.INFO)
|
|
77
|
-
with open(gitignore_path, "a", encoding="utf-8") as f:
|
|
78
|
-
# 确保文件以换行符结尾
|
|
79
|
-
if not content.endswith("\n"):
|
|
80
|
-
f.write("\n")
|
|
81
|
-
f.write(f"{jarvis_ignore_pattern}\n")
|
|
82
|
-
gitignore_modified = True
|
|
83
|
-
|
|
84
|
-
# 4. 判断代码库是否是git仓库,如果不是,初始化git仓库
|
|
85
|
-
if not os.path.exists(os.path.join(self.root_dir, ".git")):
|
|
86
|
-
PrettyOutput.print("初始化 Git 仓库", OutputType.INFO)
|
|
87
|
-
os.system("git init")
|
|
88
|
-
os.system("git add .")
|
|
89
|
-
os.system("git commit -m 'Initial commit'")
|
|
90
|
-
# 5. 如果修改了 .gitignore,提交更改
|
|
91
|
-
elif gitignore_modified:
|
|
92
|
-
PrettyOutput.print("提交 .gitignore 更改", OutputType.INFO)
|
|
93
|
-
os.system("git add .gitignore")
|
|
94
|
-
os.system("git commit -m 'chore: update .gitignore to exclude .jarvis-* files'")
|
|
95
|
-
# 6. 查看代码库是否有未提交的文件,如果有,提交一次
|
|
96
|
-
elif self._has_uncommitted_files():
|
|
97
|
-
PrettyOutput.print("提交未保存的更改", OutputType.INFO)
|
|
98
|
-
os.system("git add .")
|
|
99
|
-
git_diff = os.popen("git diff --cached").read()
|
|
100
|
-
commit_message = generate_commit_message(git_diff)
|
|
101
|
-
os.system(f"git commit -m '{commit_message}'")
|
|
33
|
+
self.root_dir = init_git_repo(self.root_dir)
|
|
102
34
|
|
|
103
35
|
def _init_codebase(self):
|
|
104
|
-
"""
|
|
36
|
+
"""Initialize codebase"""
|
|
105
37
|
self._codebase = CodeBase(self.root_dir)
|
|
106
38
|
|
|
107
|
-
def _has_uncommitted_files(self) -> bool:
|
|
108
|
-
"""判断代码库是否有未提交的文件"""
|
|
109
|
-
# 获取未暂存的修改
|
|
110
|
-
unstaged = os.popen("git diff --name-only").read()
|
|
111
|
-
# 获取已暂存但未提交的修改
|
|
112
|
-
staged = os.popen("git diff --cached --name-only").read()
|
|
113
|
-
# 获取未跟踪的文件
|
|
114
|
-
untracked = os.popen("git ls-files --others --exclude-standard").read()
|
|
115
|
-
|
|
116
|
-
return bool(unstaged or staged or untracked)
|
|
117
|
-
|
|
118
|
-
def _prepare_execution(self) -> None:
|
|
119
|
-
"""准备执行环境"""
|
|
120
|
-
self._codebase.generate_codebase()
|
|
121
|
-
|
|
122
39
|
|
|
123
|
-
def _load_related_files(self, feature: str) -> List[
|
|
124
|
-
"""
|
|
40
|
+
def _load_related_files(self, feature: str) -> List[str]:
|
|
41
|
+
"""Load related file content"""
|
|
125
42
|
ret = []
|
|
126
|
-
#
|
|
43
|
+
# Ensure the index database is generated
|
|
127
44
|
if not self._codebase.is_index_generated():
|
|
128
|
-
PrettyOutput.print("
|
|
45
|
+
PrettyOutput.print("Index database not generated, generating...", OutputType.WARNING)
|
|
129
46
|
self._codebase.generate_codebase()
|
|
130
47
|
|
|
131
48
|
related_files = self._codebase.search_similar(feature)
|
|
132
|
-
for file
|
|
133
|
-
PrettyOutput.print(f"
|
|
134
|
-
|
|
135
|
-
ret.append({"file_path": file, "file_content": content})
|
|
49
|
+
for file in related_files:
|
|
50
|
+
PrettyOutput.print(f"Related file: {file}", OutputType.SUCCESS)
|
|
51
|
+
ret.append(file)
|
|
136
52
|
return ret
|
|
137
53
|
|
|
138
|
-
def _parse_file_selection(self, input_str: str, max_index: int) -> List[int]:
|
|
139
|
-
"""解析文件选择表达式
|
|
140
|
-
|
|
141
|
-
支持的格式:
|
|
142
|
-
- 单个数字: "1"
|
|
143
|
-
- 逗号分隔: "1,3,5"
|
|
144
|
-
- 范围: "1-5"
|
|
145
|
-
- 组合: "1,3-5,7"
|
|
146
|
-
|
|
147
|
-
Args:
|
|
148
|
-
input_str: 用户输入的选择表达式
|
|
149
|
-
max_index: 最大可选择的索引
|
|
150
|
-
|
|
151
|
-
Returns:
|
|
152
|
-
List[int]: 选中的索引列表(从0开始)
|
|
153
|
-
"""
|
|
154
|
-
selected = set()
|
|
155
|
-
|
|
156
|
-
# 移除所有空白字符
|
|
157
|
-
input_str = "".join(input_str.split())
|
|
158
|
-
|
|
159
|
-
# 处理逗号分隔的部分
|
|
160
|
-
for part in input_str.split(","):
|
|
161
|
-
if not part:
|
|
162
|
-
continue
|
|
163
|
-
|
|
164
|
-
# 处理范围(例如:3-6)
|
|
165
|
-
if "-" in part:
|
|
166
|
-
try:
|
|
167
|
-
start, end = map(int, part.split("-"))
|
|
168
|
-
# 转换为从0开始的索引
|
|
169
|
-
start = max(0, start - 1)
|
|
170
|
-
end = min(max_index, end - 1)
|
|
171
|
-
if start <= end:
|
|
172
|
-
selected.update(range(start, end + 1))
|
|
173
|
-
except ValueError:
|
|
174
|
-
PrettyOutput.print(f"忽略无效的范围表达式: {part}", OutputType.WARNING)
|
|
175
|
-
# 处理单个数字
|
|
176
|
-
else:
|
|
177
|
-
try:
|
|
178
|
-
index = int(part) - 1 # 转换为从0开始的索引
|
|
179
|
-
if 0 <= index < max_index:
|
|
180
|
-
selected.add(index)
|
|
181
|
-
else:
|
|
182
|
-
PrettyOutput.print(f"忽略超出范围的索引: {part}", OutputType.WARNING)
|
|
183
|
-
except ValueError:
|
|
184
|
-
PrettyOutput.print(f"忽略无效的数字: {part}", OutputType.WARNING)
|
|
185
|
-
|
|
186
|
-
return sorted(list(selected))
|
|
187
|
-
|
|
188
|
-
def _get_file_completer(self) -> Completer:
|
|
189
|
-
"""创建文件路径补全器"""
|
|
190
|
-
class FileCompleter(Completer):
|
|
191
|
-
def __init__(self, root_dir: str):
|
|
192
|
-
self.root_dir = root_dir
|
|
193
|
-
|
|
194
|
-
def get_completions(self, document, complete_event):
|
|
195
|
-
# 获取当前输入的文本
|
|
196
|
-
text = document.text_before_cursor
|
|
197
|
-
|
|
198
|
-
# 如果输入为空,返回根目录下的所有文件
|
|
199
|
-
if not text:
|
|
200
|
-
for path in self._list_files(""):
|
|
201
|
-
yield Completion(path, start_position=0)
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
# 获取当前目录和部分文件名
|
|
205
|
-
current_dir = os.path.dirname(text)
|
|
206
|
-
file_prefix = os.path.basename(text)
|
|
207
|
-
|
|
208
|
-
# 列出匹配的文件
|
|
209
|
-
search_dir = os.path.join(self.root_dir, current_dir) if current_dir else self.root_dir
|
|
210
|
-
if os.path.isdir(search_dir):
|
|
211
|
-
for path in self._list_files(current_dir):
|
|
212
|
-
if path.startswith(text):
|
|
213
|
-
yield Completion(path, start_position=-len(text))
|
|
214
|
-
|
|
215
|
-
def _list_files(self, current_dir: str) -> List[str]:
|
|
216
|
-
"""列出指定目录下的所有文件(递归)"""
|
|
217
|
-
files = []
|
|
218
|
-
search_dir = os.path.join(self.root_dir, current_dir)
|
|
219
|
-
|
|
220
|
-
for root, _, filenames in os.walk(search_dir):
|
|
221
|
-
for filename in filenames:
|
|
222
|
-
full_path = os.path.join(root, filename)
|
|
223
|
-
rel_path = os.path.relpath(full_path, self.root_dir)
|
|
224
|
-
# 忽略 .git 目录和其他隐藏文件
|
|
225
|
-
if not any(part.startswith('.') for part in rel_path.split(os.sep)):
|
|
226
|
-
files.append(rel_path)
|
|
227
|
-
|
|
228
|
-
return sorted(files)
|
|
229
54
|
|
|
230
|
-
return FileCompleter(self.root_dir)
|
|
231
|
-
|
|
232
|
-
def _fuzzy_match_files(self, pattern: str) -> List[str]:
|
|
233
|
-
"""模糊匹配文件路径
|
|
234
|
-
|
|
235
|
-
Args:
|
|
236
|
-
pattern: 匹配模式
|
|
237
|
-
|
|
238
|
-
Returns:
|
|
239
|
-
List[str]: 匹配的文件路径列表
|
|
240
|
-
"""
|
|
241
|
-
matches = []
|
|
242
|
-
|
|
243
|
-
# 将模式转换为正则表达式
|
|
244
|
-
pattern = pattern.replace('.', r'\.').replace('*', '.*').replace('?', '.')
|
|
245
|
-
pattern = f".*{pattern}.*" # 允许部分匹配
|
|
246
|
-
regex = re.compile(pattern, re.IGNORECASE)
|
|
247
|
-
|
|
248
|
-
# 遍历所有文件
|
|
249
|
-
for root, _, files in os.walk(self.root_dir):
|
|
250
|
-
for file in files:
|
|
251
|
-
full_path = os.path.join(root, file)
|
|
252
|
-
rel_path = os.path.relpath(full_path, self.root_dir)
|
|
253
|
-
# 忽略 .git 目录和其他隐藏文件
|
|
254
|
-
if not any(part.startswith('.') for part in rel_path.split(os.sep)):
|
|
255
|
-
if regex.match(rel_path):
|
|
256
|
-
matches.append(rel_path)
|
|
257
|
-
|
|
258
|
-
return sorted(matches)
|
|
259
|
-
|
|
260
|
-
def _select_files(self, related_files: List[Dict]) -> List[Dict]:
|
|
261
|
-
"""让用户选择和补充相关文件"""
|
|
262
|
-
PrettyOutput.section("相关文件", OutputType.INFO)
|
|
263
|
-
|
|
264
|
-
# 显示找到的文件
|
|
265
|
-
selected_files = list(related_files) # 默认全选
|
|
266
|
-
for i, file in enumerate(related_files, 1):
|
|
267
|
-
PrettyOutput.print(f"[{i}] {file['file_path']}", OutputType.INFO)
|
|
268
|
-
|
|
269
|
-
# 询问用户是否需要调整
|
|
270
|
-
user_input = input("\n是否需要调整文件列表?(y/n) [n]: ").strip().lower() or 'n'
|
|
271
|
-
if user_input == 'y':
|
|
272
|
-
# 让用户选择文件
|
|
273
|
-
PrettyOutput.print("\n请输入要包含的文件编号(支持: 1,3-6 格式,直接回车保持当前选择):", OutputType.INFO)
|
|
274
|
-
numbers = input(">>> ").strip()
|
|
275
|
-
if numbers:
|
|
276
|
-
selected_indices = self._parse_file_selection(numbers, len(related_files))
|
|
277
|
-
if selected_indices:
|
|
278
|
-
selected_files = [related_files[i] for i in selected_indices]
|
|
279
|
-
else:
|
|
280
|
-
PrettyOutput.print("未选择任何有效文件,保持原有选择", OutputType.WARNING)
|
|
281
|
-
|
|
282
|
-
# 询问是否需要补充文件
|
|
283
|
-
user_input = input("\n是否需要补充其他文件?(y/n) [n]: ").strip().lower() or 'n'
|
|
284
|
-
if user_input == 'y':
|
|
285
|
-
# 创建文件补全会话
|
|
286
|
-
session = PromptSession(
|
|
287
|
-
completer=self._get_file_completer(),
|
|
288
|
-
complete_while_typing=True
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
while True:
|
|
292
|
-
PrettyOutput.print("\n请输入要补充的文件路径(支持Tab补全和*?通配符,输入空行结束):", OutputType.INFO)
|
|
293
|
-
try:
|
|
294
|
-
file_path = session.prompt(">>> ").strip()
|
|
295
|
-
except KeyboardInterrupt:
|
|
296
|
-
break
|
|
297
|
-
|
|
298
|
-
if not file_path:
|
|
299
|
-
break
|
|
300
|
-
|
|
301
|
-
# 处理通配符匹配
|
|
302
|
-
if '*' in file_path or '?' in file_path:
|
|
303
|
-
matches = self._fuzzy_match_files(file_path)
|
|
304
|
-
if not matches:
|
|
305
|
-
PrettyOutput.print("未找到匹配的文件", OutputType.WARNING)
|
|
306
|
-
continue
|
|
307
|
-
|
|
308
|
-
# 显示匹配的文件
|
|
309
|
-
PrettyOutput.print("\n找到以下匹配的文件:", OutputType.INFO)
|
|
310
|
-
for i, path in enumerate(matches, 1):
|
|
311
|
-
PrettyOutput.print(f"[{i}] {path}", OutputType.INFO)
|
|
312
|
-
|
|
313
|
-
# 让用户选择
|
|
314
|
-
numbers = input("\n请选择要添加的文件编号(支持: 1,3-6 格式,直接回车全选): ").strip()
|
|
315
|
-
if numbers:
|
|
316
|
-
indices = self._parse_file_selection(numbers, len(matches))
|
|
317
|
-
if not indices:
|
|
318
|
-
continue
|
|
319
|
-
paths_to_add = [matches[i] for i in indices]
|
|
320
|
-
else:
|
|
321
|
-
paths_to_add = matches
|
|
322
|
-
else:
|
|
323
|
-
paths_to_add = [file_path]
|
|
324
|
-
|
|
325
|
-
# 添加选中的文件
|
|
326
|
-
for path in paths_to_add:
|
|
327
|
-
full_path = os.path.join(self.root_dir, path)
|
|
328
|
-
if not os.path.isfile(full_path):
|
|
329
|
-
PrettyOutput.print(f"文件不存在: {path}", OutputType.ERROR)
|
|
330
|
-
continue
|
|
331
|
-
|
|
332
|
-
try:
|
|
333
|
-
with open(full_path, "r", encoding="utf-8") as f:
|
|
334
|
-
content = f.read()
|
|
335
|
-
selected_files.append({
|
|
336
|
-
"file_path": path,
|
|
337
|
-
"file_content": content
|
|
338
|
-
})
|
|
339
|
-
PrettyOutput.print(f"已添加文件: {path}", OutputType.SUCCESS)
|
|
340
|
-
except Exception as e:
|
|
341
|
-
PrettyOutput.print(f"读取文件失败: {str(e)}", OutputType.ERROR)
|
|
342
|
-
|
|
343
|
-
return selected_files
|
|
344
|
-
|
|
345
|
-
def _finalize_changes(self, feature: str) -> None:
|
|
346
|
-
"""完成修改并提交"""
|
|
347
|
-
PrettyOutput.print("修改确认成功,提交修改", OutputType.INFO)
|
|
348
|
-
|
|
349
|
-
# 只添加已经在 git 控制下的修改文件
|
|
350
|
-
os.system("git add -u")
|
|
351
|
-
|
|
352
|
-
# 然后获取 git diff
|
|
353
|
-
git_diff = os.popen("git diff --cached").read()
|
|
354
|
-
|
|
355
|
-
# 自动生成commit信息,传入feature
|
|
356
|
-
commit_message = generate_commit_message(git_diff)
|
|
357
|
-
|
|
358
|
-
# 显示并确认commit信息
|
|
359
|
-
PrettyOutput.print(f"自动生成的commit信息: {commit_message}", OutputType.INFO)
|
|
360
|
-
user_confirm = input("是否使用该commit信息?(y/n) [y]: ") or "y"
|
|
361
|
-
|
|
362
|
-
if user_confirm.lower() != "y":
|
|
363
|
-
commit_message = input("请输入新的commit信息: ")
|
|
364
|
-
|
|
365
|
-
# 不需要再次 git add,因为已经添加过了
|
|
366
|
-
os.system(f"git commit -m '{commit_message}'")
|
|
367
|
-
save_edit_record(self.record_dir, commit_message, git_diff)
|
|
368
|
-
|
|
369
|
-
def _revert_changes(self) -> None:
|
|
370
|
-
"""回退所有修改"""
|
|
371
|
-
PrettyOutput.print("修改已取消,回退更改", OutputType.INFO)
|
|
372
|
-
os.system(f"git reset --hard")
|
|
373
|
-
os.system(f"git clean -df")
|
|
374
|
-
|
|
375
|
-
def get_key_code(self, files: List[Dict], feature: str):
|
|
376
|
-
"""提取文件中与需求相关的关键代码片段"""
|
|
377
|
-
for file_info in files:
|
|
378
|
-
PrettyOutput.print(f"分析文件: {file_info['file_path']}", OutputType.INFO)
|
|
379
|
-
model = PlatformRegistry.get_global_platform_registry().get_codegen_platform()
|
|
380
|
-
model.set_suppress_output(True)
|
|
381
|
-
file_path = file_info["file_path"]
|
|
382
|
-
content = file_info["file_content"]
|
|
383
|
-
|
|
384
|
-
try:
|
|
385
|
-
prompt = f"""You are a code analysis expert who can extract relevant snippets from code.
|
|
386
|
-
Please return in the following format:
|
|
387
|
-
<PART>
|
|
388
|
-
content
|
|
389
|
-
</PART>
|
|
390
|
-
|
|
391
|
-
Multiple snippets can be returned. If the file content is not relevant to the requirement, return empty.
|
|
392
|
-
|
|
393
|
-
Requirement: {feature}
|
|
394
|
-
File path: {file_path}
|
|
395
|
-
Code content:
|
|
396
|
-
{content}
|
|
397
|
-
"""
|
|
398
|
-
|
|
399
|
-
# 调用大模型进行分析
|
|
400
|
-
response = model.chat_until_success(prompt)
|
|
401
|
-
|
|
402
|
-
parts = re.findall(r'<PART>\n(.*?)\n</PART>', response, re.DOTALL)
|
|
403
|
-
file_info["parts"] = parts
|
|
404
|
-
except Exception as e:
|
|
405
|
-
PrettyOutput.print(f"分析文件失败: {str(e)}", OutputType.ERROR)
|
|
406
55
|
|
|
407
56
|
def execute(self, feature: str) -> Dict[str, Any]:
|
|
408
|
-
"""
|
|
57
|
+
"""Execute code modification"""
|
|
409
58
|
try:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
# 获取并选择相关文件
|
|
59
|
+
# Get and select related files
|
|
413
60
|
initial_files = self._load_related_files(feature)
|
|
414
|
-
selected_files = self.
|
|
61
|
+
selected_files = select_files(initial_files, self.root_dir)
|
|
415
62
|
|
|
416
|
-
#
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
else:
|
|
420
|
-
for file in selected_files:
|
|
421
|
-
file["parts"] = [file["file_content"]]
|
|
422
|
-
|
|
423
|
-
# 获取修改方案
|
|
424
|
-
raw_plan, structed_plan = PlanGenerator().generate_plan(feature, selected_files)
|
|
425
|
-
if not raw_plan or not structed_plan:
|
|
63
|
+
# Get modification plan
|
|
64
|
+
structed_plan = PlanGenerator().generate_plan(feature, selected_files)
|
|
65
|
+
if not structed_plan:
|
|
426
66
|
return {
|
|
427
67
|
"success": False,
|
|
428
68
|
"stdout": "",
|
|
429
|
-
"stderr": "
|
|
69
|
+
"stderr": "Failed to generate modification plan, please modify the requirement and try again",
|
|
430
70
|
}
|
|
431
71
|
|
|
432
|
-
#
|
|
433
|
-
if PatchHandler().handle_patch_application(feature
|
|
434
|
-
self._finalize_changes(feature)
|
|
72
|
+
# Execute modification
|
|
73
|
+
if PatchHandler().handle_patch_application(feature, structed_plan):
|
|
435
74
|
return {
|
|
436
75
|
"success": True,
|
|
437
|
-
"stdout": "
|
|
76
|
+
"stdout": "Code modification successful",
|
|
438
77
|
"stderr": "",
|
|
439
78
|
}
|
|
440
79
|
else:
|
|
441
|
-
self._revert_changes()
|
|
442
80
|
return {
|
|
443
81
|
"success": False,
|
|
444
82
|
"stdout": "",
|
|
445
|
-
"stderr": "
|
|
83
|
+
"stderr": "Code modification failed, please modify the requirement and try again",
|
|
446
84
|
}
|
|
447
85
|
|
|
448
86
|
except Exception as e:
|
|
449
|
-
self._revert_changes()
|
|
450
87
|
return {
|
|
451
88
|
"success": False,
|
|
452
89
|
"stdout": "",
|
|
453
|
-
"stderr": f"
|
|
90
|
+
"stderr": f"Execution failed: {str(e)}, please modify the requirement and try again",
|
|
454
91
|
"error": e
|
|
455
92
|
}
|
|
456
93
|
|
|
457
94
|
def main():
|
|
458
|
-
"""
|
|
95
|
+
"""Command line entry"""
|
|
459
96
|
import argparse
|
|
460
97
|
|
|
461
98
|
load_env_from_file()
|
|
462
99
|
|
|
463
|
-
parser = argparse.ArgumentParser(description='
|
|
464
|
-
parser.add_argument('-d', '--dir', help='
|
|
465
|
-
parser.add_argument('-l', '--language', help='
|
|
100
|
+
parser = argparse.ArgumentParser(description='Code modification tool')
|
|
101
|
+
parser.add_argument('-d', '--dir', help='Project root directory', default=os.getcwd())
|
|
102
|
+
parser.add_argument('-l', '--language', help='Programming language', default="python")
|
|
466
103
|
args = parser.parse_args()
|
|
467
104
|
|
|
468
105
|
tool = JarvisCoder(args.dir, args.language)
|
|
469
106
|
|
|
470
|
-
#
|
|
107
|
+
# Loop through requirements
|
|
471
108
|
while True:
|
|
472
109
|
try:
|
|
473
|
-
#
|
|
474
|
-
feature = get_multiline_input("
|
|
110
|
+
# Get requirements, pass in project root directory
|
|
111
|
+
feature = get_multiline_input("Please enter the development requirements (input empty line to exit):", tool.root_dir)
|
|
475
112
|
|
|
476
113
|
if not feature or feature == "__interrupt__":
|
|
477
114
|
break
|
|
478
115
|
|
|
479
|
-
#
|
|
116
|
+
# Execute modification
|
|
480
117
|
result = tool.execute(feature)
|
|
481
118
|
|
|
482
|
-
#
|
|
119
|
+
# Display results
|
|
483
120
|
if result["success"]:
|
|
484
121
|
PrettyOutput.print(result["stdout"], OutputType.SUCCESS)
|
|
485
122
|
else:
|
|
486
123
|
if result.get("stderr"):
|
|
487
124
|
PrettyOutput.print(result["stderr"], OutputType.WARNING)
|
|
488
|
-
if result.get("error"): #
|
|
125
|
+
if result.get("error"): # Use get() method to avoid KeyError
|
|
489
126
|
error = result["error"]
|
|
490
|
-
PrettyOutput.print(f"
|
|
491
|
-
PrettyOutput.print(f"
|
|
492
|
-
#
|
|
493
|
-
PrettyOutput.print("\
|
|
127
|
+
PrettyOutput.print(f"Error type: {type(error).__name__}", OutputType.WARNING)
|
|
128
|
+
PrettyOutput.print(f"Error information: {str(error)}", OutputType.WARNING)
|
|
129
|
+
# Prompt user to continue input
|
|
130
|
+
PrettyOutput.print("\nYou can modify the requirements and try again", OutputType.INFO)
|
|
494
131
|
|
|
495
132
|
except KeyboardInterrupt:
|
|
496
|
-
print("\
|
|
133
|
+
print("\nUser interrupted execution")
|
|
497
134
|
break
|
|
498
135
|
except Exception as e:
|
|
499
|
-
PrettyOutput.print(f"
|
|
500
|
-
PrettyOutput.print("\
|
|
136
|
+
PrettyOutput.print(f"Execution failed: {str(e)}", OutputType.ERROR)
|
|
137
|
+
PrettyOutput.print("\nYou can modify the requirements and try again", OutputType.INFO)
|
|
501
138
|
continue
|
|
502
139
|
|
|
503
140
|
return 0
|
|
@@ -506,93 +143,78 @@ if __name__ == "__main__":
|
|
|
506
143
|
exit(main())
|
|
507
144
|
|
|
508
145
|
class FilePathCompleter(Completer):
|
|
509
|
-
"""
|
|
146
|
+
"""File path auto-completer"""
|
|
510
147
|
|
|
511
148
|
def __init__(self, root_dir: str):
|
|
512
149
|
self.root_dir = root_dir
|
|
513
150
|
self._file_list = None
|
|
514
151
|
|
|
515
152
|
def _get_files(self) -> List[str]:
|
|
516
|
-
"""
|
|
153
|
+
"""Get the list of files managed by git"""
|
|
517
154
|
if self._file_list is None:
|
|
518
155
|
try:
|
|
519
|
-
#
|
|
156
|
+
# Switch to project root directory
|
|
520
157
|
old_cwd = os.getcwd()
|
|
521
158
|
os.chdir(self.root_dir)
|
|
522
159
|
|
|
523
|
-
#
|
|
160
|
+
# Get the list of files managed by git
|
|
524
161
|
self._file_list = os.popen("git ls-files").read().splitlines()
|
|
525
162
|
|
|
526
|
-
#
|
|
163
|
+
# Restore working directory
|
|
527
164
|
os.chdir(old_cwd)
|
|
528
165
|
except Exception as e:
|
|
529
|
-
PrettyOutput.print(f"
|
|
166
|
+
PrettyOutput.print(f"Failed to get file list: {str(e)}", OutputType.WARNING)
|
|
530
167
|
self._file_list = []
|
|
531
168
|
return self._file_list
|
|
532
169
|
|
|
533
170
|
def get_completions(self, document, complete_event):
|
|
534
|
-
"""
|
|
171
|
+
"""Get completion suggestions"""
|
|
535
172
|
text_before_cursor = document.text_before_cursor
|
|
536
173
|
|
|
537
|
-
#
|
|
174
|
+
# Check if @ was just entered
|
|
538
175
|
if text_before_cursor.endswith('@'):
|
|
539
|
-
#
|
|
176
|
+
# Display all files
|
|
540
177
|
for path in self._get_files():
|
|
541
178
|
yield Completion(path, start_position=0)
|
|
542
179
|
return
|
|
543
180
|
|
|
544
|
-
#
|
|
181
|
+
# Check if there was an @ before, and get the search word after @
|
|
545
182
|
at_pos = text_before_cursor.rfind('@')
|
|
546
183
|
if at_pos == -1:
|
|
547
184
|
return
|
|
548
185
|
|
|
549
186
|
search = text_before_cursor[at_pos + 1:].lower().strip()
|
|
550
187
|
|
|
551
|
-
#
|
|
188
|
+
# Provide matching file suggestions
|
|
552
189
|
for path in self._get_files():
|
|
553
190
|
path_lower = path.lower()
|
|
554
|
-
if (search in path_lower or #
|
|
555
|
-
search in os.path.basename(path_lower) or #
|
|
556
|
-
any(fnmatch.fnmatch(path_lower, f'*{s}*') for s in search.split())): #
|
|
557
|
-
#
|
|
191
|
+
if (search in path_lower or # Directly included
|
|
192
|
+
search in os.path.basename(path_lower) or # File name included
|
|
193
|
+
any(fnmatch.fnmatch(path_lower, f'*{s}*') for s in search.split())): # Wildcard matching
|
|
194
|
+
# Calculate the correct start_position
|
|
558
195
|
yield Completion(path, start_position=-(len(search)))
|
|
559
196
|
|
|
560
|
-
class SmartCompleter(Completer):
|
|
561
|
-
"""智能自动完成器,组合词语和文件路径补全"""
|
|
562
|
-
|
|
563
|
-
def __init__(self, word_completer: WordCompleter, file_completer: FilePathCompleter):
|
|
564
|
-
self.word_completer = word_completer
|
|
565
|
-
self.file_completer = file_completer
|
|
566
|
-
|
|
567
|
-
def get_completions(self, document, complete_event):
|
|
568
|
-
"""获取补全建议"""
|
|
569
|
-
# 如果当前行以@结尾,使用文件补全
|
|
570
|
-
if document.text_before_cursor.strip().endswith('@'):
|
|
571
|
-
yield from self.file_completer.get_completions(document, complete_event)
|
|
572
|
-
else:
|
|
573
|
-
# 否则使用词语补全
|
|
574
|
-
yield from self.word_completer.get_completions(document, complete_event)
|
|
575
197
|
|
|
576
198
|
def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
|
|
577
|
-
"""
|
|
199
|
+
"""Get multi-line input, support file path auto-completion function
|
|
578
200
|
|
|
579
201
|
Args:
|
|
580
|
-
prompt_text:
|
|
581
|
-
root_dir:
|
|
202
|
+
prompt_text: Prompt text
|
|
203
|
+
root_dir: Project root directory, for file completion
|
|
582
204
|
|
|
583
205
|
Returns:
|
|
584
|
-
str:
|
|
206
|
+
str: User input text
|
|
585
207
|
"""
|
|
586
|
-
#
|
|
208
|
+
# Create file completion
|
|
587
209
|
file_completer = FilePathCompleter(root_dir or os.getcwd())
|
|
588
210
|
|
|
589
|
-
#
|
|
211
|
+
# Create prompt style
|
|
590
212
|
style = Style.from_dict({
|
|
591
213
|
'prompt': 'ansicyan bold',
|
|
592
214
|
'input': 'ansiwhite',
|
|
593
215
|
})
|
|
594
216
|
|
|
595
|
-
#
|
|
217
|
+
# Create session
|
|
596
218
|
session = PromptSession(
|
|
597
219
|
completer=file_completer,
|
|
598
220
|
style=style,
|
|
@@ -601,20 +223,20 @@ def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
|
|
|
601
223
|
complete_while_typing=True
|
|
602
224
|
)
|
|
603
225
|
|
|
604
|
-
#
|
|
226
|
+
# Display initial prompt text
|
|
605
227
|
print(f"\n{prompt_text}")
|
|
606
228
|
|
|
607
|
-
#
|
|
229
|
+
# Create prompt
|
|
608
230
|
prompt = FormattedText([
|
|
609
231
|
('class:prompt', ">>> ")
|
|
610
232
|
])
|
|
611
233
|
|
|
612
|
-
#
|
|
234
|
+
# Get input
|
|
613
235
|
lines = []
|
|
614
236
|
try:
|
|
615
237
|
while True:
|
|
616
238
|
line = session.prompt(prompt).strip()
|
|
617
|
-
if not line: #
|
|
239
|
+
if not line: # Empty line means input end
|
|
618
240
|
break
|
|
619
241
|
lines.append(line)
|
|
620
242
|
except KeyboardInterrupt:
|