jarvis-ai-assistant 0.1.98__py3-none-any.whl → 0.1.100__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 +202 -0
- jarvis/jarvis_codebase/main.py +415 -287
- jarvis/jarvis_coder/file_select.py +209 -0
- jarvis/jarvis_coder/git_utils.py +64 -2
- jarvis/jarvis_coder/main.py +13 -397
- jarvis/jarvis_coder/patch_handler.py +229 -81
- jarvis/jarvis_coder/plan_generator.py +49 -7
- jarvis/jarvis_platform/main.py +2 -2
- jarvis/jarvis_rag/main.py +11 -11
- jarvis/jarvis_smart_shell/main.py +5 -5
- jarvis/models/base.py +6 -1
- jarvis/models/kimi.py +2 -2
- jarvis/models/ollama.py +2 -2
- jarvis/models/openai.py +1 -1
- jarvis/models/registry.py +38 -18
- jarvis/tools/ask_user.py +12 -9
- jarvis/tools/chdir.py +9 -5
- jarvis/tools/create_code_sub_agent.py +56 -0
- jarvis/tools/{sub_agent.py → create_sub_agent.py} +6 -2
- jarvis/tools/execute_code_modification.py +70 -0
- jarvis/tools/{shell.py → execute_shell.py} +2 -2
- jarvis/tools/{file_ops.py → file_operation.py} +19 -15
- jarvis/tools/find_files.py +119 -0
- jarvis/tools/{generator.py → generate_tool.py} +27 -25
- jarvis/tools/methodology.py +32 -26
- jarvis/tools/rag.py +37 -33
- jarvis/tools/{webpage.py → read_webpage.py} +4 -2
- jarvis/tools/registry.py +94 -48
- jarvis/tools/search.py +19 -16
- jarvis/tools/select_code_files.py +61 -0
- jarvis/tools/thinker.py +7 -5
- jarvis/utils.py +155 -32
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/METADATA +9 -8
- jarvis_ai_assistant-0.1.100.dist-info/RECORD +51 -0
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/entry_points.txt +2 -1
- jarvis/main.py +0 -155
- jarvis/tools/codebase_qa.py +0 -74
- jarvis/tools/coder.py +0 -69
- jarvis_ai_assistant-0.1.98.dist-info/RECORD +0 -47
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/top_level.txt +0 -0
jarvis/jarvis_coder/main.py
CHANGED
|
@@ -3,7 +3,8 @@ import threading
|
|
|
3
3
|
from typing import Dict, Any, List, Optional
|
|
4
4
|
import re
|
|
5
5
|
|
|
6
|
-
from jarvis.
|
|
6
|
+
from jarvis.jarvis_coder.file_select import select_files
|
|
7
|
+
from jarvis.utils import OutputType, PrettyOutput, find_git_root, get_max_context_length, is_long_context, init_env, while_success
|
|
7
8
|
from jarvis.models.registry import PlatformRegistry
|
|
8
9
|
from jarvis.jarvis_codebase.main import CodeBase
|
|
9
10
|
from prompt_toolkit import PromptSession
|
|
@@ -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
|
# 全局锁对象
|
|
@@ -29,98 +30,14 @@ class JarvisCoder:
|
|
|
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 root directory: {self.root_dir}", OutputType.INFO)
|
|
40
|
-
|
|
41
|
-
# 1. Check if the code repository path exists, if it does not exist, create it
|
|
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. Create .jarvis-coder directory
|
|
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. Process .gitignore file
|
|
59
|
-
gitignore_path = os.path.join(self.root_dir, ".gitignore")
|
|
60
|
-
gitignore_modified = False
|
|
61
|
-
jarvis_ignore_pattern = ".jarvis-*"
|
|
62
|
-
|
|
63
|
-
# 3.1 If .gitignore does not exist, create it
|
|
64
|
-
if not os.path.exists(gitignore_path):
|
|
65
|
-
PrettyOutput.print("Create .gitignore file", 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 Check if it already contains the .jarvis-* pattern
|
|
71
|
-
with open(gitignore_path, "r", encoding="utf-8") as f:
|
|
72
|
-
content = f.read()
|
|
73
|
-
|
|
74
|
-
# 3.2 Check if it already contains the .jarvis-* pattern
|
|
75
|
-
if jarvis_ignore_pattern not in content.split("\n"):
|
|
76
|
-
PrettyOutput.print("Add .jarvis-* to .gitignore", OutputType.INFO)
|
|
77
|
-
with open(gitignore_path, "a", encoding="utf-8") as f:
|
|
78
|
-
# Ensure the file ends with a newline
|
|
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. Check if the code repository is a git repository, if not, initialize the git repository
|
|
85
|
-
if not os.path.exists(os.path.join(self.root_dir, ".git")):
|
|
86
|
-
PrettyOutput.print("Initialize Git repository", OutputType.INFO)
|
|
87
|
-
os.system("git init")
|
|
88
|
-
os.system("git add .")
|
|
89
|
-
os.system("git commit -m 'Initial commit'")
|
|
90
|
-
# 5. If .gitignore is modified, commit the changes
|
|
91
|
-
elif gitignore_modified:
|
|
92
|
-
PrettyOutput.print("Commit .gitignore changes", OutputType.INFO)
|
|
93
|
-
os.system("git add .gitignore")
|
|
94
|
-
os.system("git commit -m 'chore: update .gitignore to exclude .jarvis-* files'")
|
|
95
|
-
# 6. Check if there are uncommitted files in the code repository, if there are, commit once
|
|
96
|
-
elif self._has_uncommitted_files():
|
|
97
|
-
PrettyOutput.print("Commit uncommitted changes", 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
|
-
"""Check if there are uncommitted files in the code repository"""
|
|
109
|
-
# Get unstaged modifications
|
|
110
|
-
unstaged = os.popen("git diff --name-only").read()
|
|
111
|
-
# Get staged but uncommitted modifications
|
|
112
|
-
staged = os.popen("git diff --cached --name-only").read()
|
|
113
|
-
# Get untracked files
|
|
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
|
-
"""Prepare execution environment"""
|
|
120
|
-
self._codebase.generate_codebase()
|
|
121
|
-
|
|
122
39
|
|
|
123
|
-
def _load_related_files(self, feature: str) -> List[
|
|
40
|
+
def _load_related_files(self, feature: str) -> List[str]:
|
|
124
41
|
"""Load related file content"""
|
|
125
42
|
ret = []
|
|
126
43
|
# Ensure the index database is generated
|
|
@@ -129,300 +46,23 @@ class JarvisCoder:
|
|
|
129
46
|
self._codebase.generate_codebase()
|
|
130
47
|
|
|
131
48
|
related_files = self._codebase.search_similar(feature)
|
|
132
|
-
for file
|
|
133
|
-
PrettyOutput.print(f"Related file: {file}
|
|
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
|
-
"""Parse file selection expression
|
|
140
|
-
|
|
141
|
-
Supported formats:
|
|
142
|
-
- Single number: "1"
|
|
143
|
-
- Comma-separated: "1,3,5"
|
|
144
|
-
- Range: "1-5"
|
|
145
|
-
- Combination: "1,3-5,7"
|
|
146
|
-
|
|
147
|
-
Args:
|
|
148
|
-
input_str: User input selection expression
|
|
149
|
-
max_index: Maximum selectable index
|
|
150
|
-
|
|
151
|
-
Returns:
|
|
152
|
-
List[int]: Selected index list (starting from 0)
|
|
153
|
-
"""
|
|
154
|
-
selected = set()
|
|
155
|
-
|
|
156
|
-
# Remove all whitespace characters
|
|
157
|
-
input_str = "".join(input_str.split())
|
|
158
|
-
|
|
159
|
-
# Process comma-separated parts
|
|
160
|
-
for part in input_str.split(","):
|
|
161
|
-
if not part:
|
|
162
|
-
continue
|
|
163
|
-
|
|
164
|
-
# Process range (e.g.: 3-6)
|
|
165
|
-
if "-" in part:
|
|
166
|
-
try:
|
|
167
|
-
start, end = map(int, part.split("-"))
|
|
168
|
-
# Convert to index starting from 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"Ignore invalid range expression: {part}", OutputType.WARNING)
|
|
175
|
-
# Process single number
|
|
176
|
-
else:
|
|
177
|
-
try:
|
|
178
|
-
index = int(part) - 1 # Convert to index starting from 0
|
|
179
|
-
if 0 <= index < max_index:
|
|
180
|
-
selected.add(index)
|
|
181
|
-
else:
|
|
182
|
-
PrettyOutput.print(f"Ignore index out of range: {part}", OutputType.WARNING)
|
|
183
|
-
except ValueError:
|
|
184
|
-
PrettyOutput.print(f"Ignore invalid number: {part}", OutputType.WARNING)
|
|
185
|
-
|
|
186
|
-
return sorted(list(selected))
|
|
187
|
-
|
|
188
|
-
def _get_file_completer(self) -> Completer:
|
|
189
|
-
"""Create file path completer"""
|
|
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
|
-
# Get the text of the current input
|
|
196
|
-
text = document.text_before_cursor
|
|
197
|
-
|
|
198
|
-
# If the input is empty, return all files in the root directory
|
|
199
|
-
if not text:
|
|
200
|
-
for path in self._list_files(""):
|
|
201
|
-
yield Completion(path, start_position=0)
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
# Get the current directory and partial file name
|
|
205
|
-
current_dir = os.path.dirname(text)
|
|
206
|
-
file_prefix = os.path.basename(text)
|
|
207
|
-
|
|
208
|
-
# List matching files
|
|
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
|
-
"""List all files in the specified directory (recursively)"""
|
|
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
|
-
# Ignore .git directory and other hidden files
|
|
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
|
-
|
|
230
|
-
return FileCompleter(self.root_dir)
|
|
231
|
-
|
|
232
|
-
def _fuzzy_match_files(self, pattern: str) -> List[str]:
|
|
233
|
-
"""Fuzzy match file path
|
|
234
|
-
|
|
235
|
-
Args:
|
|
236
|
-
pattern: Matching pattern
|
|
237
|
-
|
|
238
|
-
Returns:
|
|
239
|
-
List[str]: List of matching file paths
|
|
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
|
-
"""Let the user select and supplement related files"""
|
|
262
|
-
PrettyOutput.section("Related files", OutputType.INFO)
|
|
263
|
-
|
|
264
|
-
# Display found files
|
|
265
|
-
selected_files = list(related_files) # Default select all
|
|
266
|
-
for i, file in enumerate(related_files, 1):
|
|
267
|
-
PrettyOutput.print(f"[{i}] {file['file_path']}", OutputType.INFO)
|
|
268
|
-
|
|
269
|
-
# Ask the user if they need to adjust the file list
|
|
270
|
-
user_input = input("\nDo you need to adjust the file list? (y/n) [n]: ").strip().lower() or 'n'
|
|
271
|
-
if user_input == 'y':
|
|
272
|
-
# Let the user select files
|
|
273
|
-
PrettyOutput.print("\nPlease enter the file numbers to include (support: 1,3-6 format, press Enter to keep the current selection):", 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("No valid files selected, keep the current selection", OutputType.WARNING)
|
|
281
|
-
|
|
282
|
-
# Ask if they need to supplement files
|
|
283
|
-
user_input = input("\nDo you need to supplement other files? (y/n) [n]: ").strip().lower() or 'n'
|
|
284
|
-
if user_input == 'y':
|
|
285
|
-
# Create file completion session
|
|
286
|
-
session = PromptSession(
|
|
287
|
-
completer=self._get_file_completer(),
|
|
288
|
-
complete_while_typing=True
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
while True:
|
|
292
|
-
PrettyOutput.print("\nPlease enter the file path to supplement (support Tab completion and *? wildcard, input empty line to end):", 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
|
-
# Process wildcard matching
|
|
302
|
-
if '*' in file_path or '?' in file_path:
|
|
303
|
-
matches = self._fuzzy_match_files(file_path)
|
|
304
|
-
if not matches:
|
|
305
|
-
PrettyOutput.print("No matching files found", OutputType.WARNING)
|
|
306
|
-
continue
|
|
307
|
-
|
|
308
|
-
# Display matching files
|
|
309
|
-
PrettyOutput.print("\nFound the following matching files:", OutputType.INFO)
|
|
310
|
-
for i, path in enumerate(matches, 1):
|
|
311
|
-
PrettyOutput.print(f"[{i}] {path}", OutputType.INFO)
|
|
312
|
-
|
|
313
|
-
# Let the user select
|
|
314
|
-
numbers = input("\nPlease select the file numbers to add (support: 1,3-6 format, press Enter to select all): ").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
|
-
# Add selected files
|
|
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"File does not exist: {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"File added: {path}", OutputType.SUCCESS)
|
|
340
|
-
except Exception as e:
|
|
341
|
-
PrettyOutput.print(f"Failed to read file: {str(e)}", OutputType.ERROR)
|
|
342
|
-
|
|
343
|
-
return selected_files
|
|
344
54
|
|
|
345
|
-
def _finalize_changes(self, feature: str) -> None:
|
|
346
|
-
"""Complete changes and commit"""
|
|
347
|
-
PrettyOutput.print("Modification confirmed, committing...", OutputType.INFO)
|
|
348
|
-
|
|
349
|
-
# Add only modified files under git control
|
|
350
|
-
os.system("git add -u")
|
|
351
|
-
|
|
352
|
-
# Then get git diff
|
|
353
|
-
git_diff = os.popen("git diff --cached").read()
|
|
354
|
-
|
|
355
|
-
# Automatically generate commit information, pass in feature
|
|
356
|
-
commit_message = generate_commit_message(git_diff)
|
|
357
|
-
|
|
358
|
-
# Display and confirm commit information
|
|
359
|
-
PrettyOutput.print(f"Automatically generated commit information: {commit_message}", OutputType.INFO)
|
|
360
|
-
user_confirm = input("Use this commit information? (y/n) [y]: ") or "y"
|
|
361
|
-
|
|
362
|
-
if user_confirm.lower() != "y":
|
|
363
|
-
commit_message = input("Please enter a new commit information: ")
|
|
364
|
-
|
|
365
|
-
# No need to git add again, it has already been added
|
|
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
|
-
"""Revert all changes"""
|
|
371
|
-
PrettyOutput.print("Modification cancelled, reverting changes", 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
|
-
"""Extract relevant key code snippets from files"""
|
|
377
|
-
for file_info in files:
|
|
378
|
-
PrettyOutput.print(f"Analyzing file: {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"Failed to analyze file: {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
|
-
self._prepare_execution()
|
|
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
|
-
# Whether it is a long context
|
|
417
|
-
if is_long_context([file['file_path'] for file in selected_files]):
|
|
418
|
-
self.get_key_code(selected_files, feature)
|
|
419
|
-
else:
|
|
420
|
-
for file in selected_files:
|
|
421
|
-
file["parts"] = [file["file_content"]]
|
|
422
|
-
|
|
423
63
|
# Get modification plan
|
|
424
|
-
|
|
425
|
-
if not
|
|
64
|
+
structed_plan = PlanGenerator().generate_plan(feature, selected_files)
|
|
65
|
+
if not structed_plan:
|
|
426
66
|
return {
|
|
427
67
|
"success": False,
|
|
428
68
|
"stdout": "",
|
|
@@ -430,15 +70,13 @@ Code content:
|
|
|
430
70
|
}
|
|
431
71
|
|
|
432
72
|
# Execute modification
|
|
433
|
-
if PatchHandler().handle_patch_application(feature
|
|
434
|
-
self._finalize_changes(feature)
|
|
73
|
+
if PatchHandler().handle_patch_application(feature, structed_plan):
|
|
435
74
|
return {
|
|
436
75
|
"success": True,
|
|
437
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": "",
|
|
@@ -446,19 +84,17 @@ Code content:
|
|
|
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
90
|
"stderr": f"Execution failed: {str(e)}, please modify the requirement and try again",
|
|
454
|
-
"error": e
|
|
455
91
|
}
|
|
456
92
|
|
|
457
93
|
def main():
|
|
458
94
|
"""Command line entry"""
|
|
459
95
|
import argparse
|
|
460
96
|
|
|
461
|
-
|
|
97
|
+
init_env()
|
|
462
98
|
|
|
463
99
|
parser = argparse.ArgumentParser(description='Code modification tool')
|
|
464
100
|
parser.add_argument('-d', '--dir', help='Project root directory', default=os.getcwd())
|
|
@@ -485,11 +121,6 @@ def main():
|
|
|
485
121
|
else:
|
|
486
122
|
if result.get("stderr"):
|
|
487
123
|
PrettyOutput.print(result["stderr"], OutputType.WARNING)
|
|
488
|
-
if result.get("error"): # Use get() method to avoid KeyError
|
|
489
|
-
error = result["error"]
|
|
490
|
-
PrettyOutput.print(f"Error type: {type(error).__name__}", OutputType.WARNING)
|
|
491
|
-
PrettyOutput.print(f"Error information: {str(error)}", OutputType.WARNING)
|
|
492
|
-
# Prompt user to continue input
|
|
493
124
|
PrettyOutput.print("\nYou can modify the requirements and try again", OutputType.INFO)
|
|
494
125
|
|
|
495
126
|
except KeyboardInterrupt:
|
|
@@ -557,21 +188,6 @@ class FilePathCompleter(Completer):
|
|
|
557
188
|
# Calculate the correct start_position
|
|
558
189
|
yield Completion(path, start_position=-(len(search)))
|
|
559
190
|
|
|
560
|
-
class SmartCompleter(Completer):
|
|
561
|
-
"""Smart auto-completer, combine word and file path completion"""
|
|
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
|
-
"""Get completion suggestions"""
|
|
569
|
-
# If the current line ends with @, use file completion
|
|
570
|
-
if document.text_before_cursor.strip().endswith('@'):
|
|
571
|
-
yield from self.file_completer.get_completions(document, complete_event)
|
|
572
|
-
else:
|
|
573
|
-
# Otherwise, use word completion
|
|
574
|
-
yield from self.word_completer.get_completions(document, complete_event)
|
|
575
191
|
|
|
576
192
|
def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
|
|
577
193
|
"""Get multi-line input, support file path auto-completion function
|