jarvis-ai-assistant 0.1.97__py3-none-any.whl → 0.1.98__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/jarvis_coder/git_utils.py +18 -18
- jarvis/jarvis_coder/main.py +150 -150
- jarvis/jarvis_coder/patch_handler.py +38 -38
- jarvis/jarvis_coder/plan_generator.py +21 -21
- jarvis/jarvis_platform/main.py +38 -38
- jarvis/jarvis_rag/main.py +181 -181
- jarvis/jarvis_smart_shell/main.py +19 -19
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/METADATA +1 -1
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/RECORD +14 -14
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/top_level.txt +0 -0
jarvis/jarvis_coder/main.py
CHANGED
|
@@ -20,14 +20,14 @@ index_lock = threading.Lock()
|
|
|
20
20
|
|
|
21
21
|
class JarvisCoder:
|
|
22
22
|
def __init__(self, root_dir: str, language: Optional[str] = "python"):
|
|
23
|
-
"""
|
|
23
|
+
"""Initialize code modification tool"""
|
|
24
24
|
self.root_dir = root_dir
|
|
25
25
|
self.language = language
|
|
26
26
|
self._init_directories()
|
|
27
27
|
self._init_codebase()
|
|
28
28
|
|
|
29
29
|
def _init_directories(self):
|
|
30
|
-
"""
|
|
30
|
+
"""Initialize directories"""
|
|
31
31
|
self.max_context_length = get_max_context_length()
|
|
32
32
|
|
|
33
33
|
root_dir = find_git_root(self.root_dir)
|
|
@@ -36,9 +36,9 @@ class JarvisCoder:
|
|
|
36
36
|
|
|
37
37
|
self.root_dir = root_dir
|
|
38
38
|
|
|
39
|
-
PrettyOutput.print(f"Git
|
|
39
|
+
PrettyOutput.print(f"Git root directory: {self.root_dir}", OutputType.INFO)
|
|
40
40
|
|
|
41
|
-
# 1.
|
|
41
|
+
# 1. Check if the code repository path exists, if it does not exist, create it
|
|
42
42
|
if not os.path.exists(self.root_dir):
|
|
43
43
|
PrettyOutput.print(
|
|
44
44
|
"Root directory does not exist, creating...", OutputType.INFO)
|
|
@@ -46,7 +46,7 @@ class JarvisCoder:
|
|
|
46
46
|
|
|
47
47
|
os.chdir(self.root_dir)
|
|
48
48
|
|
|
49
|
-
# 2.
|
|
49
|
+
# 2. Create .jarvis-coder directory
|
|
50
50
|
self.jarvis_dir = os.path.join(self.root_dir, ".jarvis-coder")
|
|
51
51
|
if not os.path.exists(self.jarvis_dir):
|
|
52
52
|
os.makedirs(self.jarvis_dir)
|
|
@@ -55,157 +55,157 @@ class JarvisCoder:
|
|
|
55
55
|
if not os.path.exists(self.record_dir):
|
|
56
56
|
os.makedirs(self.record_dir)
|
|
57
57
|
|
|
58
|
-
# 3.
|
|
58
|
+
# 3. Process .gitignore file
|
|
59
59
|
gitignore_path = os.path.join(self.root_dir, ".gitignore")
|
|
60
60
|
gitignore_modified = False
|
|
61
61
|
jarvis_ignore_pattern = ".jarvis-*"
|
|
62
62
|
|
|
63
|
-
# 3.1
|
|
63
|
+
# 3.1 If .gitignore does not exist, create it
|
|
64
64
|
if not os.path.exists(gitignore_path):
|
|
65
|
-
PrettyOutput.print("
|
|
65
|
+
PrettyOutput.print("Create .gitignore file", OutputType.INFO)
|
|
66
66
|
with open(gitignore_path, "w", encoding="utf-8") as f:
|
|
67
67
|
f.write(f"{jarvis_ignore_pattern}\n")
|
|
68
68
|
gitignore_modified = True
|
|
69
69
|
else:
|
|
70
|
-
# 3.2
|
|
70
|
+
# 3.2 Check if it already contains the .jarvis-* pattern
|
|
71
71
|
with open(gitignore_path, "r", encoding="utf-8") as f:
|
|
72
72
|
content = f.read()
|
|
73
73
|
|
|
74
|
-
#
|
|
74
|
+
# 3.2 Check if it already contains the .jarvis-* pattern
|
|
75
75
|
if jarvis_ignore_pattern not in content.split("\n"):
|
|
76
|
-
PrettyOutput.print("
|
|
76
|
+
PrettyOutput.print("Add .jarvis-* to .gitignore", OutputType.INFO)
|
|
77
77
|
with open(gitignore_path, "a", encoding="utf-8") as f:
|
|
78
|
-
#
|
|
78
|
+
# Ensure the file ends with a newline
|
|
79
79
|
if not content.endswith("\n"):
|
|
80
80
|
f.write("\n")
|
|
81
81
|
f.write(f"{jarvis_ignore_pattern}\n")
|
|
82
82
|
gitignore_modified = True
|
|
83
83
|
|
|
84
|
-
# 4.
|
|
84
|
+
# 4. Check if the code repository is a git repository, if not, initialize the git repository
|
|
85
85
|
if not os.path.exists(os.path.join(self.root_dir, ".git")):
|
|
86
|
-
PrettyOutput.print("
|
|
86
|
+
PrettyOutput.print("Initialize Git repository", OutputType.INFO)
|
|
87
87
|
os.system("git init")
|
|
88
88
|
os.system("git add .")
|
|
89
89
|
os.system("git commit -m 'Initial commit'")
|
|
90
|
-
# 5.
|
|
90
|
+
# 5. If .gitignore is modified, commit the changes
|
|
91
91
|
elif gitignore_modified:
|
|
92
|
-
PrettyOutput.print("
|
|
92
|
+
PrettyOutput.print("Commit .gitignore changes", OutputType.INFO)
|
|
93
93
|
os.system("git add .gitignore")
|
|
94
94
|
os.system("git commit -m 'chore: update .gitignore to exclude .jarvis-* files'")
|
|
95
|
-
# 6.
|
|
95
|
+
# 6. Check if there are uncommitted files in the code repository, if there are, commit once
|
|
96
96
|
elif self._has_uncommitted_files():
|
|
97
|
-
PrettyOutput.print("
|
|
97
|
+
PrettyOutput.print("Commit uncommitted changes", OutputType.INFO)
|
|
98
98
|
os.system("git add .")
|
|
99
99
|
git_diff = os.popen("git diff --cached").read()
|
|
100
100
|
commit_message = generate_commit_message(git_diff)
|
|
101
101
|
os.system(f"git commit -m '{commit_message}'")
|
|
102
102
|
|
|
103
103
|
def _init_codebase(self):
|
|
104
|
-
"""
|
|
104
|
+
"""Initialize codebase"""
|
|
105
105
|
self._codebase = CodeBase(self.root_dir)
|
|
106
106
|
|
|
107
107
|
def _has_uncommitted_files(self) -> bool:
|
|
108
|
-
"""
|
|
109
|
-
#
|
|
108
|
+
"""Check if there are uncommitted files in the code repository"""
|
|
109
|
+
# Get unstaged modifications
|
|
110
110
|
unstaged = os.popen("git diff --name-only").read()
|
|
111
|
-
#
|
|
111
|
+
# Get staged but uncommitted modifications
|
|
112
112
|
staged = os.popen("git diff --cached --name-only").read()
|
|
113
|
-
#
|
|
113
|
+
# Get untracked files
|
|
114
114
|
untracked = os.popen("git ls-files --others --exclude-standard").read()
|
|
115
115
|
|
|
116
116
|
return bool(unstaged or staged or untracked)
|
|
117
117
|
|
|
118
118
|
def _prepare_execution(self) -> None:
|
|
119
|
-
"""
|
|
119
|
+
"""Prepare execution environment"""
|
|
120
120
|
self._codebase.generate_codebase()
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
def _load_related_files(self, feature: str) -> List[Dict]:
|
|
124
|
-
"""
|
|
124
|
+
"""Load related file content"""
|
|
125
125
|
ret = []
|
|
126
|
-
#
|
|
126
|
+
# Ensure the index database is generated
|
|
127
127
|
if not self._codebase.is_index_generated():
|
|
128
|
-
PrettyOutput.print("
|
|
128
|
+
PrettyOutput.print("Index database not generated, generating...", OutputType.WARNING)
|
|
129
129
|
self._codebase.generate_codebase()
|
|
130
130
|
|
|
131
131
|
related_files = self._codebase.search_similar(feature)
|
|
132
132
|
for file, score in related_files:
|
|
133
|
-
PrettyOutput.print(f"
|
|
133
|
+
PrettyOutput.print(f"Related file: {file} (score: {score:.3f})", OutputType.SUCCESS)
|
|
134
134
|
content = open(file, "r", encoding="utf-8").read()
|
|
135
135
|
ret.append({"file_path": file, "file_content": content})
|
|
136
136
|
return ret
|
|
137
137
|
|
|
138
138
|
def _parse_file_selection(self, input_str: str, max_index: int) -> List[int]:
|
|
139
|
-
"""
|
|
139
|
+
"""Parse file selection expression
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
141
|
+
Supported formats:
|
|
142
|
+
- Single number: "1"
|
|
143
|
+
- Comma-separated: "1,3,5"
|
|
144
|
+
- Range: "1-5"
|
|
145
|
+
- Combination: "1,3-5,7"
|
|
146
146
|
|
|
147
147
|
Args:
|
|
148
|
-
input_str:
|
|
149
|
-
max_index:
|
|
148
|
+
input_str: User input selection expression
|
|
149
|
+
max_index: Maximum selectable index
|
|
150
150
|
|
|
151
151
|
Returns:
|
|
152
|
-
List[int]:
|
|
152
|
+
List[int]: Selected index list (starting from 0)
|
|
153
153
|
"""
|
|
154
154
|
selected = set()
|
|
155
155
|
|
|
156
|
-
#
|
|
156
|
+
# Remove all whitespace characters
|
|
157
157
|
input_str = "".join(input_str.split())
|
|
158
158
|
|
|
159
|
-
#
|
|
159
|
+
# Process comma-separated parts
|
|
160
160
|
for part in input_str.split(","):
|
|
161
161
|
if not part:
|
|
162
162
|
continue
|
|
163
163
|
|
|
164
|
-
#
|
|
164
|
+
# Process range (e.g.: 3-6)
|
|
165
165
|
if "-" in part:
|
|
166
166
|
try:
|
|
167
167
|
start, end = map(int, part.split("-"))
|
|
168
|
-
#
|
|
168
|
+
# Convert to index starting from 0
|
|
169
169
|
start = max(0, start - 1)
|
|
170
170
|
end = min(max_index, end - 1)
|
|
171
171
|
if start <= end:
|
|
172
172
|
selected.update(range(start, end + 1))
|
|
173
173
|
except ValueError:
|
|
174
|
-
PrettyOutput.print(f"
|
|
175
|
-
#
|
|
174
|
+
PrettyOutput.print(f"Ignore invalid range expression: {part}", OutputType.WARNING)
|
|
175
|
+
# Process single number
|
|
176
176
|
else:
|
|
177
177
|
try:
|
|
178
|
-
index = int(part) - 1 #
|
|
178
|
+
index = int(part) - 1 # Convert to index starting from 0
|
|
179
179
|
if 0 <= index < max_index:
|
|
180
180
|
selected.add(index)
|
|
181
181
|
else:
|
|
182
|
-
PrettyOutput.print(f"
|
|
182
|
+
PrettyOutput.print(f"Ignore index out of range: {part}", OutputType.WARNING)
|
|
183
183
|
except ValueError:
|
|
184
|
-
PrettyOutput.print(f"
|
|
184
|
+
PrettyOutput.print(f"Ignore invalid number: {part}", OutputType.WARNING)
|
|
185
185
|
|
|
186
186
|
return sorted(list(selected))
|
|
187
187
|
|
|
188
188
|
def _get_file_completer(self) -> Completer:
|
|
189
|
-
"""
|
|
189
|
+
"""Create file path completer"""
|
|
190
190
|
class FileCompleter(Completer):
|
|
191
191
|
def __init__(self, root_dir: str):
|
|
192
192
|
self.root_dir = root_dir
|
|
193
193
|
|
|
194
194
|
def get_completions(self, document, complete_event):
|
|
195
|
-
#
|
|
195
|
+
# Get the text of the current input
|
|
196
196
|
text = document.text_before_cursor
|
|
197
197
|
|
|
198
|
-
#
|
|
198
|
+
# If the input is empty, return all files in the root directory
|
|
199
199
|
if not text:
|
|
200
200
|
for path in self._list_files(""):
|
|
201
201
|
yield Completion(path, start_position=0)
|
|
202
202
|
return
|
|
203
203
|
|
|
204
|
-
#
|
|
204
|
+
# Get the current directory and partial file name
|
|
205
205
|
current_dir = os.path.dirname(text)
|
|
206
206
|
file_prefix = os.path.basename(text)
|
|
207
207
|
|
|
208
|
-
#
|
|
208
|
+
# List matching files
|
|
209
209
|
search_dir = os.path.join(self.root_dir, current_dir) if current_dir else self.root_dir
|
|
210
210
|
if os.path.isdir(search_dir):
|
|
211
211
|
for path in self._list_files(current_dir):
|
|
@@ -213,7 +213,7 @@ class JarvisCoder:
|
|
|
213
213
|
yield Completion(path, start_position=-len(text))
|
|
214
214
|
|
|
215
215
|
def _list_files(self, current_dir: str) -> List[str]:
|
|
216
|
-
"""
|
|
216
|
+
"""List all files in the specified directory (recursively)"""
|
|
217
217
|
files = []
|
|
218
218
|
search_dir = os.path.join(self.root_dir, current_dir)
|
|
219
219
|
|
|
@@ -221,7 +221,7 @@ class JarvisCoder:
|
|
|
221
221
|
for filename in filenames:
|
|
222
222
|
full_path = os.path.join(root, filename)
|
|
223
223
|
rel_path = os.path.relpath(full_path, self.root_dir)
|
|
224
|
-
#
|
|
224
|
+
# Ignore .git directory and other hidden files
|
|
225
225
|
if not any(part.startswith('.') for part in rel_path.split(os.sep)):
|
|
226
226
|
files.append(rel_path)
|
|
227
227
|
|
|
@@ -230,13 +230,13 @@ class JarvisCoder:
|
|
|
230
230
|
return FileCompleter(self.root_dir)
|
|
231
231
|
|
|
232
232
|
def _fuzzy_match_files(self, pattern: str) -> List[str]:
|
|
233
|
-
"""
|
|
233
|
+
"""Fuzzy match file path
|
|
234
234
|
|
|
235
235
|
Args:
|
|
236
|
-
pattern:
|
|
236
|
+
pattern: Matching pattern
|
|
237
237
|
|
|
238
238
|
Returns:
|
|
239
|
-
List[str]:
|
|
239
|
+
List[str]: List of matching file paths
|
|
240
240
|
"""
|
|
241
241
|
matches = []
|
|
242
242
|
|
|
@@ -258,38 +258,38 @@ class JarvisCoder:
|
|
|
258
258
|
return sorted(matches)
|
|
259
259
|
|
|
260
260
|
def _select_files(self, related_files: List[Dict]) -> List[Dict]:
|
|
261
|
-
"""
|
|
262
|
-
PrettyOutput.section("
|
|
261
|
+
"""Let the user select and supplement related files"""
|
|
262
|
+
PrettyOutput.section("Related files", OutputType.INFO)
|
|
263
263
|
|
|
264
|
-
#
|
|
265
|
-
selected_files = list(related_files) #
|
|
264
|
+
# Display found files
|
|
265
|
+
selected_files = list(related_files) # Default select all
|
|
266
266
|
for i, file in enumerate(related_files, 1):
|
|
267
267
|
PrettyOutput.print(f"[{i}] {file['file_path']}", OutputType.INFO)
|
|
268
268
|
|
|
269
|
-
#
|
|
270
|
-
user_input = input("\
|
|
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
271
|
if user_input == 'y':
|
|
272
|
-
#
|
|
273
|
-
PrettyOutput.print("\
|
|
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
274
|
numbers = input(">>> ").strip()
|
|
275
275
|
if numbers:
|
|
276
276
|
selected_indices = self._parse_file_selection(numbers, len(related_files))
|
|
277
277
|
if selected_indices:
|
|
278
278
|
selected_files = [related_files[i] for i in selected_indices]
|
|
279
279
|
else:
|
|
280
|
-
PrettyOutput.print("
|
|
280
|
+
PrettyOutput.print("No valid files selected, keep the current selection", OutputType.WARNING)
|
|
281
281
|
|
|
282
|
-
#
|
|
283
|
-
user_input = input("\
|
|
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
284
|
if user_input == 'y':
|
|
285
|
-
#
|
|
285
|
+
# Create file completion session
|
|
286
286
|
session = PromptSession(
|
|
287
287
|
completer=self._get_file_completer(),
|
|
288
288
|
complete_while_typing=True
|
|
289
289
|
)
|
|
290
290
|
|
|
291
291
|
while True:
|
|
292
|
-
PrettyOutput.print("\
|
|
292
|
+
PrettyOutput.print("\nPlease enter the file path to supplement (support Tab completion and *? wildcard, input empty line to end):", OutputType.INFO)
|
|
293
293
|
try:
|
|
294
294
|
file_path = session.prompt(">>> ").strip()
|
|
295
295
|
except KeyboardInterrupt:
|
|
@@ -298,20 +298,20 @@ class JarvisCoder:
|
|
|
298
298
|
if not file_path:
|
|
299
299
|
break
|
|
300
300
|
|
|
301
|
-
#
|
|
301
|
+
# Process wildcard matching
|
|
302
302
|
if '*' in file_path or '?' in file_path:
|
|
303
303
|
matches = self._fuzzy_match_files(file_path)
|
|
304
304
|
if not matches:
|
|
305
|
-
PrettyOutput.print("
|
|
305
|
+
PrettyOutput.print("No matching files found", OutputType.WARNING)
|
|
306
306
|
continue
|
|
307
307
|
|
|
308
|
-
#
|
|
309
|
-
PrettyOutput.print("\
|
|
308
|
+
# Display matching files
|
|
309
|
+
PrettyOutput.print("\nFound the following matching files:", OutputType.INFO)
|
|
310
310
|
for i, path in enumerate(matches, 1):
|
|
311
311
|
PrettyOutput.print(f"[{i}] {path}", OutputType.INFO)
|
|
312
312
|
|
|
313
|
-
#
|
|
314
|
-
numbers = input("\
|
|
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
315
|
if numbers:
|
|
316
316
|
indices = self._parse_file_selection(numbers, len(matches))
|
|
317
317
|
if not indices:
|
|
@@ -322,11 +322,11 @@ class JarvisCoder:
|
|
|
322
322
|
else:
|
|
323
323
|
paths_to_add = [file_path]
|
|
324
324
|
|
|
325
|
-
#
|
|
325
|
+
# Add selected files
|
|
326
326
|
for path in paths_to_add:
|
|
327
327
|
full_path = os.path.join(self.root_dir, path)
|
|
328
328
|
if not os.path.isfile(full_path):
|
|
329
|
-
PrettyOutput.print(f"
|
|
329
|
+
PrettyOutput.print(f"File does not exist: {path}", OutputType.ERROR)
|
|
330
330
|
continue
|
|
331
331
|
|
|
332
332
|
try:
|
|
@@ -336,46 +336,46 @@ class JarvisCoder:
|
|
|
336
336
|
"file_path": path,
|
|
337
337
|
"file_content": content
|
|
338
338
|
})
|
|
339
|
-
PrettyOutput.print(f"
|
|
339
|
+
PrettyOutput.print(f"File added: {path}", OutputType.SUCCESS)
|
|
340
340
|
except Exception as e:
|
|
341
|
-
PrettyOutput.print(f"
|
|
341
|
+
PrettyOutput.print(f"Failed to read file: {str(e)}", OutputType.ERROR)
|
|
342
342
|
|
|
343
343
|
return selected_files
|
|
344
344
|
|
|
345
345
|
def _finalize_changes(self, feature: str) -> None:
|
|
346
|
-
"""
|
|
347
|
-
PrettyOutput.print("
|
|
346
|
+
"""Complete changes and commit"""
|
|
347
|
+
PrettyOutput.print("Modification confirmed, committing...", OutputType.INFO)
|
|
348
348
|
|
|
349
|
-
#
|
|
349
|
+
# Add only modified files under git control
|
|
350
350
|
os.system("git add -u")
|
|
351
351
|
|
|
352
|
-
#
|
|
352
|
+
# Then get git diff
|
|
353
353
|
git_diff = os.popen("git diff --cached").read()
|
|
354
354
|
|
|
355
|
-
#
|
|
355
|
+
# Automatically generate commit information, pass in feature
|
|
356
356
|
commit_message = generate_commit_message(git_diff)
|
|
357
357
|
|
|
358
|
-
#
|
|
359
|
-
PrettyOutput.print(f"
|
|
360
|
-
user_confirm = input("
|
|
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
361
|
|
|
362
362
|
if user_confirm.lower() != "y":
|
|
363
|
-
commit_message = input("
|
|
363
|
+
commit_message = input("Please enter a new commit information: ")
|
|
364
364
|
|
|
365
|
-
#
|
|
365
|
+
# No need to git add again, it has already been added
|
|
366
366
|
os.system(f"git commit -m '{commit_message}'")
|
|
367
367
|
save_edit_record(self.record_dir, commit_message, git_diff)
|
|
368
368
|
|
|
369
369
|
def _revert_changes(self) -> None:
|
|
370
|
-
"""
|
|
371
|
-
PrettyOutput.print("
|
|
370
|
+
"""Revert all changes"""
|
|
371
|
+
PrettyOutput.print("Modification cancelled, reverting changes", OutputType.INFO)
|
|
372
372
|
os.system(f"git reset --hard")
|
|
373
373
|
os.system(f"git clean -df")
|
|
374
374
|
|
|
375
375
|
def get_key_code(self, files: List[Dict], feature: str):
|
|
376
|
-
"""
|
|
376
|
+
"""Extract relevant key code snippets from files"""
|
|
377
377
|
for file_info in files:
|
|
378
|
-
PrettyOutput.print(f"
|
|
378
|
+
PrettyOutput.print(f"Analyzing file: {file_info['file_path']}", OutputType.INFO)
|
|
379
379
|
model = PlatformRegistry.get_global_platform_registry().get_codegen_platform()
|
|
380
380
|
model.set_suppress_output(True)
|
|
381
381
|
file_path = file_info["file_path"]
|
|
@@ -402,39 +402,39 @@ Code content:
|
|
|
402
402
|
parts = re.findall(r'<PART>\n(.*?)\n</PART>', response, re.DOTALL)
|
|
403
403
|
file_info["parts"] = parts
|
|
404
404
|
except Exception as e:
|
|
405
|
-
PrettyOutput.print(f"
|
|
405
|
+
PrettyOutput.print(f"Failed to analyze file: {str(e)}", OutputType.ERROR)
|
|
406
406
|
|
|
407
407
|
def execute(self, feature: str) -> Dict[str, Any]:
|
|
408
|
-
"""
|
|
408
|
+
"""Execute code modification"""
|
|
409
409
|
try:
|
|
410
410
|
self._prepare_execution()
|
|
411
411
|
|
|
412
|
-
#
|
|
412
|
+
# Get and select related files
|
|
413
413
|
initial_files = self._load_related_files(feature)
|
|
414
414
|
selected_files = self._select_files(initial_files)
|
|
415
415
|
|
|
416
|
-
#
|
|
416
|
+
# Whether it is a long context
|
|
417
417
|
if is_long_context([file['file_path'] for file in selected_files]):
|
|
418
418
|
self.get_key_code(selected_files, feature)
|
|
419
419
|
else:
|
|
420
420
|
for file in selected_files:
|
|
421
421
|
file["parts"] = [file["file_content"]]
|
|
422
422
|
|
|
423
|
-
#
|
|
423
|
+
# Get modification plan
|
|
424
424
|
raw_plan, structed_plan = PlanGenerator().generate_plan(feature, selected_files)
|
|
425
425
|
if not raw_plan or not structed_plan:
|
|
426
426
|
return {
|
|
427
427
|
"success": False,
|
|
428
428
|
"stdout": "",
|
|
429
|
-
"stderr": "
|
|
429
|
+
"stderr": "Failed to generate modification plan, please modify the requirement and try again",
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
-
#
|
|
432
|
+
# Execute modification
|
|
433
433
|
if PatchHandler().handle_patch_application(feature ,raw_plan, structed_plan):
|
|
434
434
|
self._finalize_changes(feature)
|
|
435
435
|
return {
|
|
436
436
|
"success": True,
|
|
437
|
-
"stdout": "
|
|
437
|
+
"stdout": "Code modification successful",
|
|
438
438
|
"stderr": "",
|
|
439
439
|
}
|
|
440
440
|
else:
|
|
@@ -442,7 +442,7 @@ Code content:
|
|
|
442
442
|
return {
|
|
443
443
|
"success": False,
|
|
444
444
|
"stdout": "",
|
|
445
|
-
"stderr": "
|
|
445
|
+
"stderr": "Code modification failed, please modify the requirement and try again",
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
except Exception as e:
|
|
@@ -450,54 +450,54 @@ Code content:
|
|
|
450
450
|
return {
|
|
451
451
|
"success": False,
|
|
452
452
|
"stdout": "",
|
|
453
|
-
"stderr": f"
|
|
453
|
+
"stderr": f"Execution failed: {str(e)}, please modify the requirement and try again",
|
|
454
454
|
"error": e
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
def main():
|
|
458
|
-
"""
|
|
458
|
+
"""Command line entry"""
|
|
459
459
|
import argparse
|
|
460
460
|
|
|
461
461
|
load_env_from_file()
|
|
462
462
|
|
|
463
|
-
parser = argparse.ArgumentParser(description='
|
|
464
|
-
parser.add_argument('-d', '--dir', help='
|
|
465
|
-
parser.add_argument('-l', '--language', help='
|
|
463
|
+
parser = argparse.ArgumentParser(description='Code modification tool')
|
|
464
|
+
parser.add_argument('-d', '--dir', help='Project root directory', default=os.getcwd())
|
|
465
|
+
parser.add_argument('-l', '--language', help='Programming language', default="python")
|
|
466
466
|
args = parser.parse_args()
|
|
467
467
|
|
|
468
468
|
tool = JarvisCoder(args.dir, args.language)
|
|
469
469
|
|
|
470
|
-
#
|
|
470
|
+
# Loop through requirements
|
|
471
471
|
while True:
|
|
472
472
|
try:
|
|
473
|
-
#
|
|
474
|
-
feature = get_multiline_input("
|
|
473
|
+
# Get requirements, pass in project root directory
|
|
474
|
+
feature = get_multiline_input("Please enter the development requirements (input empty line to exit):", tool.root_dir)
|
|
475
475
|
|
|
476
476
|
if not feature or feature == "__interrupt__":
|
|
477
477
|
break
|
|
478
478
|
|
|
479
|
-
#
|
|
479
|
+
# Execute modification
|
|
480
480
|
result = tool.execute(feature)
|
|
481
481
|
|
|
482
|
-
#
|
|
482
|
+
# Display results
|
|
483
483
|
if result["success"]:
|
|
484
484
|
PrettyOutput.print(result["stdout"], OutputType.SUCCESS)
|
|
485
485
|
else:
|
|
486
486
|
if result.get("stderr"):
|
|
487
487
|
PrettyOutput.print(result["stderr"], OutputType.WARNING)
|
|
488
|
-
if result.get("error"): #
|
|
488
|
+
if result.get("error"): # Use get() method to avoid KeyError
|
|
489
489
|
error = result["error"]
|
|
490
|
-
PrettyOutput.print(f"
|
|
491
|
-
PrettyOutput.print(f"
|
|
492
|
-
#
|
|
493
|
-
PrettyOutput.print("\
|
|
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
|
+
PrettyOutput.print("\nYou can modify the requirements and try again", OutputType.INFO)
|
|
494
494
|
|
|
495
495
|
except KeyboardInterrupt:
|
|
496
|
-
print("\
|
|
496
|
+
print("\nUser interrupted execution")
|
|
497
497
|
break
|
|
498
498
|
except Exception as e:
|
|
499
|
-
PrettyOutput.print(f"
|
|
500
|
-
PrettyOutput.print("\
|
|
499
|
+
PrettyOutput.print(f"Execution failed: {str(e)}", OutputType.ERROR)
|
|
500
|
+
PrettyOutput.print("\nYou can modify the requirements and try again", OutputType.INFO)
|
|
501
501
|
continue
|
|
502
502
|
|
|
503
503
|
return 0
|
|
@@ -506,93 +506,93 @@ if __name__ == "__main__":
|
|
|
506
506
|
exit(main())
|
|
507
507
|
|
|
508
508
|
class FilePathCompleter(Completer):
|
|
509
|
-
"""
|
|
509
|
+
"""File path auto-completer"""
|
|
510
510
|
|
|
511
511
|
def __init__(self, root_dir: str):
|
|
512
512
|
self.root_dir = root_dir
|
|
513
513
|
self._file_list = None
|
|
514
514
|
|
|
515
515
|
def _get_files(self) -> List[str]:
|
|
516
|
-
"""
|
|
516
|
+
"""Get the list of files managed by git"""
|
|
517
517
|
if self._file_list is None:
|
|
518
518
|
try:
|
|
519
|
-
#
|
|
519
|
+
# Switch to project root directory
|
|
520
520
|
old_cwd = os.getcwd()
|
|
521
521
|
os.chdir(self.root_dir)
|
|
522
522
|
|
|
523
|
-
#
|
|
523
|
+
# Get the list of files managed by git
|
|
524
524
|
self._file_list = os.popen("git ls-files").read().splitlines()
|
|
525
525
|
|
|
526
|
-
#
|
|
526
|
+
# Restore working directory
|
|
527
527
|
os.chdir(old_cwd)
|
|
528
528
|
except Exception as e:
|
|
529
|
-
PrettyOutput.print(f"
|
|
529
|
+
PrettyOutput.print(f"Failed to get file list: {str(e)}", OutputType.WARNING)
|
|
530
530
|
self._file_list = []
|
|
531
531
|
return self._file_list
|
|
532
532
|
|
|
533
533
|
def get_completions(self, document, complete_event):
|
|
534
|
-
"""
|
|
534
|
+
"""Get completion suggestions"""
|
|
535
535
|
text_before_cursor = document.text_before_cursor
|
|
536
536
|
|
|
537
|
-
#
|
|
537
|
+
# Check if @ was just entered
|
|
538
538
|
if text_before_cursor.endswith('@'):
|
|
539
|
-
#
|
|
539
|
+
# Display all files
|
|
540
540
|
for path in self._get_files():
|
|
541
541
|
yield Completion(path, start_position=0)
|
|
542
542
|
return
|
|
543
543
|
|
|
544
|
-
#
|
|
544
|
+
# Check if there was an @ before, and get the search word after @
|
|
545
545
|
at_pos = text_before_cursor.rfind('@')
|
|
546
546
|
if at_pos == -1:
|
|
547
547
|
return
|
|
548
548
|
|
|
549
549
|
search = text_before_cursor[at_pos + 1:].lower().strip()
|
|
550
550
|
|
|
551
|
-
#
|
|
551
|
+
# Provide matching file suggestions
|
|
552
552
|
for path in self._get_files():
|
|
553
553
|
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
|
-
#
|
|
554
|
+
if (search in path_lower or # Directly included
|
|
555
|
+
search in os.path.basename(path_lower) or # File name included
|
|
556
|
+
any(fnmatch.fnmatch(path_lower, f'*{s}*') for s in search.split())): # Wildcard matching
|
|
557
|
+
# Calculate the correct start_position
|
|
558
558
|
yield Completion(path, start_position=-(len(search)))
|
|
559
559
|
|
|
560
560
|
class SmartCompleter(Completer):
|
|
561
|
-
"""
|
|
561
|
+
"""Smart auto-completer, combine word and file path completion"""
|
|
562
562
|
|
|
563
563
|
def __init__(self, word_completer: WordCompleter, file_completer: FilePathCompleter):
|
|
564
564
|
self.word_completer = word_completer
|
|
565
565
|
self.file_completer = file_completer
|
|
566
566
|
|
|
567
567
|
def get_completions(self, document, complete_event):
|
|
568
|
-
"""
|
|
569
|
-
#
|
|
568
|
+
"""Get completion suggestions"""
|
|
569
|
+
# If the current line ends with @, use file completion
|
|
570
570
|
if document.text_before_cursor.strip().endswith('@'):
|
|
571
571
|
yield from self.file_completer.get_completions(document, complete_event)
|
|
572
572
|
else:
|
|
573
|
-
#
|
|
573
|
+
# Otherwise, use word completion
|
|
574
574
|
yield from self.word_completer.get_completions(document, complete_event)
|
|
575
575
|
|
|
576
576
|
def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
|
|
577
|
-
"""
|
|
577
|
+
"""Get multi-line input, support file path auto-completion function
|
|
578
578
|
|
|
579
579
|
Args:
|
|
580
|
-
prompt_text:
|
|
581
|
-
root_dir:
|
|
580
|
+
prompt_text: Prompt text
|
|
581
|
+
root_dir: Project root directory, for file completion
|
|
582
582
|
|
|
583
583
|
Returns:
|
|
584
|
-
str:
|
|
584
|
+
str: User input text
|
|
585
585
|
"""
|
|
586
|
-
#
|
|
586
|
+
# Create file completion
|
|
587
587
|
file_completer = FilePathCompleter(root_dir or os.getcwd())
|
|
588
588
|
|
|
589
|
-
#
|
|
589
|
+
# Create prompt style
|
|
590
590
|
style = Style.from_dict({
|
|
591
591
|
'prompt': 'ansicyan bold',
|
|
592
592
|
'input': 'ansiwhite',
|
|
593
593
|
})
|
|
594
594
|
|
|
595
|
-
#
|
|
595
|
+
# Create session
|
|
596
596
|
session = PromptSession(
|
|
597
597
|
completer=file_completer,
|
|
598
598
|
style=style,
|
|
@@ -601,20 +601,20 @@ def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
|
|
|
601
601
|
complete_while_typing=True
|
|
602
602
|
)
|
|
603
603
|
|
|
604
|
-
#
|
|
604
|
+
# Display initial prompt text
|
|
605
605
|
print(f"\n{prompt_text}")
|
|
606
606
|
|
|
607
|
-
#
|
|
607
|
+
# Create prompt
|
|
608
608
|
prompt = FormattedText([
|
|
609
609
|
('class:prompt', ">>> ")
|
|
610
610
|
])
|
|
611
611
|
|
|
612
|
-
#
|
|
612
|
+
# Get input
|
|
613
613
|
lines = []
|
|
614
614
|
try:
|
|
615
615
|
while True:
|
|
616
616
|
line = session.prompt(prompt).strip()
|
|
617
|
-
if not line: #
|
|
617
|
+
if not line: # Empty line means input end
|
|
618
618
|
break
|
|
619
619
|
lines.append(line)
|
|
620
620
|
except KeyboardInterrupt:
|