jarvis-ai-assistant 0.1.96__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/agent.py +138 -144
- jarvis/jarvis_codebase/main.py +87 -54
- jarvis/jarvis_coder/git_utils.py +22 -25
- jarvis/jarvis_coder/main.py +166 -171
- jarvis/jarvis_coder/patch_handler.py +153 -453
- jarvis/jarvis_coder/plan_generator.py +76 -48
- jarvis/jarvis_platform/main.py +39 -39
- jarvis/jarvis_rag/main.py +182 -182
- jarvis/jarvis_smart_shell/main.py +34 -34
- jarvis/main.py +24 -24
- jarvis/models/ai8.py +22 -22
- jarvis/models/base.py +17 -13
- jarvis/models/kimi.py +31 -31
- jarvis/models/ollama.py +28 -28
- jarvis/models/openai.py +22 -24
- jarvis/models/oyi.py +25 -25
- jarvis/models/registry.py +33 -34
- jarvis/tools/ask_user.py +5 -5
- jarvis/tools/base.py +2 -2
- jarvis/tools/chdir.py +9 -9
- jarvis/tools/codebase_qa.py +4 -4
- jarvis/tools/coder.py +4 -4
- jarvis/tools/file_ops.py +1 -1
- jarvis/tools/generator.py +23 -23
- jarvis/tools/methodology.py +4 -4
- jarvis/tools/rag.py +4 -4
- jarvis/tools/registry.py +38 -38
- jarvis/tools/search.py +42 -42
- jarvis/tools/shell.py +13 -13
- jarvis/tools/sub_agent.py +16 -16
- jarvis/tools/thinker.py +41 -41
- jarvis/tools/webpage.py +17 -17
- jarvis/utils.py +59 -60
- {jarvis_ai_assistant-0.1.96.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/METADATA +1 -1
- jarvis_ai_assistant-0.1.98.dist-info/RECORD +47 -0
- jarvis_ai_assistant-0.1.96.dist-info/RECORD +0 -47
- {jarvis_ai_assistant-0.1.96.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.96.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.96.dist-info → jarvis_ai_assistant-0.1.98.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.1.96.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,158 +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
|
-
commit_message = generate_commit_message(git_diff
|
|
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
|
-
for file, score
|
|
133
|
-
PrettyOutput.print(f"
|
|
134
|
-
|
|
135
|
-
content = f.read()
|
|
132
|
+
for file, score in related_files:
|
|
133
|
+
PrettyOutput.print(f"Related file: {file} (score: {score:.3f})", OutputType.SUCCESS)
|
|
134
|
+
content = open(file, "r", encoding="utf-8").read()
|
|
136
135
|
ret.append({"file_path": file, "file_content": content})
|
|
137
136
|
return ret
|
|
138
137
|
|
|
139
138
|
def _parse_file_selection(self, input_str: str, max_index: int) -> List[int]:
|
|
140
|
-
"""
|
|
139
|
+
"""Parse file selection expression
|
|
141
140
|
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
-
|
|
141
|
+
Supported formats:
|
|
142
|
+
- Single number: "1"
|
|
143
|
+
- Comma-separated: "1,3,5"
|
|
144
|
+
- Range: "1-5"
|
|
145
|
+
- Combination: "1,3-5,7"
|
|
147
146
|
|
|
148
147
|
Args:
|
|
149
|
-
input_str:
|
|
150
|
-
max_index:
|
|
148
|
+
input_str: User input selection expression
|
|
149
|
+
max_index: Maximum selectable index
|
|
151
150
|
|
|
152
151
|
Returns:
|
|
153
|
-
List[int]:
|
|
152
|
+
List[int]: Selected index list (starting from 0)
|
|
154
153
|
"""
|
|
155
154
|
selected = set()
|
|
156
155
|
|
|
157
|
-
#
|
|
156
|
+
# Remove all whitespace characters
|
|
158
157
|
input_str = "".join(input_str.split())
|
|
159
158
|
|
|
160
|
-
#
|
|
159
|
+
# Process comma-separated parts
|
|
161
160
|
for part in input_str.split(","):
|
|
162
161
|
if not part:
|
|
163
162
|
continue
|
|
164
163
|
|
|
165
|
-
#
|
|
164
|
+
# Process range (e.g.: 3-6)
|
|
166
165
|
if "-" in part:
|
|
167
166
|
try:
|
|
168
167
|
start, end = map(int, part.split("-"))
|
|
169
|
-
#
|
|
168
|
+
# Convert to index starting from 0
|
|
170
169
|
start = max(0, start - 1)
|
|
171
170
|
end = min(max_index, end - 1)
|
|
172
171
|
if start <= end:
|
|
173
172
|
selected.update(range(start, end + 1))
|
|
174
173
|
except ValueError:
|
|
175
|
-
PrettyOutput.print(f"
|
|
176
|
-
#
|
|
174
|
+
PrettyOutput.print(f"Ignore invalid range expression: {part}", OutputType.WARNING)
|
|
175
|
+
# Process single number
|
|
177
176
|
else:
|
|
178
177
|
try:
|
|
179
|
-
index = int(part) - 1 #
|
|
178
|
+
index = int(part) - 1 # Convert to index starting from 0
|
|
180
179
|
if 0 <= index < max_index:
|
|
181
180
|
selected.add(index)
|
|
182
181
|
else:
|
|
183
|
-
PrettyOutput.print(f"
|
|
182
|
+
PrettyOutput.print(f"Ignore index out of range: {part}", OutputType.WARNING)
|
|
184
183
|
except ValueError:
|
|
185
|
-
PrettyOutput.print(f"
|
|
184
|
+
PrettyOutput.print(f"Ignore invalid number: {part}", OutputType.WARNING)
|
|
186
185
|
|
|
187
186
|
return sorted(list(selected))
|
|
188
187
|
|
|
189
188
|
def _get_file_completer(self) -> Completer:
|
|
190
|
-
"""
|
|
189
|
+
"""Create file path completer"""
|
|
191
190
|
class FileCompleter(Completer):
|
|
192
191
|
def __init__(self, root_dir: str):
|
|
193
192
|
self.root_dir = root_dir
|
|
194
193
|
|
|
195
194
|
def get_completions(self, document, complete_event):
|
|
196
|
-
#
|
|
195
|
+
# Get the text of the current input
|
|
197
196
|
text = document.text_before_cursor
|
|
198
197
|
|
|
199
|
-
#
|
|
198
|
+
# If the input is empty, return all files in the root directory
|
|
200
199
|
if not text:
|
|
201
200
|
for path in self._list_files(""):
|
|
202
201
|
yield Completion(path, start_position=0)
|
|
203
202
|
return
|
|
204
203
|
|
|
205
|
-
#
|
|
204
|
+
# Get the current directory and partial file name
|
|
206
205
|
current_dir = os.path.dirname(text)
|
|
207
206
|
file_prefix = os.path.basename(text)
|
|
208
207
|
|
|
209
|
-
#
|
|
208
|
+
# List matching files
|
|
210
209
|
search_dir = os.path.join(self.root_dir, current_dir) if current_dir else self.root_dir
|
|
211
210
|
if os.path.isdir(search_dir):
|
|
212
211
|
for path in self._list_files(current_dir):
|
|
@@ -214,7 +213,7 @@ class JarvisCoder:
|
|
|
214
213
|
yield Completion(path, start_position=-len(text))
|
|
215
214
|
|
|
216
215
|
def _list_files(self, current_dir: str) -> List[str]:
|
|
217
|
-
"""
|
|
216
|
+
"""List all files in the specified directory (recursively)"""
|
|
218
217
|
files = []
|
|
219
218
|
search_dir = os.path.join(self.root_dir, current_dir)
|
|
220
219
|
|
|
@@ -222,7 +221,7 @@ class JarvisCoder:
|
|
|
222
221
|
for filename in filenames:
|
|
223
222
|
full_path = os.path.join(root, filename)
|
|
224
223
|
rel_path = os.path.relpath(full_path, self.root_dir)
|
|
225
|
-
#
|
|
224
|
+
# Ignore .git directory and other hidden files
|
|
226
225
|
if not any(part.startswith('.') for part in rel_path.split(os.sep)):
|
|
227
226
|
files.append(rel_path)
|
|
228
227
|
|
|
@@ -231,13 +230,13 @@ class JarvisCoder:
|
|
|
231
230
|
return FileCompleter(self.root_dir)
|
|
232
231
|
|
|
233
232
|
def _fuzzy_match_files(self, pattern: str) -> List[str]:
|
|
234
|
-
"""
|
|
233
|
+
"""Fuzzy match file path
|
|
235
234
|
|
|
236
235
|
Args:
|
|
237
|
-
pattern:
|
|
236
|
+
pattern: Matching pattern
|
|
238
237
|
|
|
239
238
|
Returns:
|
|
240
|
-
List[str]:
|
|
239
|
+
List[str]: List of matching file paths
|
|
241
240
|
"""
|
|
242
241
|
matches = []
|
|
243
242
|
|
|
@@ -259,38 +258,38 @@ class JarvisCoder:
|
|
|
259
258
|
return sorted(matches)
|
|
260
259
|
|
|
261
260
|
def _select_files(self, related_files: List[Dict]) -> List[Dict]:
|
|
262
|
-
"""
|
|
263
|
-
PrettyOutput.section("
|
|
261
|
+
"""Let the user select and supplement related files"""
|
|
262
|
+
PrettyOutput.section("Related files", OutputType.INFO)
|
|
264
263
|
|
|
265
|
-
#
|
|
266
|
-
selected_files = list(related_files) #
|
|
264
|
+
# Display found files
|
|
265
|
+
selected_files = list(related_files) # Default select all
|
|
267
266
|
for i, file in enumerate(related_files, 1):
|
|
268
267
|
PrettyOutput.print(f"[{i}] {file['file_path']}", OutputType.INFO)
|
|
269
268
|
|
|
270
|
-
#
|
|
271
|
-
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'
|
|
272
271
|
if user_input == 'y':
|
|
273
|
-
#
|
|
274
|
-
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)
|
|
275
274
|
numbers = input(">>> ").strip()
|
|
276
275
|
if numbers:
|
|
277
276
|
selected_indices = self._parse_file_selection(numbers, len(related_files))
|
|
278
277
|
if selected_indices:
|
|
279
278
|
selected_files = [related_files[i] for i in selected_indices]
|
|
280
279
|
else:
|
|
281
|
-
PrettyOutput.print("
|
|
280
|
+
PrettyOutput.print("No valid files selected, keep the current selection", OutputType.WARNING)
|
|
282
281
|
|
|
283
|
-
#
|
|
284
|
-
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'
|
|
285
284
|
if user_input == 'y':
|
|
286
|
-
#
|
|
285
|
+
# Create file completion session
|
|
287
286
|
session = PromptSession(
|
|
288
287
|
completer=self._get_file_completer(),
|
|
289
288
|
complete_while_typing=True
|
|
290
289
|
)
|
|
291
290
|
|
|
292
291
|
while True:
|
|
293
|
-
PrettyOutput.print("\
|
|
292
|
+
PrettyOutput.print("\nPlease enter the file path to supplement (support Tab completion and *? wildcard, input empty line to end):", OutputType.INFO)
|
|
294
293
|
try:
|
|
295
294
|
file_path = session.prompt(">>> ").strip()
|
|
296
295
|
except KeyboardInterrupt:
|
|
@@ -299,20 +298,20 @@ class JarvisCoder:
|
|
|
299
298
|
if not file_path:
|
|
300
299
|
break
|
|
301
300
|
|
|
302
|
-
#
|
|
301
|
+
# Process wildcard matching
|
|
303
302
|
if '*' in file_path or '?' in file_path:
|
|
304
303
|
matches = self._fuzzy_match_files(file_path)
|
|
305
304
|
if not matches:
|
|
306
|
-
PrettyOutput.print("
|
|
305
|
+
PrettyOutput.print("No matching files found", OutputType.WARNING)
|
|
307
306
|
continue
|
|
308
307
|
|
|
309
|
-
#
|
|
310
|
-
PrettyOutput.print("\
|
|
308
|
+
# Display matching files
|
|
309
|
+
PrettyOutput.print("\nFound the following matching files:", OutputType.INFO)
|
|
311
310
|
for i, path in enumerate(matches, 1):
|
|
312
311
|
PrettyOutput.print(f"[{i}] {path}", OutputType.INFO)
|
|
313
312
|
|
|
314
|
-
#
|
|
315
|
-
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()
|
|
316
315
|
if numbers:
|
|
317
316
|
indices = self._parse_file_selection(numbers, len(matches))
|
|
318
317
|
if not indices:
|
|
@@ -323,11 +322,11 @@ class JarvisCoder:
|
|
|
323
322
|
else:
|
|
324
323
|
paths_to_add = [file_path]
|
|
325
324
|
|
|
326
|
-
#
|
|
325
|
+
# Add selected files
|
|
327
326
|
for path in paths_to_add:
|
|
328
327
|
full_path = os.path.join(self.root_dir, path)
|
|
329
328
|
if not os.path.isfile(full_path):
|
|
330
|
-
PrettyOutput.print(f"
|
|
329
|
+
PrettyOutput.print(f"File does not exist: {path}", OutputType.ERROR)
|
|
331
330
|
continue
|
|
332
331
|
|
|
333
332
|
try:
|
|
@@ -337,109 +336,105 @@ class JarvisCoder:
|
|
|
337
336
|
"file_path": path,
|
|
338
337
|
"file_content": content
|
|
339
338
|
})
|
|
340
|
-
PrettyOutput.print(f"
|
|
339
|
+
PrettyOutput.print(f"File added: {path}", OutputType.SUCCESS)
|
|
341
340
|
except Exception as e:
|
|
342
|
-
PrettyOutput.print(f"
|
|
341
|
+
PrettyOutput.print(f"Failed to read file: {str(e)}", OutputType.ERROR)
|
|
343
342
|
|
|
344
343
|
return selected_files
|
|
345
344
|
|
|
346
345
|
def _finalize_changes(self, feature: str) -> None:
|
|
347
|
-
"""
|
|
348
|
-
PrettyOutput.print("
|
|
346
|
+
"""Complete changes and commit"""
|
|
347
|
+
PrettyOutput.print("Modification confirmed, committing...", OutputType.INFO)
|
|
349
348
|
|
|
350
|
-
#
|
|
349
|
+
# Add only modified files under git control
|
|
351
350
|
os.system("git add -u")
|
|
352
351
|
|
|
353
|
-
#
|
|
352
|
+
# Then get git diff
|
|
354
353
|
git_diff = os.popen("git diff --cached").read()
|
|
355
354
|
|
|
356
|
-
#
|
|
357
|
-
commit_message = generate_commit_message(git_diff
|
|
355
|
+
# Automatically generate commit information, pass in feature
|
|
356
|
+
commit_message = generate_commit_message(git_diff)
|
|
358
357
|
|
|
359
|
-
#
|
|
360
|
-
PrettyOutput.print(f"
|
|
361
|
-
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"
|
|
362
361
|
|
|
363
362
|
if user_confirm.lower() != "y":
|
|
364
|
-
commit_message = input("
|
|
363
|
+
commit_message = input("Please enter a new commit information: ")
|
|
365
364
|
|
|
366
|
-
#
|
|
365
|
+
# No need to git add again, it has already been added
|
|
367
366
|
os.system(f"git commit -m '{commit_message}'")
|
|
368
367
|
save_edit_record(self.record_dir, commit_message, git_diff)
|
|
369
368
|
|
|
370
369
|
def _revert_changes(self) -> None:
|
|
371
|
-
"""
|
|
372
|
-
PrettyOutput.print("
|
|
370
|
+
"""Revert all changes"""
|
|
371
|
+
PrettyOutput.print("Modification cancelled, reverting changes", OutputType.INFO)
|
|
373
372
|
os.system(f"git reset --hard")
|
|
374
373
|
os.system(f"git clean -df")
|
|
375
374
|
|
|
376
375
|
def get_key_code(self, files: List[Dict], feature: str):
|
|
377
|
-
"""
|
|
376
|
+
"""Extract relevant key code snippets from files"""
|
|
378
377
|
for file_info in files:
|
|
379
|
-
PrettyOutput.print(f"
|
|
378
|
+
PrettyOutput.print(f"Analyzing file: {file_info['file_path']}", OutputType.INFO)
|
|
380
379
|
model = PlatformRegistry.get_global_platform_registry().get_codegen_platform()
|
|
381
380
|
model.set_suppress_output(True)
|
|
382
381
|
file_path = file_info["file_path"]
|
|
383
382
|
content = file_info["file_content"]
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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:
|
|
388
387
|
<PART>
|
|
389
388
|
content
|
|
390
389
|
</PART>
|
|
391
390
|
|
|
392
|
-
|
|
393
|
-
"""
|
|
394
|
-
model.set_system_message(system_message)
|
|
395
|
-
|
|
396
|
-
try:
|
|
391
|
+
Multiple snippets can be returned. If the file content is not relevant to the requirement, return empty.
|
|
397
392
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
393
|
+
Requirement: {feature}
|
|
394
|
+
File path: {file_path}
|
|
395
|
+
Code content:
|
|
401
396
|
{content}
|
|
402
397
|
"""
|
|
403
398
|
|
|
404
399
|
# 调用大模型进行分析
|
|
405
|
-
response =
|
|
400
|
+
response = model.chat_until_success(prompt)
|
|
406
401
|
|
|
407
402
|
parts = re.findall(r'<PART>\n(.*?)\n</PART>', response, re.DOTALL)
|
|
408
403
|
file_info["parts"] = parts
|
|
409
404
|
except Exception as e:
|
|
410
|
-
PrettyOutput.print(f"
|
|
405
|
+
PrettyOutput.print(f"Failed to analyze file: {str(e)}", OutputType.ERROR)
|
|
411
406
|
|
|
412
407
|
def execute(self, feature: str) -> Dict[str, Any]:
|
|
413
|
-
"""
|
|
408
|
+
"""Execute code modification"""
|
|
414
409
|
try:
|
|
415
410
|
self._prepare_execution()
|
|
416
411
|
|
|
417
|
-
#
|
|
412
|
+
# Get and select related files
|
|
418
413
|
initial_files = self._load_related_files(feature)
|
|
419
414
|
selected_files = self._select_files(initial_files)
|
|
420
415
|
|
|
421
|
-
#
|
|
416
|
+
# Whether it is a long context
|
|
422
417
|
if is_long_context([file['file_path'] for file in selected_files]):
|
|
423
418
|
self.get_key_code(selected_files, feature)
|
|
424
419
|
else:
|
|
425
420
|
for file in selected_files:
|
|
426
421
|
file["parts"] = [file["file_content"]]
|
|
427
422
|
|
|
428
|
-
#
|
|
429
|
-
|
|
430
|
-
if not
|
|
423
|
+
# Get modification plan
|
|
424
|
+
raw_plan, structed_plan = PlanGenerator().generate_plan(feature, selected_files)
|
|
425
|
+
if not raw_plan or not structed_plan:
|
|
431
426
|
return {
|
|
432
427
|
"success": False,
|
|
433
428
|
"stdout": "",
|
|
434
|
-
"stderr": "
|
|
429
|
+
"stderr": "Failed to generate modification plan, please modify the requirement and try again",
|
|
435
430
|
}
|
|
436
431
|
|
|
437
|
-
#
|
|
438
|
-
if PatchHandler().handle_patch_application(
|
|
432
|
+
# Execute modification
|
|
433
|
+
if PatchHandler().handle_patch_application(feature ,raw_plan, structed_plan):
|
|
439
434
|
self._finalize_changes(feature)
|
|
440
435
|
return {
|
|
441
436
|
"success": True,
|
|
442
|
-
"stdout": "
|
|
437
|
+
"stdout": "Code modification successful",
|
|
443
438
|
"stderr": "",
|
|
444
439
|
}
|
|
445
440
|
else:
|
|
@@ -447,7 +442,7 @@ content
|
|
|
447
442
|
return {
|
|
448
443
|
"success": False,
|
|
449
444
|
"stdout": "",
|
|
450
|
-
"stderr": "
|
|
445
|
+
"stderr": "Code modification failed, please modify the requirement and try again",
|
|
451
446
|
}
|
|
452
447
|
|
|
453
448
|
except Exception as e:
|
|
@@ -455,54 +450,54 @@ content
|
|
|
455
450
|
return {
|
|
456
451
|
"success": False,
|
|
457
452
|
"stdout": "",
|
|
458
|
-
"stderr": f"
|
|
453
|
+
"stderr": f"Execution failed: {str(e)}, please modify the requirement and try again",
|
|
459
454
|
"error": e
|
|
460
455
|
}
|
|
461
456
|
|
|
462
457
|
def main():
|
|
463
|
-
"""
|
|
458
|
+
"""Command line entry"""
|
|
464
459
|
import argparse
|
|
465
460
|
|
|
466
461
|
load_env_from_file()
|
|
467
462
|
|
|
468
|
-
parser = argparse.ArgumentParser(description='
|
|
469
|
-
parser.add_argument('-d', '--dir', help='
|
|
470
|
-
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")
|
|
471
466
|
args = parser.parse_args()
|
|
472
467
|
|
|
473
468
|
tool = JarvisCoder(args.dir, args.language)
|
|
474
469
|
|
|
475
|
-
#
|
|
470
|
+
# Loop through requirements
|
|
476
471
|
while True:
|
|
477
472
|
try:
|
|
478
|
-
#
|
|
479
|
-
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)
|
|
480
475
|
|
|
481
476
|
if not feature or feature == "__interrupt__":
|
|
482
477
|
break
|
|
483
478
|
|
|
484
|
-
#
|
|
479
|
+
# Execute modification
|
|
485
480
|
result = tool.execute(feature)
|
|
486
481
|
|
|
487
|
-
#
|
|
482
|
+
# Display results
|
|
488
483
|
if result["success"]:
|
|
489
484
|
PrettyOutput.print(result["stdout"], OutputType.SUCCESS)
|
|
490
485
|
else:
|
|
491
486
|
if result.get("stderr"):
|
|
492
487
|
PrettyOutput.print(result["stderr"], OutputType.WARNING)
|
|
493
|
-
if result.get("error"): #
|
|
488
|
+
if result.get("error"): # Use get() method to avoid KeyError
|
|
494
489
|
error = result["error"]
|
|
495
|
-
PrettyOutput.print(f"
|
|
496
|
-
PrettyOutput.print(f"
|
|
497
|
-
#
|
|
498
|
-
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)
|
|
499
494
|
|
|
500
495
|
except KeyboardInterrupt:
|
|
501
|
-
print("\
|
|
496
|
+
print("\nUser interrupted execution")
|
|
502
497
|
break
|
|
503
498
|
except Exception as e:
|
|
504
|
-
PrettyOutput.print(f"
|
|
505
|
-
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)
|
|
506
501
|
continue
|
|
507
502
|
|
|
508
503
|
return 0
|
|
@@ -511,93 +506,93 @@ if __name__ == "__main__":
|
|
|
511
506
|
exit(main())
|
|
512
507
|
|
|
513
508
|
class FilePathCompleter(Completer):
|
|
514
|
-
"""
|
|
509
|
+
"""File path auto-completer"""
|
|
515
510
|
|
|
516
511
|
def __init__(self, root_dir: str):
|
|
517
512
|
self.root_dir = root_dir
|
|
518
513
|
self._file_list = None
|
|
519
514
|
|
|
520
515
|
def _get_files(self) -> List[str]:
|
|
521
|
-
"""
|
|
516
|
+
"""Get the list of files managed by git"""
|
|
522
517
|
if self._file_list is None:
|
|
523
518
|
try:
|
|
524
|
-
#
|
|
519
|
+
# Switch to project root directory
|
|
525
520
|
old_cwd = os.getcwd()
|
|
526
521
|
os.chdir(self.root_dir)
|
|
527
522
|
|
|
528
|
-
#
|
|
523
|
+
# Get the list of files managed by git
|
|
529
524
|
self._file_list = os.popen("git ls-files").read().splitlines()
|
|
530
525
|
|
|
531
|
-
#
|
|
526
|
+
# Restore working directory
|
|
532
527
|
os.chdir(old_cwd)
|
|
533
528
|
except Exception as e:
|
|
534
|
-
PrettyOutput.print(f"
|
|
529
|
+
PrettyOutput.print(f"Failed to get file list: {str(e)}", OutputType.WARNING)
|
|
535
530
|
self._file_list = []
|
|
536
531
|
return self._file_list
|
|
537
532
|
|
|
538
533
|
def get_completions(self, document, complete_event):
|
|
539
|
-
"""
|
|
534
|
+
"""Get completion suggestions"""
|
|
540
535
|
text_before_cursor = document.text_before_cursor
|
|
541
536
|
|
|
542
|
-
#
|
|
537
|
+
# Check if @ was just entered
|
|
543
538
|
if text_before_cursor.endswith('@'):
|
|
544
|
-
#
|
|
539
|
+
# Display all files
|
|
545
540
|
for path in self._get_files():
|
|
546
541
|
yield Completion(path, start_position=0)
|
|
547
542
|
return
|
|
548
543
|
|
|
549
|
-
#
|
|
544
|
+
# Check if there was an @ before, and get the search word after @
|
|
550
545
|
at_pos = text_before_cursor.rfind('@')
|
|
551
546
|
if at_pos == -1:
|
|
552
547
|
return
|
|
553
548
|
|
|
554
549
|
search = text_before_cursor[at_pos + 1:].lower().strip()
|
|
555
550
|
|
|
556
|
-
#
|
|
551
|
+
# Provide matching file suggestions
|
|
557
552
|
for path in self._get_files():
|
|
558
553
|
path_lower = path.lower()
|
|
559
|
-
if (search in path_lower or #
|
|
560
|
-
search in os.path.basename(path_lower) or #
|
|
561
|
-
any(fnmatch.fnmatch(path_lower, f'*{s}*') for s in search.split())): #
|
|
562
|
-
#
|
|
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
|
|
563
558
|
yield Completion(path, start_position=-(len(search)))
|
|
564
559
|
|
|
565
560
|
class SmartCompleter(Completer):
|
|
566
|
-
"""
|
|
561
|
+
"""Smart auto-completer, combine word and file path completion"""
|
|
567
562
|
|
|
568
563
|
def __init__(self, word_completer: WordCompleter, file_completer: FilePathCompleter):
|
|
569
564
|
self.word_completer = word_completer
|
|
570
565
|
self.file_completer = file_completer
|
|
571
566
|
|
|
572
567
|
def get_completions(self, document, complete_event):
|
|
573
|
-
"""
|
|
574
|
-
#
|
|
568
|
+
"""Get completion suggestions"""
|
|
569
|
+
# If the current line ends with @, use file completion
|
|
575
570
|
if document.text_before_cursor.strip().endswith('@'):
|
|
576
571
|
yield from self.file_completer.get_completions(document, complete_event)
|
|
577
572
|
else:
|
|
578
|
-
#
|
|
573
|
+
# Otherwise, use word completion
|
|
579
574
|
yield from self.word_completer.get_completions(document, complete_event)
|
|
580
575
|
|
|
581
576
|
def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
|
|
582
|
-
"""
|
|
577
|
+
"""Get multi-line input, support file path auto-completion function
|
|
583
578
|
|
|
584
579
|
Args:
|
|
585
|
-
prompt_text:
|
|
586
|
-
root_dir:
|
|
580
|
+
prompt_text: Prompt text
|
|
581
|
+
root_dir: Project root directory, for file completion
|
|
587
582
|
|
|
588
583
|
Returns:
|
|
589
|
-
str:
|
|
584
|
+
str: User input text
|
|
590
585
|
"""
|
|
591
|
-
#
|
|
586
|
+
# Create file completion
|
|
592
587
|
file_completer = FilePathCompleter(root_dir or os.getcwd())
|
|
593
588
|
|
|
594
|
-
#
|
|
589
|
+
# Create prompt style
|
|
595
590
|
style = Style.from_dict({
|
|
596
591
|
'prompt': 'ansicyan bold',
|
|
597
592
|
'input': 'ansiwhite',
|
|
598
593
|
})
|
|
599
594
|
|
|
600
|
-
#
|
|
595
|
+
# Create session
|
|
601
596
|
session = PromptSession(
|
|
602
597
|
completer=file_completer,
|
|
603
598
|
style=style,
|
|
@@ -606,20 +601,20 @@ def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
|
|
|
606
601
|
complete_while_typing=True
|
|
607
602
|
)
|
|
608
603
|
|
|
609
|
-
#
|
|
604
|
+
# Display initial prompt text
|
|
610
605
|
print(f"\n{prompt_text}")
|
|
611
606
|
|
|
612
|
-
#
|
|
607
|
+
# Create prompt
|
|
613
608
|
prompt = FormattedText([
|
|
614
609
|
('class:prompt', ">>> ")
|
|
615
610
|
])
|
|
616
611
|
|
|
617
|
-
#
|
|
612
|
+
# Get input
|
|
618
613
|
lines = []
|
|
619
614
|
try:
|
|
620
615
|
while True:
|
|
621
616
|
line = session.prompt(prompt).strip()
|
|
622
|
-
if not line: #
|
|
617
|
+
if not line: # Empty line means input end
|
|
623
618
|
break
|
|
624
619
|
lines.append(line)
|
|
625
620
|
except KeyboardInterrupt:
|