jarvis-ai-assistant 0.1.97__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 (41) 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 +81 -19
  8. jarvis/jarvis_coder/main.py +68 -446
  9. jarvis/jarvis_coder/patch_handler.py +117 -47
  10. jarvis/jarvis_coder/plan_generator.py +69 -27
  11. jarvis/jarvis_platform/main.py +38 -38
  12. jarvis/jarvis_rag/main.py +189 -189
  13. jarvis/jarvis_smart_shell/main.py +22 -24
  14. jarvis/models/base.py +6 -1
  15. jarvis/models/ollama.py +2 -2
  16. jarvis/models/registry.py +3 -6
  17. jarvis/tools/ask_user.py +6 -6
  18. jarvis/tools/codebase_qa.py +5 -7
  19. jarvis/tools/create_code_sub_agent.py +55 -0
  20. jarvis/tools/{sub_agent.py → create_sub_agent.py} +4 -1
  21. jarvis/tools/execute_code_modification.py +72 -0
  22. jarvis/tools/{file_ops.py → file_operation.py} +13 -14
  23. jarvis/tools/find_related_files.py +86 -0
  24. jarvis/tools/methodology.py +25 -25
  25. jarvis/tools/rag.py +32 -32
  26. jarvis/tools/registry.py +72 -36
  27. jarvis/tools/search.py +1 -1
  28. jarvis/tools/select_code_files.py +64 -0
  29. jarvis/utils.py +153 -49
  30. {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/METADATA +1 -1
  31. jarvis_ai_assistant-0.1.99.dist-info/RECORD +52 -0
  32. {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/entry_points.txt +2 -1
  33. jarvis/main.py +0 -155
  34. jarvis/tools/coder.py +0 -69
  35. jarvis_ai_assistant-0.1.97.dist-info/RECORD +0 -47
  36. /jarvis/tools/{shell.py → execute_shell.py} +0 -0
  37. /jarvis/tools/{generator.py → generate_tool.py} +0 -0
  38. /jarvis/tools/{webpage.py → read_webpage.py} +0 -0
  39. {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/LICENSE +0 -0
  40. {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/WHEEL +0 -0
  41. {jarvis_ai_assistant-0.1.97.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,35 +2,35 @@ 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:
9
- """判断代码库是否有未提交的文件"""
10
- # 获取未暂存的修改
9
+ """Check if there are uncommitted files in the repository"""
10
+ # Get unstaged modifications
11
11
  unstaged = os.popen("git diff --name-only").read()
12
- # 获取已暂存但未提交的修改
12
+ # Get staged but uncommitted modifications
13
13
  staged = os.popen("git diff --cached --name-only").read()
14
- # 获取未跟踪的文件
14
+ # Get untracked files
15
15
  untracked = os.popen("git ls-files --others --exclude-standard").read()
16
16
 
17
17
  return bool(unstaged or staged or untracked)
18
18
 
19
19
  def generate_commit_message(git_diff: str) -> str:
20
- """根据git diff和功能描述生成commit信息"""
21
- prompt = f"""你是一个经验丰富的程序员,请根据以下代码变更和功能描述生成简洁明了的commit信息:
20
+ """Generate commit message based on git diff and feature description"""
21
+ prompt = f"""You are an experienced programmer, please generate a concise and clear commit message based on the following code changes and feature description:
22
22
 
23
- 代码变更:
23
+ Code changes:
24
24
  Git Diff:
25
25
  {git_diff}
26
26
 
27
- 请遵循以下规则:
28
- 1. 使用英文编写
29
- 2. 采用常规的commit message格式:<type>(<scope>): <subject>
30
- 3. 保持简洁,不超过50个字符
31
- 4. 准确描述代码变更的主要内容
32
- 5. 优先考虑功能描述和git diff中的变更内容
33
- 6. 仅生成commit信息的文本,不要输出任何其他内容
27
+ Please follow these rules:
28
+ 1. Write in English
29
+ 2. Use conventional commit message format: <type>(<scope>): <subject>
30
+ 3. Keep it concise, no more than 50 characters
31
+ 4. Accurately describe the main content of code changes
32
+ 5. Prioritize feature description and changes in git diff
33
+ 6. Only generate the commit message text, do not output anything else
34
34
  """
35
35
 
36
36
  model = PlatformRegistry().get_global_platform_registry().get_normal_platform()
@@ -39,15 +39,15 @@ Git Diff:
39
39
  return ';'.join(response.strip().split("\n"))
40
40
 
41
41
  def save_edit_record(record_dir: str, commit_message: str, git_diff: str) -> None:
42
- """保存代码修改记录"""
43
- # 获取下一个序号
42
+ """Save code modification record"""
43
+ # Get next sequence number
44
44
  existing_records = [f for f in os.listdir(record_dir) if f.endswith('.yaml')]
45
45
  next_num = 1
46
46
  if existing_records:
47
47
  last_num = max(int(f[:4]) for f in existing_records)
48
48
  next_num = last_num + 1
49
49
 
50
- # 创建记录文件
50
+ # Create record file
51
51
  record = {
52
52
  "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
53
53
  "commit_message": commit_message,
@@ -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"已保存修改记录: {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