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.

Files changed (40) 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 +203 -0
  5. jarvis/jarvis_codebase/main.py +412 -284
  6. jarvis/jarvis_coder/file_select.py +209 -0
  7. jarvis/jarvis_coder/git_utils.py +64 -2
  8. jarvis/jarvis_coder/main.py +11 -389
  9. jarvis/jarvis_coder/patch_handler.py +84 -14
  10. jarvis/jarvis_coder/plan_generator.py +49 -7
  11. jarvis/jarvis_rag/main.py +9 -9
  12. jarvis/jarvis_smart_shell/main.py +5 -7
  13. jarvis/models/base.py +6 -1
  14. jarvis/models/ollama.py +2 -2
  15. jarvis/models/registry.py +3 -6
  16. jarvis/tools/ask_user.py +6 -6
  17. jarvis/tools/codebase_qa.py +5 -7
  18. jarvis/tools/create_code_sub_agent.py +55 -0
  19. jarvis/tools/{sub_agent.py → create_sub_agent.py} +4 -1
  20. jarvis/tools/execute_code_modification.py +72 -0
  21. jarvis/tools/{file_ops.py → file_operation.py} +13 -14
  22. jarvis/tools/find_related_files.py +86 -0
  23. jarvis/tools/methodology.py +25 -25
  24. jarvis/tools/rag.py +32 -32
  25. jarvis/tools/registry.py +72 -36
  26. jarvis/tools/search.py +1 -1
  27. jarvis/tools/select_code_files.py +64 -0
  28. jarvis/utils.py +153 -49
  29. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/METADATA +1 -1
  30. jarvis_ai_assistant-0.1.99.dist-info/RECORD +52 -0
  31. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/entry_points.txt +2 -1
  32. jarvis/main.py +0 -155
  33. jarvis/tools/coder.py +0 -69
  34. jarvis_ai_assistant-0.1.98.dist-info/RECORD +0 -47
  35. /jarvis/tools/{shell.py → execute_shell.py} +0 -0
  36. /jarvis/tools/{generator.py → generate_tool.py} +0 -0
  37. /jarvis/tools/{webpage.py → read_webpage.py} +0 -0
  38. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/LICENSE +0 -0
  39. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/WHEEL +0 -0
  40. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,209 @@
1
+
2
+ import os
3
+ import re
4
+ from typing import Dict, List
5
+ from prompt_toolkit import PromptSession
6
+ from prompt_toolkit.completion import WordCompleter, Completer, Completion
7
+ from jarvis.utils import OutputType, PrettyOutput, get_single_line_input
8
+
9
+
10
+ def _parse_file_selection(input_str: str, max_index: int) -> List[int]:
11
+ """Parse file selection expression
12
+
13
+ Supported formats:
14
+ - Single number: "1"
15
+ - Comma-separated: "1,3,5"
16
+ - Range: "1-5"
17
+ - Combination: "1,3-5,7"
18
+
19
+ Args:
20
+ input_str: User input selection expression
21
+ max_index: Maximum selectable index
22
+
23
+ Returns:
24
+ List[int]: Selected index list (starting from 0)
25
+ """
26
+ selected = set()
27
+
28
+ # Remove all whitespace characters
29
+ input_str = "".join(input_str.split())
30
+
31
+ # Process comma-separated parts
32
+ for part in input_str.split(","):
33
+ if not part:
34
+ continue
35
+
36
+ # Process range (e.g.: 3-6)
37
+ if "-" in part:
38
+ try:
39
+ start, end = map(int, part.split("-"))
40
+ # Convert to index starting from 0
41
+ start = max(0, start - 1)
42
+ end = min(max_index, end - 1)
43
+ if start <= end:
44
+ selected.update(range(start, end + 1))
45
+ except ValueError:
46
+ PrettyOutput.print(f"Ignore invalid range expression: {part}", OutputType.WARNING)
47
+ # Process single number
48
+ else:
49
+ try:
50
+ index = int(part) - 1 # Convert to index starting from 0
51
+ if 0 <= index < max_index:
52
+ selected.add(index)
53
+ else:
54
+ PrettyOutput.print(f"Ignore index out of range: {part}", OutputType.WARNING)
55
+ except ValueError:
56
+ PrettyOutput.print(f"Ignore invalid number: {part}", OutputType.WARNING)
57
+
58
+ return sorted(list(selected))
59
+
60
+ def _get_file_completer(root_dir: str) -> Completer:
61
+ """Create file path completer"""
62
+ class FileCompleter(Completer):
63
+ def __init__(self, root_dir: str):
64
+ self.root_dir = root_dir
65
+
66
+ def get_completions(self, document, complete_event):
67
+ # Get the text of the current input
68
+ text = document.text_before_cursor
69
+
70
+ # If the input is empty, return all files in the root directory
71
+ if not text:
72
+ for path in self._list_files(""):
73
+ yield Completion(path, start_position=0)
74
+ return
75
+
76
+ # Get the current directory and partial file name
77
+ current_dir = os.path.dirname(text)
78
+ file_prefix = os.path.basename(text)
79
+
80
+ # List matching files
81
+ search_dir = os.path.join(self.root_dir, current_dir) if current_dir else self.root_dir
82
+ if os.path.isdir(search_dir):
83
+ for path in self._list_files(current_dir):
84
+ if path.startswith(text):
85
+ yield Completion(path, start_position=-len(text))
86
+
87
+ def _list_files(self, current_dir: str) -> List[str]:
88
+ """List all files in the specified directory (recursively)"""
89
+ files = []
90
+ search_dir = os.path.join(self.root_dir, current_dir)
91
+
92
+ for root, _, filenames in os.walk(search_dir):
93
+ for filename in filenames:
94
+ full_path = os.path.join(root, filename)
95
+ rel_path = os.path.relpath(full_path, self.root_dir)
96
+ # Ignore .git directory and other hidden files
97
+ if not any(part.startswith('.') for part in rel_path.split(os.sep)):
98
+ files.append(rel_path)
99
+
100
+ return sorted(files)
101
+
102
+ return FileCompleter(root_dir)
103
+
104
+ def _fuzzy_match_files(root_dir: str, pattern: str) -> List[str]:
105
+ """Fuzzy match file path
106
+
107
+ Args:
108
+ pattern: Matching pattern
109
+
110
+ Returns:
111
+ List[str]: List of matching file paths
112
+ """
113
+ matches = []
114
+
115
+ # 将模式转换为正则表达式
116
+ pattern = pattern.replace('.', r'\.').replace('*', '.*').replace('?', '.')
117
+ pattern = f".*{pattern}.*" # 允许部分匹配
118
+ regex = re.compile(pattern, re.IGNORECASE)
119
+
120
+ # 遍历所有文件
121
+ for root, _, files in os.walk(root_dir):
122
+ for file in files:
123
+ full_path = os.path.join(root, file)
124
+ rel_path = os.path.relpath(full_path, root_dir)
125
+ # 忽略 .git 目录和其他隐藏文件
126
+ if not any(part.startswith('.') for part in rel_path.split(os.sep)):
127
+ if regex.match(rel_path):
128
+ matches.append(rel_path)
129
+
130
+ return sorted(matches)
131
+
132
+ def select_files(related_files: List[str], root_dir: str) -> List[str]:
133
+ """Let the user select and supplement related files"""
134
+ PrettyOutput.section("Related files", OutputType.INFO)
135
+
136
+ # Display found files
137
+ selected_files = list(related_files) # Default select all
138
+ for i, file in enumerate(related_files, 1):
139
+ PrettyOutput.print(f"[{i}] {file}", OutputType.INFO)
140
+
141
+ # Ask the user if they need to adjust the file list
142
+ user_input = get_single_line_input("Do you need to adjust the file list? (y/n) [n]").strip().lower() or 'n'
143
+ if user_input == 'y':
144
+ # Let the user select files
145
+ numbers = get_single_line_input("Please enter the file numbers to include (support: 1,3-6 format, press Enter to keep the current selection)").strip()
146
+ if numbers:
147
+ selected_indices = _parse_file_selection(numbers, len(related_files))
148
+ if selected_indices:
149
+ selected_files = [related_files[i] for i in selected_indices]
150
+ else:
151
+ PrettyOutput.print("No valid files selected, keep the current selection", OutputType.WARNING)
152
+
153
+ # Ask if they need to supplement files
154
+ user_input = get_single_line_input("Do you need to supplement other files? (y/n) [n]").strip().lower() or 'n'
155
+ if user_input == 'y':
156
+ # Create file completion session
157
+ session = PromptSession(
158
+ completer=_get_file_completer(root_dir),
159
+ complete_while_typing=True
160
+ )
161
+
162
+ while True:
163
+ PrettyOutput.print("\nPlease enter the file path to supplement (support Tab completion and *? wildcard, input empty line to end):", OutputType.INFO)
164
+ try:
165
+ file_path = session.prompt(">>> ").strip()
166
+ except KeyboardInterrupt:
167
+ break
168
+
169
+ if not file_path:
170
+ break
171
+
172
+ # Process wildcard matching
173
+ if '*' in file_path or '?' in file_path:
174
+ matches = _fuzzy_match_files(root_dir, file_path)
175
+ if not matches:
176
+ PrettyOutput.print("No matching files found", OutputType.WARNING)
177
+ continue
178
+
179
+ # Display matching files
180
+ PrettyOutput.print("\nFound the following matching files:", OutputType.INFO)
181
+ for i, path in enumerate(matches, 1):
182
+ PrettyOutput.print(f"[{i}] {path}", OutputType.INFO)
183
+
184
+ # Let the user select
185
+ numbers = get_single_line_input("Please select the file numbers to add (support: 1,3-6 format, press Enter to select all)").strip()
186
+ if numbers:
187
+ indices = _parse_file_selection(numbers, len(matches))
188
+ if not indices:
189
+ continue
190
+ paths_to_add = [matches[i] for i in indices]
191
+ else:
192
+ paths_to_add = matches
193
+ else:
194
+ paths_to_add = [file_path]
195
+
196
+ # Add selected files
197
+ for path in paths_to_add:
198
+ full_path = os.path.join(root_dir, path)
199
+ if not os.path.isfile(full_path):
200
+ PrettyOutput.print(f"File does not exist: {path}", OutputType.ERROR)
201
+ continue
202
+
203
+ try:
204
+ selected_files.append(path)
205
+ PrettyOutput.print(f"File added: {path}", OutputType.SUCCESS)
206
+ except Exception as e:
207
+ PrettyOutput.print(f"Failed to read file: {str(e)}", OutputType.ERROR)
208
+
209
+ return selected_files
@@ -2,7 +2,7 @@ import os
2
2
  from typing import List
3
3
  import yaml
4
4
  import time
5
- from jarvis.utils import OutputType, PrettyOutput, while_success
5
+ from jarvis.utils import OutputType, PrettyOutput, find_git_root, while_success
6
6
  from jarvis.models.registry import PlatformRegistry
7
7
 
8
8
  def has_uncommitted_files() -> bool:
@@ -58,4 +58,66 @@ def save_edit_record(record_dir: str, commit_message: str, git_diff: str) -> Non
58
58
  with open(record_path, "w", encoding="utf-8") as f:
59
59
  yaml.safe_dump(record, f, allow_unicode=True)
60
60
 
61
- PrettyOutput.print(f"Modification record saved: {record_path}", OutputType.SUCCESS)
61
+ PrettyOutput.print(f"Modification record saved: {record_path}", OutputType.SUCCESS)
62
+
63
+
64
+ def init_git_repo(root_dir: str) -> str:
65
+ git_dir = find_git_root(root_dir)
66
+ if not git_dir:
67
+ git_dir = root_dir
68
+
69
+ PrettyOutput.print(f"Git root directory: {git_dir}", OutputType.INFO)
70
+
71
+ # 1. Check if the code repository path exists, if it does not exist, create it
72
+ if not os.path.exists(git_dir):
73
+ PrettyOutput.print(
74
+ "Root directory does not exist, creating...", OutputType.INFO)
75
+ os.makedirs(git_dir)
76
+
77
+ os.chdir(git_dir)
78
+
79
+ # 3. Process .gitignore file
80
+ gitignore_path = os.path.join(git_dir, ".gitignore")
81
+ gitignore_modified = False
82
+ jarvis_ignore_pattern = ".jarvis-*"
83
+
84
+ # 3.1 If .gitignore does not exist, create it
85
+ if not os.path.exists(gitignore_path):
86
+ PrettyOutput.print("Create .gitignore file", OutputType.INFO)
87
+ with open(gitignore_path, "w", encoding="utf-8") as f:
88
+ f.write(f"{jarvis_ignore_pattern}\n")
89
+ gitignore_modified = True
90
+ else:
91
+ # 3.2 Check if it already contains the .jarvis-* pattern
92
+ with open(gitignore_path, "r", encoding="utf-8") as f:
93
+ content = f.read()
94
+
95
+ # 3.2 Check if it already contains the .jarvis-* pattern
96
+ if jarvis_ignore_pattern not in content.split("\n"):
97
+ PrettyOutput.print("Add .jarvis-* to .gitignore", OutputType.INFO)
98
+ with open(gitignore_path, "a", encoding="utf-8") as f:
99
+ # Ensure the file ends with a newline
100
+ if not content.endswith("\n"):
101
+ f.write("\n")
102
+ f.write(f"{jarvis_ignore_pattern}\n")
103
+ gitignore_modified = True
104
+
105
+ # 4. Check if the code repository is a git repository, if not, initialize the git repository
106
+ if not os.path.exists(os.path.join(git_dir, ".git")):
107
+ PrettyOutput.print("Initialize Git repository", OutputType.INFO)
108
+ os.system("git init")
109
+ os.system("git add .")
110
+ os.system("git commit -m 'Initial commit'")
111
+ # 5. If .gitignore is modified, commit the changes
112
+ elif gitignore_modified:
113
+ PrettyOutput.print("Commit .gitignore changes", OutputType.INFO)
114
+ os.system("git add .gitignore")
115
+ os.system("git commit -m 'chore: update .gitignore to exclude .jarvis-* files'")
116
+ # 6. Check if there are uncommitted files in the code repository, if there are, commit once
117
+ elif has_uncommitted_files():
118
+ PrettyOutput.print("Commit uncommitted changes", OutputType.INFO)
119
+ os.system("git add .")
120
+ git_diff = os.popen("git diff --cached").read()
121
+ commit_message = generate_commit_message(git_diff)
122
+ os.system(f"git commit -m '{commit_message}'")
123
+ return git_dir