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.
- 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 +81 -19
- jarvis/jarvis_coder/main.py +68 -446
- jarvis/jarvis_coder/patch_handler.py +117 -47
- jarvis/jarvis_coder/plan_generator.py +69 -27
- jarvis/jarvis_platform/main.py +38 -38
- jarvis/jarvis_rag/main.py +189 -189
- jarvis/jarvis_smart_shell/main.py +22 -24
- 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.97.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.97.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.97.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.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.97.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/WHEEL +0 -0
- {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
|
jarvis/jarvis_coder/git_utils.py
CHANGED
|
@@ -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
|
-
"""
|
|
21
|
-
prompt = f"""
|
|
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.
|
|
30
|
-
3.
|
|
31
|
-
4.
|
|
32
|
-
5.
|
|
33
|
-
6.
|
|
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"
|
|
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
|