jarvis-ai-assistant 0.1.98__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 +64 -2
- jarvis/jarvis_coder/main.py +11 -389
- jarvis/jarvis_coder/patch_handler.py +84 -14
- jarvis/jarvis_coder/plan_generator.py +49 -7
- jarvis/jarvis_rag/main.py +9 -9
- jarvis/jarvis_smart_shell/main.py +5 -7
- 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.98.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.98.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.98.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.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.98.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
|
# 全局锁对象
|
|
@@ -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,7 +84,6 @@ 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": "",
|
|
@@ -557,21 +194,6 @@ class FilePathCompleter(Completer):
|
|
|
557
194
|
# Calculate the correct start_position
|
|
558
195
|
yield Completion(path, start_position=-(len(search)))
|
|
559
196
|
|
|
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
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
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import os
|
|
3
3
|
from typing import List, Tuple, Dict
|
|
4
|
+
import yaml
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
|
|
7
|
+
from jarvis.jarvis_coder.git_utils import generate_commit_message, init_git_repo, save_edit_record
|
|
5
8
|
from jarvis.models.base import BasePlatform
|
|
6
9
|
from jarvis.models.registry import PlatformRegistry
|
|
7
|
-
from jarvis.utils import OutputType, PrettyOutput, get_multiline_input, while_success
|
|
10
|
+
from jarvis.utils import OutputType, PrettyOutput, get_multiline_input, get_single_line_input, while_success
|
|
8
11
|
|
|
9
12
|
class Patch:
|
|
10
13
|
def __init__(self, old_code: str, new_code: str):
|
|
@@ -12,7 +15,32 @@ class Patch:
|
|
|
12
15
|
self.new_code = new_code
|
|
13
16
|
|
|
14
17
|
class PatchHandler:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.prompt_file = Path.home() / ".jarvis-coder-patch-prompt"
|
|
20
|
+
self.additional_info = self._load_additional_info()
|
|
21
|
+
self.root_dir = init_git_repo(os.getcwd())
|
|
22
|
+
self.record_dir = os.path.join(self.root_dir, ".jarvis-coder", "record")
|
|
23
|
+
if not os.path.exists(self.record_dir):
|
|
24
|
+
os.makedirs(self.record_dir)
|
|
25
|
+
def _load_additional_info(self) -> str:
|
|
26
|
+
"""Load saved additional info from prompt file"""
|
|
27
|
+
if not self.prompt_file.exists():
|
|
28
|
+
return ""
|
|
29
|
+
try:
|
|
30
|
+
with open(self.prompt_file, 'r') as f:
|
|
31
|
+
data = yaml.safe_load(f)
|
|
32
|
+
return data.get('additional_info', '') if data else ''
|
|
33
|
+
except Exception as e:
|
|
34
|
+
PrettyOutput.print(f"Failed to load additional info: {e}", OutputType.WARNING)
|
|
35
|
+
return ""
|
|
15
36
|
|
|
37
|
+
def _save_additional_info(self, info: str):
|
|
38
|
+
"""Save additional info to prompt file"""
|
|
39
|
+
try:
|
|
40
|
+
with open(self.prompt_file, 'w') as f:
|
|
41
|
+
yaml.dump({'additional_info': info}, f)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
PrettyOutput.print(f"Failed to save additional info: {e}", OutputType.WARNING)
|
|
16
44
|
|
|
17
45
|
def _extract_patches(self, response: str) -> List[Patch]:
|
|
18
46
|
"""Extract patches from response
|
|
@@ -34,7 +62,7 @@ class PatchHandler:
|
|
|
34
62
|
def _confirm_and_apply_changes(self, file_path: str) -> bool:
|
|
35
63
|
"""Confirm and apply changes"""
|
|
36
64
|
os.system(f"git diff --cached {file_path}")
|
|
37
|
-
confirm =
|
|
65
|
+
confirm = get_single_line_input(f"Accept {file_path} changes? (y/n) [y]").lower() or "y"
|
|
38
66
|
if confirm == "y":
|
|
39
67
|
return True
|
|
40
68
|
else:
|
|
@@ -43,6 +71,38 @@ class PatchHandler:
|
|
|
43
71
|
os.system(f"git checkout -- {file_path}")
|
|
44
72
|
PrettyOutput.print(f"Changes to {file_path} have been rolled back", OutputType.WARNING)
|
|
45
73
|
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _finalize_changes(self) -> None:
|
|
78
|
+
"""Complete changes and commit"""
|
|
79
|
+
PrettyOutput.print("Modification confirmed, committing...", OutputType.INFO)
|
|
80
|
+
|
|
81
|
+
# Add only modified files under git control
|
|
82
|
+
os.system("git add -u")
|
|
83
|
+
|
|
84
|
+
# Then get git diff
|
|
85
|
+
git_diff = os.popen("git diff --cached").read()
|
|
86
|
+
|
|
87
|
+
# Automatically generate commit information, pass in feature
|
|
88
|
+
commit_message = generate_commit_message(git_diff)
|
|
89
|
+
|
|
90
|
+
# Display and confirm commit information
|
|
91
|
+
PrettyOutput.print(f"Automatically generated commit information: {commit_message}", OutputType.INFO)
|
|
92
|
+
user_confirm = get_single_line_input("Use this commit information? (y/n) [y]").lower() or "y"
|
|
93
|
+
|
|
94
|
+
if user_confirm.lower() != "y":
|
|
95
|
+
commit_message = get_single_line_input("Please enter a new commit information")
|
|
96
|
+
|
|
97
|
+
# No need to git add again, it has already been added
|
|
98
|
+
os.system(f"git commit -m '{commit_message}'")
|
|
99
|
+
save_edit_record(self.record_dir, commit_message, git_diff)
|
|
100
|
+
|
|
101
|
+
def _revert_changes(self) -> None:
|
|
102
|
+
"""Revert all changes"""
|
|
103
|
+
PrettyOutput.print("Modification cancelled, reverting changes", OutputType.INFO)
|
|
104
|
+
os.system(f"git reset --hard")
|
|
105
|
+
os.system(f"git clean -df")
|
|
46
106
|
|
|
47
107
|
|
|
48
108
|
def apply_file_patch(self, file_path: str, patches: List[Patch]) -> bool:
|
|
@@ -79,18 +139,26 @@ class PatchHandler:
|
|
|
79
139
|
return True
|
|
80
140
|
|
|
81
141
|
|
|
82
|
-
def retry_comfirm(self) -> Tuple[str, str]
|
|
83
|
-
choice =
|
|
142
|
+
def retry_comfirm(self) -> Tuple[str, str]:
|
|
143
|
+
choice = get_single_line_input("\nPlease choose an action: (1) Retry (2) Skip (3) Completely stop [1]: ") or "1"
|
|
84
144
|
if choice == "2":
|
|
85
145
|
return "skip", ""
|
|
86
146
|
if choice == "3":
|
|
87
147
|
return "break", ""
|
|
88
|
-
|
|
148
|
+
|
|
149
|
+
feedback = get_multiline_input("Please enter additional information and requirements:")
|
|
150
|
+
if feedback:
|
|
151
|
+
save_prompt = get_single_line_input("Would you like to save this as general feedback for future patches? (y/n) [n]: ").lower() or "n"
|
|
152
|
+
if save_prompt == "y":
|
|
153
|
+
self._save_additional_info(feedback)
|
|
154
|
+
PrettyOutput.print("Feedback saved for future use", OutputType.SUCCESS)
|
|
155
|
+
|
|
156
|
+
return "continue", feedback
|
|
89
157
|
|
|
90
|
-
def apply_patch(self, feature: str,
|
|
158
|
+
def apply_patch(self, feature: str, structed_plan: Dict[str, str]) -> bool:
|
|
91
159
|
"""Apply patch (main entry)"""
|
|
92
160
|
for file_path, current_plan in structed_plan.items():
|
|
93
|
-
additional_info =
|
|
161
|
+
additional_info = self.additional_info # Initialize with saved info
|
|
94
162
|
while True:
|
|
95
163
|
|
|
96
164
|
if os.path.exists(file_path):
|
|
@@ -114,16 +182,17 @@ class PatchHandler:
|
|
|
114
182
|
2. old_code will be replaced with new_code, pay attention to context continuity
|
|
115
183
|
3. Avoid breaking existing code logic when generating patches, e.g., don't insert function definitions inside existing function bodies
|
|
116
184
|
4. Include sufficient context to avoid ambiguity
|
|
117
|
-
5. Patches will be merged using file_content.replace(patch.old_code, patch.new_code, 1), so old_code and new_code need to match exactly, including
|
|
185
|
+
5. Patches will be merged using file_content.replace(patch.old_code, patch.new_code, 1), so old_code and new_code need to match exactly, including EMPTY LINES, LINE BREAKS, WHITESPACE, TABS, and COMMENTS
|
|
118
186
|
6. Ensure generated code has correct format (syntax, indentation, line breaks)
|
|
119
187
|
7. Ensure new_code's indentation and format matches old_code
|
|
120
188
|
8. Ensure code is inserted in appropriate locations, e.g., code using variables should be after declarations/definitions
|
|
121
189
|
9. Provide at least 3 lines of context before and after modified code for location
|
|
190
|
+
10. Each patch should be no more than 20 lines of code, if it is more than 20 lines, split it into multiple patches
|
|
191
|
+
11. old code's line breaks should be consistent with the original code
|
|
122
192
|
|
|
123
193
|
|
|
124
194
|
"""
|
|
125
195
|
prompt += f"""# Original requirement: {feature}
|
|
126
|
-
# Complete modification plan: {raw_plan}
|
|
127
196
|
# Current file path: {file_path}
|
|
128
197
|
# Current file content:
|
|
129
198
|
<CONTENT>
|
|
@@ -146,7 +215,7 @@ class PatchHandler:
|
|
|
146
215
|
act, msg = self.retry_comfirm()
|
|
147
216
|
if act == "break":
|
|
148
217
|
PrettyOutput.print("Terminate patch application", OutputType.WARNING)
|
|
149
|
-
return False
|
|
218
|
+
return False
|
|
150
219
|
if act == "skip":
|
|
151
220
|
PrettyOutput.print(f"Skip file {file_path}", OutputType.WARNING)
|
|
152
221
|
break
|
|
@@ -154,13 +223,14 @@ class PatchHandler:
|
|
|
154
223
|
additional_info += msg + "\n"
|
|
155
224
|
continue
|
|
156
225
|
else:
|
|
226
|
+
self._finalize_changes()
|
|
157
227
|
break
|
|
158
228
|
|
|
159
|
-
return True
|
|
229
|
+
return True
|
|
160
230
|
|
|
161
231
|
|
|
162
232
|
|
|
163
|
-
def handle_patch_application(self, feature: str,
|
|
233
|
+
def handle_patch_application(self, feature: str, structed_plan: Dict[str,str]) -> bool:
|
|
164
234
|
"""Process patch application process
|
|
165
235
|
|
|
166
236
|
Args:
|
|
@@ -176,13 +246,13 @@ class PatchHandler:
|
|
|
176
246
|
PrettyOutput.print(f"\nFile: {file_path}", OutputType.INFO)
|
|
177
247
|
PrettyOutput.print(f"Modification plan: \n{patches_code}", OutputType.INFO)
|
|
178
248
|
# 3. Apply patches
|
|
179
|
-
success
|
|
249
|
+
success = self.apply_patch(feature, structed_plan)
|
|
180
250
|
if not success:
|
|
181
251
|
os.system("git reset --hard")
|
|
182
252
|
return False
|
|
183
253
|
# 6. Apply successfully, let user confirm changes
|
|
184
254
|
PrettyOutput.print("\nPatches applied, please check the modification effect.", OutputType.SUCCESS)
|
|
185
|
-
confirm =
|
|
255
|
+
confirm = get_single_line_input("\nKeep these changes? (y/n) [y]: ").lower() or "y"
|
|
186
256
|
if confirm != "y":
|
|
187
257
|
PrettyOutput.print("User cancelled changes, rolling back", OutputType.WARNING)
|
|
188
258
|
os.system("git reset --hard") # Rollback all changes
|