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.

Files changed (45) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/agent.py +199 -157
  3. jarvis/jarvis_code_agent/__init__.py +0 -0
  4. jarvis/jarvis_code_agent/main.py +202 -0
  5. jarvis/jarvis_codebase/main.py +415 -287
  6. jarvis/jarvis_coder/file_select.py +209 -0
  7. jarvis/jarvis_coder/git_utils.py +64 -2
  8. jarvis/jarvis_coder/main.py +13 -397
  9. jarvis/jarvis_coder/patch_handler.py +229 -81
  10. jarvis/jarvis_coder/plan_generator.py +49 -7
  11. jarvis/jarvis_platform/main.py +2 -2
  12. jarvis/jarvis_rag/main.py +11 -11
  13. jarvis/jarvis_smart_shell/main.py +5 -5
  14. jarvis/models/base.py +6 -1
  15. jarvis/models/kimi.py +2 -2
  16. jarvis/models/ollama.py +2 -2
  17. jarvis/models/openai.py +1 -1
  18. jarvis/models/registry.py +38 -18
  19. jarvis/tools/ask_user.py +12 -9
  20. jarvis/tools/chdir.py +9 -5
  21. jarvis/tools/create_code_sub_agent.py +56 -0
  22. jarvis/tools/{sub_agent.py → create_sub_agent.py} +6 -2
  23. jarvis/tools/execute_code_modification.py +70 -0
  24. jarvis/tools/{shell.py → execute_shell.py} +2 -2
  25. jarvis/tools/{file_ops.py → file_operation.py} +19 -15
  26. jarvis/tools/find_files.py +119 -0
  27. jarvis/tools/{generator.py → generate_tool.py} +27 -25
  28. jarvis/tools/methodology.py +32 -26
  29. jarvis/tools/rag.py +37 -33
  30. jarvis/tools/{webpage.py → read_webpage.py} +4 -2
  31. jarvis/tools/registry.py +94 -48
  32. jarvis/tools/search.py +19 -16
  33. jarvis/tools/select_code_files.py +61 -0
  34. jarvis/tools/thinker.py +7 -5
  35. jarvis/utils.py +155 -32
  36. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/METADATA +9 -8
  37. jarvis_ai_assistant-0.1.100.dist-info/RECORD +51 -0
  38. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/entry_points.txt +2 -1
  39. jarvis/main.py +0 -155
  40. jarvis/tools/codebase_qa.py +0 -74
  41. jarvis/tools/coder.py +0 -69
  42. jarvis_ai_assistant-0.1.98.dist-info/RECORD +0 -47
  43. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/LICENSE +0 -0
  44. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/WHEEL +0 -0
  45. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.100.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,8 @@ import threading
3
3
  from typing import Dict, Any, List, Optional
4
4
  import re
5
5
 
6
- from jarvis.utils import OutputType, PrettyOutput, find_git_root, get_max_context_length, is_long_context, load_env_from_file, while_success
6
+ from jarvis.jarvis_coder.file_select import select_files
7
+ from jarvis.utils import OutputType, PrettyOutput, find_git_root, get_max_context_length, is_long_context, init_env, while_success
7
8
  from jarvis.models.registry import PlatformRegistry
8
9
  from jarvis.jarvis_codebase.main import CodeBase
9
10
  from prompt_toolkit import PromptSession
@@ -12,7 +13,7 @@ from prompt_toolkit.formatted_text import FormattedText
12
13
  from prompt_toolkit.styles import Style
13
14
  import fnmatch
14
15
  from .patch_handler import PatchHandler
15
- from .git_utils import generate_commit_message, save_edit_record
16
+ from .git_utils import generate_commit_message, init_git_repo, save_edit_record
16
17
  from .plan_generator import PlanGenerator
17
18
 
18
19
  # 全局锁对象
@@ -29,98 +30,14 @@ class JarvisCoder:
29
30
  def _init_directories(self):
30
31
  """Initialize directories"""
31
32
  self.max_context_length = get_max_context_length()
32
-
33
- root_dir = find_git_root(self.root_dir)
34
- if not root_dir:
35
- root_dir = self.root_dir
36
-
37
- self.root_dir = root_dir
38
-
39
- PrettyOutput.print(f"Git root directory: {self.root_dir}", OutputType.INFO)
40
-
41
- # 1. Check if the code repository path exists, if it does not exist, create it
42
- if not os.path.exists(self.root_dir):
43
- PrettyOutput.print(
44
- "Root directory does not exist, creating...", OutputType.INFO)
45
- os.makedirs(self.root_dir)
46
-
47
- os.chdir(self.root_dir)
48
-
49
- # 2. Create .jarvis-coder directory
50
- self.jarvis_dir = os.path.join(self.root_dir, ".jarvis-coder")
51
- if not os.path.exists(self.jarvis_dir):
52
- os.makedirs(self.jarvis_dir)
53
-
54
- self.record_dir = os.path.join(self.jarvis_dir, "record")
55
- if not os.path.exists(self.record_dir):
56
- os.makedirs(self.record_dir)
57
-
58
- # 3. Process .gitignore file
59
- gitignore_path = os.path.join(self.root_dir, ".gitignore")
60
- gitignore_modified = False
61
- jarvis_ignore_pattern = ".jarvis-*"
62
-
63
- # 3.1 If .gitignore does not exist, create it
64
- if not os.path.exists(gitignore_path):
65
- PrettyOutput.print("Create .gitignore file", OutputType.INFO)
66
- with open(gitignore_path, "w", encoding="utf-8") as f:
67
- f.write(f"{jarvis_ignore_pattern}\n")
68
- gitignore_modified = True
69
- else:
70
- # 3.2 Check if it already contains the .jarvis-* pattern
71
- with open(gitignore_path, "r", encoding="utf-8") as f:
72
- content = f.read()
73
-
74
- # 3.2 Check if it already contains the .jarvis-* pattern
75
- if jarvis_ignore_pattern not in content.split("\n"):
76
- PrettyOutput.print("Add .jarvis-* to .gitignore", OutputType.INFO)
77
- with open(gitignore_path, "a", encoding="utf-8") as f:
78
- # Ensure the file ends with a newline
79
- if not content.endswith("\n"):
80
- f.write("\n")
81
- f.write(f"{jarvis_ignore_pattern}\n")
82
- gitignore_modified = True
83
-
84
- # 4. Check if the code repository is a git repository, if not, initialize the git repository
85
- if not os.path.exists(os.path.join(self.root_dir, ".git")):
86
- PrettyOutput.print("Initialize Git repository", OutputType.INFO)
87
- os.system("git init")
88
- os.system("git add .")
89
- os.system("git commit -m 'Initial commit'")
90
- # 5. If .gitignore is modified, commit the changes
91
- elif gitignore_modified:
92
- PrettyOutput.print("Commit .gitignore changes", OutputType.INFO)
93
- os.system("git add .gitignore")
94
- os.system("git commit -m 'chore: update .gitignore to exclude .jarvis-* files'")
95
- # 6. Check if there are uncommitted files in the code repository, if there are, commit once
96
- elif self._has_uncommitted_files():
97
- PrettyOutput.print("Commit uncommitted changes", OutputType.INFO)
98
- os.system("git add .")
99
- git_diff = os.popen("git diff --cached").read()
100
- commit_message = generate_commit_message(git_diff)
101
- os.system(f"git commit -m '{commit_message}'")
33
+ self.root_dir = init_git_repo(self.root_dir)
102
34
 
103
35
  def _init_codebase(self):
104
36
  """Initialize codebase"""
105
37
  self._codebase = CodeBase(self.root_dir)
106
38
 
107
- def _has_uncommitted_files(self) -> bool:
108
- """Check if there are uncommitted files in the code repository"""
109
- # Get unstaged modifications
110
- unstaged = os.popen("git diff --name-only").read()
111
- # Get staged but uncommitted modifications
112
- staged = os.popen("git diff --cached --name-only").read()
113
- # Get untracked files
114
- untracked = os.popen("git ls-files --others --exclude-standard").read()
115
-
116
- return bool(unstaged or staged or untracked)
117
-
118
- def _prepare_execution(self) -> None:
119
- """Prepare execution environment"""
120
- self._codebase.generate_codebase()
121
-
122
39
 
123
- def _load_related_files(self, feature: str) -> List[Dict]:
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, 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()
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._select_files(initial_files)
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
- raw_plan, structed_plan = PlanGenerator().generate_plan(feature, selected_files)
425
- if not raw_plan or not structed_plan:
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 ,raw_plan, structed_plan):
434
- self._finalize_changes(feature)
73
+ if PatchHandler().handle_patch_application(feature, structed_plan):
435
74
  return {
436
75
  "success": True,
437
76
  "stdout": "Code modification successful",
438
77
  "stderr": "",
439
78
  }
440
79
  else:
441
- self._revert_changes()
442
80
  return {
443
81
  "success": False,
444
82
  "stdout": "",
@@ -446,19 +84,17 @@ Code content:
446
84
  }
447
85
 
448
86
  except Exception as e:
449
- self._revert_changes()
450
87
  return {
451
88
  "success": False,
452
89
  "stdout": "",
453
90
  "stderr": f"Execution failed: {str(e)}, please modify the requirement and try again",
454
- "error": e
455
91
  }
456
92
 
457
93
  def main():
458
94
  """Command line entry"""
459
95
  import argparse
460
96
 
461
- load_env_from_file()
97
+ init_env()
462
98
 
463
99
  parser = argparse.ArgumentParser(description='Code modification tool')
464
100
  parser.add_argument('-d', '--dir', help='Project root directory', default=os.getcwd())
@@ -485,11 +121,6 @@ def main():
485
121
  else:
486
122
  if result.get("stderr"):
487
123
  PrettyOutput.print(result["stderr"], OutputType.WARNING)
488
- if result.get("error"): # Use get() method to avoid KeyError
489
- error = result["error"]
490
- PrettyOutput.print(f"Error type: {type(error).__name__}", OutputType.WARNING)
491
- PrettyOutput.print(f"Error information: {str(error)}", OutputType.WARNING)
492
- # Prompt user to continue input
493
124
  PrettyOutput.print("\nYou can modify the requirements and try again", OutputType.INFO)
494
125
 
495
126
  except KeyboardInterrupt:
@@ -557,21 +188,6 @@ class FilePathCompleter(Completer):
557
188
  # Calculate the correct start_position
558
189
  yield Completion(path, start_position=-(len(search)))
559
190
 
560
- class SmartCompleter(Completer):
561
- """Smart auto-completer, combine word and file path completion"""
562
-
563
- def __init__(self, word_completer: WordCompleter, file_completer: FilePathCompleter):
564
- self.word_completer = word_completer
565
- self.file_completer = file_completer
566
-
567
- def get_completions(self, document, complete_event):
568
- """Get completion suggestions"""
569
- # If the current line ends with @, use file completion
570
- if document.text_before_cursor.strip().endswith('@'):
571
- yield from self.file_completer.get_completions(document, complete_event)
572
- else:
573
- # Otherwise, use word completion
574
- yield from self.word_completer.get_completions(document, complete_event)
575
191
 
576
192
  def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
577
193
  """Get multi-line input, support file path auto-completion function