jarvis-ai-assistant 0.1.98__py3-none-any.whl → 0.1.100__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of jarvis-ai-assistant might be problematic. Click here for more details.
- jarvis/__init__.py +1 -1
- jarvis/agent.py +199 -157
- jarvis/jarvis_code_agent/__init__.py +0 -0
- jarvis/jarvis_code_agent/main.py +202 -0
- jarvis/jarvis_codebase/main.py +415 -287
- jarvis/jarvis_coder/file_select.py +209 -0
- jarvis/jarvis_coder/git_utils.py +64 -2
- jarvis/jarvis_coder/main.py +13 -397
- jarvis/jarvis_coder/patch_handler.py +229 -81
- jarvis/jarvis_coder/plan_generator.py +49 -7
- jarvis/jarvis_platform/main.py +2 -2
- jarvis/jarvis_rag/main.py +11 -11
- jarvis/jarvis_smart_shell/main.py +5 -5
- jarvis/models/base.py +6 -1
- jarvis/models/kimi.py +2 -2
- jarvis/models/ollama.py +2 -2
- jarvis/models/openai.py +1 -1
- jarvis/models/registry.py +38 -18
- jarvis/tools/ask_user.py +12 -9
- jarvis/tools/chdir.py +9 -5
- jarvis/tools/create_code_sub_agent.py +56 -0
- jarvis/tools/{sub_agent.py → create_sub_agent.py} +6 -2
- jarvis/tools/execute_code_modification.py +70 -0
- jarvis/tools/{shell.py → execute_shell.py} +2 -2
- jarvis/tools/{file_ops.py → file_operation.py} +19 -15
- jarvis/tools/find_files.py +119 -0
- jarvis/tools/{generator.py → generate_tool.py} +27 -25
- jarvis/tools/methodology.py +32 -26
- jarvis/tools/rag.py +37 -33
- jarvis/tools/{webpage.py → read_webpage.py} +4 -2
- jarvis/tools/registry.py +94 -48
- jarvis/tools/search.py +19 -16
- jarvis/tools/select_code_files.py +61 -0
- jarvis/tools/thinker.py +7 -5
- jarvis/utils.py +155 -32
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/METADATA +9 -8
- jarvis_ai_assistant-0.1.100.dist-info/RECORD +51 -0
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/entry_points.txt +2 -1
- jarvis/main.py +0 -155
- jarvis/tools/codebase_qa.py +0 -74
- jarvis/tools/coder.py +0 -69
- jarvis_ai_assistant-0.1.98.dist-info/RECORD +0 -47
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/top_level.txt +0 -0
|
@@ -1,40 +1,70 @@
|
|
|
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
|
-
def __init__(self,
|
|
11
|
-
self.
|
|
12
|
-
self.
|
|
13
|
+
def __init__(self, start: int, end: int, new_code: str):
|
|
14
|
+
self.start = start # Line number where patch starts (inclusive)
|
|
15
|
+
self.end = end # Line number where patch ends (exclusive)
|
|
16
|
+
self.new_code = new_code # New code to insert/replace
|
|
13
17
|
|
|
14
18
|
class PatchHandler:
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.prompt_file = Path.home() / ".jarvis-coder-patch-prompt"
|
|
21
|
+
self.additional_info = self._load_additional_info()
|
|
22
|
+
self.root_dir = init_git_repo(os.getcwd())
|
|
23
|
+
self.record_dir = os.path.join(self.root_dir, ".jarvis-coder", "record")
|
|
24
|
+
if not os.path.exists(self.record_dir):
|
|
25
|
+
os.makedirs(self.record_dir)
|
|
26
|
+
def _load_additional_info(self) -> str:
|
|
27
|
+
"""Load saved additional info from prompt file"""
|
|
28
|
+
if not self.prompt_file.exists():
|
|
29
|
+
return ""
|
|
30
|
+
try:
|
|
31
|
+
with open(self.prompt_file, 'r') as f:
|
|
32
|
+
data = yaml.safe_load(f)
|
|
33
|
+
return data.get('additional_info', '') if data else ''
|
|
34
|
+
except Exception as e:
|
|
35
|
+
PrettyOutput.print(f"Failed to load additional info: {e}", OutputType.WARNING)
|
|
36
|
+
return ""
|
|
15
37
|
|
|
38
|
+
def _save_additional_info(self, info: str):
|
|
39
|
+
"""Save additional info to prompt file"""
|
|
40
|
+
try:
|
|
41
|
+
with open(self.prompt_file, 'w') as f:
|
|
42
|
+
yaml.dump({'additional_info': info}, f)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
PrettyOutput.print(f"Failed to save additional info: {e}", OutputType.WARNING)
|
|
16
45
|
|
|
17
46
|
def _extract_patches(self, response: str) -> List[Patch]:
|
|
18
|
-
"""Extract patches from response
|
|
47
|
+
"""Extract patches from response with hexadecimal line numbers
|
|
19
48
|
|
|
20
49
|
Args:
|
|
21
50
|
response: Model response content
|
|
22
51
|
|
|
23
52
|
Returns:
|
|
24
|
-
List[
|
|
53
|
+
List[Patch]: List of patches, each containing the line range and new code
|
|
25
54
|
"""
|
|
26
|
-
|
|
27
|
-
fmt_pattern = r'<PATCH>\n>>>>>> SEARCH\n(.*?)\n?(={5,})\n(.*?)\n?<<<<<< REPLACE\n</PATCH>'
|
|
55
|
+
fmt_pattern = r'<PATCH>\n\[([0-9a-f]+),([0-9a-f]+)\)\n(.*?\n)</PATCH>'
|
|
28
56
|
ret = []
|
|
29
57
|
for m in re.finditer(fmt_pattern, response, re.DOTALL):
|
|
30
|
-
|
|
58
|
+
start = int(m.group(1), 16) # Convert hex to decimal
|
|
59
|
+
end = int(m.group(2), 16)
|
|
60
|
+
new_code = m.group(3)
|
|
61
|
+
ret.append(Patch(start, end, new_code))
|
|
31
62
|
return ret
|
|
32
63
|
|
|
33
|
-
|
|
34
64
|
def _confirm_and_apply_changes(self, file_path: str) -> bool:
|
|
35
65
|
"""Confirm and apply changes"""
|
|
36
66
|
os.system(f"git diff --cached {file_path}")
|
|
37
|
-
confirm =
|
|
67
|
+
confirm = get_single_line_input(f"Accept {file_path} changes? (y/n) [y]").lower() or "y"
|
|
38
68
|
if confirm == "y":
|
|
39
69
|
return True
|
|
40
70
|
else:
|
|
@@ -43,87 +73,208 @@ class PatchHandler:
|
|
|
43
73
|
os.system(f"git checkout -- {file_path}")
|
|
44
74
|
PrettyOutput.print(f"Changes to {file_path} have been rolled back", OutputType.WARNING)
|
|
45
75
|
return False
|
|
46
|
-
|
|
76
|
+
|
|
47
77
|
|
|
78
|
+
|
|
79
|
+
def _finalize_changes(self) -> None:
|
|
80
|
+
"""Complete changes and commit"""
|
|
81
|
+
PrettyOutput.print("Modification confirmed, committing...", OutputType.INFO)
|
|
82
|
+
|
|
83
|
+
# Add only modified files under git control
|
|
84
|
+
os.system("git add -u")
|
|
85
|
+
|
|
86
|
+
# Then get git diff
|
|
87
|
+
git_diff = os.popen("git diff --cached").read()
|
|
88
|
+
|
|
89
|
+
# Automatically generate commit information, pass in feature
|
|
90
|
+
commit_message = generate_commit_message(git_diff)
|
|
91
|
+
|
|
92
|
+
# Display and confirm commit information
|
|
93
|
+
PrettyOutput.print(f"Automatically generated commit information: {commit_message}", OutputType.INFO)
|
|
94
|
+
user_confirm = get_single_line_input("Use this commit information? (y/n) [y]").lower() or "y"
|
|
95
|
+
|
|
96
|
+
if user_confirm.lower() != "y":
|
|
97
|
+
commit_message = get_single_line_input("Please enter a new commit information")
|
|
98
|
+
|
|
99
|
+
# No need to git add again, it has already been added
|
|
100
|
+
os.system(f"git commit -m '{commit_message}'")
|
|
101
|
+
save_edit_record(self.record_dir, commit_message, git_diff)
|
|
102
|
+
|
|
103
|
+
def _revert_changes(self) -> None:
|
|
104
|
+
"""Revert all changes"""
|
|
105
|
+
PrettyOutput.print("Modification cancelled, reverting changes", OutputType.INFO)
|
|
106
|
+
os.system(f"git reset --hard")
|
|
107
|
+
os.system(f"git clean -df")
|
|
108
|
+
|
|
109
|
+
def _check_patches_overlap(self, patches: List[Patch]) -> bool:
|
|
110
|
+
"""Check if any patches overlap with each other
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
patches: List of patches to check
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
bool: True if patches overlap, False otherwise
|
|
117
|
+
"""
|
|
118
|
+
if not patches:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
# Sort patches by start line
|
|
122
|
+
sorted_patches = sorted(patches, key=lambda x: x.start)
|
|
123
|
+
|
|
124
|
+
# Check for overlaps
|
|
125
|
+
for i in range(len(sorted_patches) - 1):
|
|
126
|
+
current = sorted_patches[i]
|
|
127
|
+
next_patch = sorted_patches[i + 1]
|
|
128
|
+
|
|
129
|
+
if current.end > next_patch.start:
|
|
130
|
+
PrettyOutput.print(
|
|
131
|
+
f"Overlapping patches detected: [{current.start:04x},{current.end:04x}) and [{next_patch.start:04x},{next_patch.end:04x})",
|
|
132
|
+
OutputType.WARNING
|
|
133
|
+
)
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
return False
|
|
137
|
+
|
|
48
138
|
def apply_file_patch(self, file_path: str, patches: List[Patch]) -> bool:
|
|
49
|
-
"""Apply file
|
|
139
|
+
"""Apply file patches using line numbers"""
|
|
50
140
|
if not os.path.exists(file_path):
|
|
51
141
|
base_dir = os.path.dirname(file_path)
|
|
52
142
|
os.makedirs(base_dir, exist_ok=True)
|
|
53
143
|
open(file_path, "w", encoding="utf-8").close()
|
|
54
|
-
|
|
144
|
+
|
|
145
|
+
# Check for overlapping patches
|
|
146
|
+
if self._check_patches_overlap(patches):
|
|
147
|
+
PrettyOutput.print("Cannot apply overlapping patches", OutputType.ERROR)
|
|
148
|
+
os.system(f"git reset {file_path}")
|
|
149
|
+
os.system(f"git checkout -- {file_path}")
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
153
|
+
lines = f.readlines()
|
|
154
|
+
|
|
155
|
+
# Sort patches by start line in reverse order to apply from bottom to top
|
|
156
|
+
patches.sort(key=lambda x: x.start, reverse=True)
|
|
157
|
+
|
|
55
158
|
for i, patch in enumerate(patches):
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
159
|
+
PrettyOutput.print(f"Applying patch {i+1}/{len(patches)} at lines [{patch.start},{patch.end})", OutputType.INFO)
|
|
160
|
+
|
|
161
|
+
if patch.start > len(lines):
|
|
162
|
+
PrettyOutput.print(f"Invalid patch: start line {patch.start} exceeds file length {len(lines)}", OutputType.WARNING)
|
|
163
|
+
os.system(f"git reset {file_path}")
|
|
164
|
+
os.system(f"git checkout -- {file_path}")
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
if patch.new_code:
|
|
168
|
+
new_lines = patch.new_code.splitlines(keepends=True)
|
|
169
|
+
lines[patch.start:patch.end] = new_lines
|
|
67
170
|
else:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
open(file_path, "w", encoding="utf-8").write(file_content)
|
|
77
|
-
os.system(f"git add {file_path}")
|
|
78
|
-
PrettyOutput.print(f"Apply patch {i+1}/{len(patches)} successfully", OutputType.SUCCESS)
|
|
171
|
+
del lines[patch.start:patch.end]
|
|
172
|
+
|
|
173
|
+
# Write modified content back to file
|
|
174
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
175
|
+
f.writelines(lines)
|
|
176
|
+
|
|
177
|
+
os.system(f"git add {file_path}")
|
|
178
|
+
PrettyOutput.print(f"Successfully applied all patches to {file_path}", OutputType.SUCCESS)
|
|
79
179
|
return True
|
|
80
180
|
|
|
81
181
|
|
|
82
|
-
def retry_comfirm(self) -> Tuple[str, str]
|
|
83
|
-
choice =
|
|
182
|
+
def retry_comfirm(self) -> Tuple[str, str]:
|
|
183
|
+
choice = get_single_line_input("\nPlease choose an action: (1) Retry (2) Skip (3) Completely stop [1]: ") or "1"
|
|
84
184
|
if choice == "2":
|
|
85
185
|
return "skip", ""
|
|
86
186
|
if choice == "3":
|
|
87
187
|
return "break", ""
|
|
88
|
-
|
|
188
|
+
|
|
189
|
+
feedback = get_multiline_input("Please enter additional information and requirements:")
|
|
190
|
+
if feedback:
|
|
191
|
+
save_prompt = get_single_line_input("Would you like to save this as general feedback for future patches? (y/n) [n]: ").lower() or "n"
|
|
192
|
+
if save_prompt == "y":
|
|
193
|
+
self._save_additional_info(feedback)
|
|
194
|
+
PrettyOutput.print("Feedback saved for future use", OutputType.SUCCESS)
|
|
195
|
+
|
|
196
|
+
return "continue", feedback
|
|
89
197
|
|
|
90
|
-
def apply_patch(self, feature: str,
|
|
198
|
+
def apply_patch(self, feature: str, structed_plan: Dict[str, str]) -> Tuple[bool, str]:
|
|
91
199
|
"""Apply patch (main entry)"""
|
|
200
|
+
feedback = ""
|
|
92
201
|
for file_path, current_plan in structed_plan.items():
|
|
93
|
-
additional_info =
|
|
202
|
+
additional_info = self.additional_info # Initialize with saved info
|
|
94
203
|
while True:
|
|
95
|
-
|
|
96
204
|
if os.path.exists(file_path):
|
|
97
|
-
|
|
205
|
+
# Read file and add line numbers
|
|
206
|
+
lines = []
|
|
207
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
208
|
+
for i, line in enumerate(f):
|
|
209
|
+
lines.append(f"{i:04x}{line}") # Changed from i+1 to i for 0-based indexing
|
|
210
|
+
content = "".join(lines)
|
|
98
211
|
else:
|
|
99
212
|
content = "<File does not exist, need to create>"
|
|
213
|
+
|
|
100
214
|
prompt = """You are a senior software development expert who can generate code patches based on the complete modification plan, current original code file path, code content, and current file's modification plan. The output format should be as follows:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
215
|
+
|
|
216
|
+
<PATCH>
|
|
217
|
+
[start,end)
|
|
218
|
+
new_code
|
|
219
|
+
</PATCH>
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
<PATCH>
|
|
223
|
+
[0004,0004)
|
|
224
|
+
def new_function():
|
|
225
|
+
pass
|
|
226
|
+
</PATCH>
|
|
227
|
+
|
|
228
|
+
means:
|
|
229
|
+
Insert code BEFORE line 4:
|
|
230
|
+
```
|
|
231
|
+
def new_function():
|
|
232
|
+
pass
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Example 2:
|
|
236
|
+
<PATCH>
|
|
237
|
+
[0004,000b)
|
|
238
|
+
aa
|
|
239
|
+
bb
|
|
240
|
+
cc
|
|
241
|
+
</PATCH>
|
|
242
|
+
|
|
243
|
+
means:
|
|
244
|
+
Replace lines [4,11) with:
|
|
245
|
+
```
|
|
246
|
+
aa
|
|
247
|
+
bb
|
|
248
|
+
cc
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Rules:
|
|
252
|
+
1. start and end are hexadecimal line numbers (e.g., 000a)
|
|
253
|
+
2. The patch will replace lines [start,end) with new_code (including start, excluding end)
|
|
254
|
+
3. If start equals end, new_code will be inserted BEFORE that line
|
|
255
|
+
4. If new_code is empty, lines [start,end) will be deleted
|
|
256
|
+
5. Multiple patches can be generated
|
|
257
|
+
6. Each line in the input file starts with its 4-digit hexadecimal line number (0-based)
|
|
258
|
+
7. Your new_code should NOT include line numbers
|
|
259
|
+
8. CRITICAL: Patches MUST NOT overlap - ensure each line is modified by at most one patch
|
|
260
|
+
9. Generate patches from bottom to top of the file
|
|
261
|
+
10. Ensure new_code maintains correct indentation and formatting
|
|
262
|
+
11. Each patch should modify no more than 20 lines
|
|
263
|
+
12. Include sufficient context in new_code to maintain code consistency
|
|
264
|
+
13. `[` and `)` must be included in the line range
|
|
265
|
+
14. Line numbers start from 0
|
|
266
|
+
15. Example of INVALID overlapping patches:
|
|
267
|
+
<PATCH>
|
|
268
|
+
[0001,0005)
|
|
269
|
+
code1
|
|
270
|
+
</PATCH>
|
|
271
|
+
<PATCH>
|
|
272
|
+
[0003,0007) # This overlaps with the previous patch
|
|
273
|
+
code2
|
|
274
|
+
</PATCH>
|
|
275
|
+
"""
|
|
276
|
+
|
|
125
277
|
prompt += f"""# Original requirement: {feature}
|
|
126
|
-
# Complete modification plan: {raw_plan}
|
|
127
278
|
# Current file path: {file_path}
|
|
128
279
|
# Current file content:
|
|
129
280
|
<CONTENT>
|
|
@@ -146,21 +297,25 @@ class PatchHandler:
|
|
|
146
297
|
act, msg = self.retry_comfirm()
|
|
147
298
|
if act == "break":
|
|
148
299
|
PrettyOutput.print("Terminate patch application", OutputType.WARNING)
|
|
149
|
-
|
|
300
|
+
additional_info = get_multiline_input("Please enter your additional information or suggestions (press Enter to cancel):")
|
|
301
|
+
return False, additional_info
|
|
150
302
|
if act == "skip":
|
|
151
303
|
PrettyOutput.print(f"Skip file {file_path}", OutputType.WARNING)
|
|
304
|
+
feedback += f"Skip file {file_path}\n"
|
|
305
|
+
feedback += "Reason: " + get_multiline_input("Please enter your reason:") + "\n"
|
|
152
306
|
break
|
|
153
307
|
else:
|
|
154
308
|
additional_info += msg + "\n"
|
|
155
309
|
continue
|
|
156
310
|
else:
|
|
311
|
+
self._finalize_changes()
|
|
157
312
|
break
|
|
158
313
|
|
|
159
|
-
return True,
|
|
314
|
+
return True, feedback
|
|
160
315
|
|
|
161
316
|
|
|
162
317
|
|
|
163
|
-
def handle_patch_application(self, feature: str,
|
|
318
|
+
def handle_patch_application(self, feature: str, structed_plan: Dict[str,str]) -> Tuple[bool, str]:
|
|
164
319
|
"""Process patch application process
|
|
165
320
|
|
|
166
321
|
Args:
|
|
@@ -176,17 +331,10 @@ class PatchHandler:
|
|
|
176
331
|
PrettyOutput.print(f"\nFile: {file_path}", OutputType.INFO)
|
|
177
332
|
PrettyOutput.print(f"Modification plan: \n{patches_code}", OutputType.INFO)
|
|
178
333
|
# 3. Apply patches
|
|
179
|
-
success,
|
|
334
|
+
success, additional_info = self.apply_patch(feature, structed_plan)
|
|
180
335
|
if not success:
|
|
181
336
|
os.system("git reset --hard")
|
|
182
|
-
return False
|
|
337
|
+
return False, additional_info
|
|
183
338
|
# 6. Apply successfully, let user confirm changes
|
|
184
339
|
PrettyOutput.print("\nPatches applied, please check the modification effect.", OutputType.SUCCESS)
|
|
185
|
-
|
|
186
|
-
if confirm != "y":
|
|
187
|
-
PrettyOutput.print("User cancelled changes, rolling back", OutputType.WARNING)
|
|
188
|
-
os.system("git reset --hard") # Rollback all changes
|
|
189
|
-
return False
|
|
190
|
-
else:
|
|
191
|
-
return True
|
|
192
|
-
|
|
340
|
+
return True, "Modification applied successfully"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from typing import Dict, List, Tuple
|
|
3
3
|
from jarvis.models.registry import PlatformRegistry
|
|
4
|
-
from jarvis.utils import PrettyOutput, OutputType, get_multiline_input
|
|
4
|
+
from jarvis.utils import PrettyOutput, OutputType, get_multiline_input, get_single_line_input, is_long_context
|
|
5
5
|
|
|
6
6
|
class PlanGenerator:
|
|
7
7
|
"""Modification plan generator"""
|
|
@@ -48,7 +48,42 @@ class PlanGenerator:
|
|
|
48
48
|
return prompt
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
@staticmethod
|
|
52
|
+
def get_key_code(files: List[str], feature: str)->List[Dict[str, List[str]]]:
|
|
53
|
+
"""Extract relevant key code snippets from files"""
|
|
54
|
+
ret = []
|
|
55
|
+
for file in files:
|
|
56
|
+
PrettyOutput.print(f"Analyzing file: {file}", OutputType.INFO)
|
|
57
|
+
model = PlatformRegistry.get_global_platform_registry().get_codegen_platform()
|
|
58
|
+
model.set_suppress_output(True)
|
|
59
|
+
file_path = file
|
|
60
|
+
content = open(file_path, "r", encoding="utf-8").read()
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
prompt = f"""You are a code analysis expert who can extract relevant snippets from code.
|
|
64
|
+
Please return in the following format:
|
|
65
|
+
<PART>
|
|
66
|
+
content
|
|
67
|
+
</PART>
|
|
68
|
+
|
|
69
|
+
Multiple snippets can be returned. If the file content is not relevant to the requirement, return empty.
|
|
70
|
+
|
|
71
|
+
Requirement: {feature}
|
|
72
|
+
File path: {file_path}
|
|
73
|
+
Code content:
|
|
74
|
+
{content}
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# 调用大模型进行分析
|
|
78
|
+
response = model.chat_until_success(prompt)
|
|
79
|
+
|
|
80
|
+
parts = re.findall(r'<PART>\n(.*?)\n</PART>', response, re.DOTALL)
|
|
81
|
+
ret.append({"file_path": file, "parts": parts})
|
|
82
|
+
except Exception as e:
|
|
83
|
+
PrettyOutput.print(f"Failed to analyze file: {str(e)}", OutputType.ERROR)
|
|
84
|
+
return ret
|
|
85
|
+
|
|
86
|
+
def generate_plan(self, feature: str, related_files: List[str]) -> Dict[str,str]:
|
|
52
87
|
"""Generate modification plan
|
|
53
88
|
|
|
54
89
|
Args:
|
|
@@ -59,8 +94,15 @@ class PlanGenerator:
|
|
|
59
94
|
Tuple[str, Dict[str,str]]: Modification plan, return None if user cancels
|
|
60
95
|
"""
|
|
61
96
|
additional_info = ""
|
|
97
|
+
file_info = []
|
|
98
|
+
if is_long_context(related_files):
|
|
99
|
+
file_info = PlanGenerator.get_key_code(related_files, feature)
|
|
100
|
+
else:
|
|
101
|
+
for file in related_files:
|
|
102
|
+
file_info.append({"file_path": file, "parts": [open(file, "r", encoding="utf-8").read()]})
|
|
103
|
+
|
|
62
104
|
while True:
|
|
63
|
-
prompt = self._build_prompt(feature,
|
|
105
|
+
prompt = self._build_prompt(feature, file_info, additional_info)
|
|
64
106
|
# Build prompt
|
|
65
107
|
PrettyOutput.print("Start generating modification plan...", OutputType.PROGRESS)
|
|
66
108
|
|
|
@@ -71,17 +113,17 @@ class PlanGenerator:
|
|
|
71
113
|
PrettyOutput.print("Modification plan generation failed, please try again", OutputType.ERROR)
|
|
72
114
|
tmp = get_multiline_input("Please enter your additional information or suggestions (press Enter to cancel):")
|
|
73
115
|
if tmp == "__interrupt__" or prompt == "":
|
|
74
|
-
return
|
|
116
|
+
return {}
|
|
75
117
|
additional_info += tmp + "\n"
|
|
76
118
|
continue
|
|
77
|
-
user_input =
|
|
119
|
+
user_input = get_single_line_input("Do you agree with this modification plan? (y/n) [y]").strip().lower() or 'y'
|
|
78
120
|
if user_input == 'y' or user_input == '':
|
|
79
|
-
return
|
|
121
|
+
return structed_plan
|
|
80
122
|
elif user_input == 'n':
|
|
81
123
|
# Get user feedback
|
|
82
124
|
tmp = get_multiline_input("Please enter your additional information or suggestions (press Enter to cancel):")
|
|
83
125
|
if prompt == "__interrupt__" or prompt == "":
|
|
84
|
-
return
|
|
126
|
+
return {}
|
|
85
127
|
additional_info += tmp + "\n"
|
|
86
128
|
continue
|
|
87
129
|
|
jarvis/jarvis_platform/main.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from jarvis.models.registry import PlatformRegistry
|
|
2
|
-
from jarvis.utils import PrettyOutput, OutputType,
|
|
2
|
+
from jarvis.utils import PrettyOutput, OutputType, init_env, get_multiline_input
|
|
3
3
|
|
|
4
4
|
def list_platforms():
|
|
5
5
|
"""List all supported platforms and models"""
|
|
@@ -106,7 +106,7 @@ def main():
|
|
|
106
106
|
"""Main function"""
|
|
107
107
|
import argparse
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
init_env()
|
|
110
110
|
|
|
111
111
|
parser = argparse.ArgumentParser(description='Jarvis AI Platform')
|
|
112
112
|
subparsers = parser.add_subparsers(dest='command', help='Available subcommands')
|
jarvis/jarvis_rag/main.py
CHANGED
|
@@ -4,7 +4,7 @@ import faiss
|
|
|
4
4
|
from typing import List, Tuple, Optional, Dict
|
|
5
5
|
import pickle
|
|
6
6
|
from jarvis.utils import OutputType, PrettyOutput, get_file_md5, get_max_context_length, load_embedding_model, load_rerank_model
|
|
7
|
-
from jarvis.utils import
|
|
7
|
+
from jarvis.utils import init_env
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from tqdm import tqdm
|
|
10
10
|
import fitz # PyMuPDF for PDF files
|
|
@@ -90,7 +90,7 @@ class TextFileProcessor(FileProcessor):
|
|
|
90
90
|
continue
|
|
91
91
|
|
|
92
92
|
if not detected_encoding:
|
|
93
|
-
raise UnicodeDecodeError(f"Failed to decode file with supported encodings: {file_path}")
|
|
93
|
+
raise UnicodeDecodeError(f"Failed to decode file with supported encodings: {file_path}") # type: ignore
|
|
94
94
|
|
|
95
95
|
# Use the detected encoding to read the file
|
|
96
96
|
with open(file_path, 'r', encoding=detected_encoding, errors='replace') as f:
|
|
@@ -114,7 +114,7 @@ class PDFProcessor(FileProcessor):
|
|
|
114
114
|
@staticmethod
|
|
115
115
|
def extract_text(file_path: str) -> str:
|
|
116
116
|
text_parts = []
|
|
117
|
-
with fitz.open(file_path) as doc:
|
|
117
|
+
with fitz.open(file_path) as doc: # type: ignore
|
|
118
118
|
for page in doc:
|
|
119
119
|
text_parts.append(page.get_text())
|
|
120
120
|
return "\n".join(text_parts)
|
|
@@ -137,7 +137,7 @@ class RAGTool:
|
|
|
137
137
|
Args:
|
|
138
138
|
root_dir: Project root directory
|
|
139
139
|
"""
|
|
140
|
-
|
|
140
|
+
init_env()
|
|
141
141
|
self.root_dir = root_dir
|
|
142
142
|
os.chdir(self.root_dir)
|
|
143
143
|
|
|
@@ -248,7 +248,7 @@ class RAGTool:
|
|
|
248
248
|
|
|
249
249
|
# Create a flat index to store original vectors, for reconstruction
|
|
250
250
|
self.flat_index = faiss.IndexFlatIP(self.vector_dim)
|
|
251
|
-
self.flat_index.add(vectors)
|
|
251
|
+
self.flat_index.add(vectors) # type: ignore
|
|
252
252
|
|
|
253
253
|
# Create an IVF index for fast search
|
|
254
254
|
nlist = max(4, int(vectors.shape[0] / 1000)) # 每1000个向量一个聚类中心
|
|
@@ -256,8 +256,8 @@ class RAGTool:
|
|
|
256
256
|
self.index = faiss.IndexIVFFlat(quantizer, self.vector_dim, nlist, faiss.METRIC_INNER_PRODUCT)
|
|
257
257
|
|
|
258
258
|
# Train and add vectors
|
|
259
|
-
self.index.train(vectors)
|
|
260
|
-
self.index.add(vectors)
|
|
259
|
+
self.index.train(vectors) # type: ignore
|
|
260
|
+
self.index.add(vectors) # type: ignore
|
|
261
261
|
# Set the number of clusters to probe during search
|
|
262
262
|
self.index.nprobe = min(nlist, 10)
|
|
263
263
|
|
|
@@ -341,7 +341,7 @@ class RAGTool:
|
|
|
341
341
|
except Exception as e:
|
|
342
342
|
PrettyOutput.print(f"Failed to get vector representation: {str(e)}",
|
|
343
343
|
output_type=OutputType.ERROR)
|
|
344
|
-
return np.zeros((len(texts), self.vector_dim), dtype=np.float32)
|
|
344
|
+
return np.zeros((len(texts), self.vector_dim), dtype=np.float32) # type: ignore
|
|
345
345
|
|
|
346
346
|
def _process_document_batch(self, documents: List[Document]) -> List[np.ndarray]:
|
|
347
347
|
"""Process a batch of documents vectorization
|
|
@@ -361,7 +361,7 @@ Content: {doc.content}
|
|
|
361
361
|
"""
|
|
362
362
|
texts.append(combined_text)
|
|
363
363
|
|
|
364
|
-
return self._get_embedding_batch(texts)
|
|
364
|
+
return self._get_embedding_batch(texts) # type: ignore
|
|
365
365
|
|
|
366
366
|
def _process_file(self, file_path: str) -> List[Document]:
|
|
367
367
|
"""Process a single file"""
|
|
@@ -516,7 +516,7 @@ Content: {doc.content}
|
|
|
516
516
|
if d.metadata['file_path'] == doc.metadata['file_path']), None)
|
|
517
517
|
if doc_idx is not None:
|
|
518
518
|
# Reconstruct vectors from flat index
|
|
519
|
-
vector = np.zeros((1, self.vector_dim), dtype=np.float32)
|
|
519
|
+
vector = np.zeros((1, self.vector_dim), dtype=np.float32) # type: ignore
|
|
520
520
|
self.flat_index.reconstruct(doc_idx, vector.ravel())
|
|
521
521
|
unchanged_vectors.append(vector)
|
|
522
522
|
|
|
@@ -585,7 +585,7 @@ Content: {doc.content}
|
|
|
585
585
|
|
|
586
586
|
# Initial search more results for MMR
|
|
587
587
|
initial_k = min(top_k * 2, len(self.documents))
|
|
588
|
-
distances, indices = self.index.search(query_vector, initial_k)
|
|
588
|
+
distances, indices = self.index.search(query_vector, initial_k) # type: ignore
|
|
589
589
|
|
|
590
590
|
# Get valid results
|
|
591
591
|
valid_indices = indices[0][indices[0] != -1]
|
|
@@ -4,11 +4,11 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
import readline
|
|
6
6
|
from typing import Optional
|
|
7
|
-
from yaspin import yaspin
|
|
8
|
-
from yaspin.spinners import Spinners
|
|
7
|
+
from yaspin import yaspin # type: ignore
|
|
8
|
+
from yaspin.spinners import Spinners # type: ignore
|
|
9
9
|
|
|
10
10
|
from jarvis.models.registry import PlatformRegistry
|
|
11
|
-
from jarvis.utils import PrettyOutput, OutputType,
|
|
11
|
+
from jarvis.utils import PrettyOutput, OutputType, init_env
|
|
12
12
|
|
|
13
13
|
def execute_command(command: str) -> None:
|
|
14
14
|
"""Show command and allow user to edit, then execute, Ctrl+C to cancel"""
|
|
@@ -27,6 +27,7 @@ def execute_command(command: str) -> None:
|
|
|
27
27
|
except Exception as e:
|
|
28
28
|
PrettyOutput.print(f"Failed to execute command: {str(e)}", OutputType.ERROR)
|
|
29
29
|
|
|
30
|
+
|
|
30
31
|
def process_request(request: str) -> Optional[str]:
|
|
31
32
|
"""Process user request and return corresponding shell command
|
|
32
33
|
|
|
@@ -38,7 +39,6 @@ def process_request(request: str) -> Optional[str]:
|
|
|
38
39
|
"""
|
|
39
40
|
try:
|
|
40
41
|
# Get language model instance
|
|
41
|
-
PlatformRegistry.suppress_output = True
|
|
42
42
|
model = PlatformRegistry.get_global_platform_registry().get_normal_platform()
|
|
43
43
|
model.set_suppress_output(True)
|
|
44
44
|
|
|
@@ -90,7 +90,7 @@ Remember: Only return the command itself, without any additional content.
|
|
|
90
90
|
|
|
91
91
|
def main():
|
|
92
92
|
# 创建参数解析器
|
|
93
|
-
|
|
93
|
+
init_env()
|
|
94
94
|
parser = argparse.ArgumentParser(
|
|
95
95
|
description="Convert natural language requirements to shell commands",
|
|
96
96
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
jarvis/models/base.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
+
import re
|
|
2
3
|
from typing import Dict, List, Tuple
|
|
3
4
|
|
|
4
5
|
from jarvis.utils import OutputType, PrettyOutput, while_success, while_true
|
|
@@ -26,7 +27,11 @@ class BasePlatform(ABC):
|
|
|
26
27
|
raise NotImplementedError("chat is not implemented")
|
|
27
28
|
|
|
28
29
|
def chat_until_success(self, message: str) -> str:
|
|
29
|
-
|
|
30
|
+
def _chat():
|
|
31
|
+
response = self.chat(message)
|
|
32
|
+
response = re.sub(r'<think>(.*?)</think>', '', response, flags=re.DOTALL)
|
|
33
|
+
return response
|
|
34
|
+
return while_true(lambda: while_success(lambda: _chat(), 5), 5)
|
|
30
35
|
|
|
31
36
|
@abstractmethod
|
|
32
37
|
def upload_files(self, file_list: List[str]) -> List[Dict]:
|