jarvis-ai-assistant 0.1.124__py3-none-any.whl → 0.1.125__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/jarvis_agent/__init__.py +18 -20
- jarvis/jarvis_code_agent/code_agent.py +195 -45
- jarvis/jarvis_code_agent/file_select.py +6 -19
- jarvis/jarvis_code_agent/patch.py +189 -310
- jarvis/jarvis_codebase/main.py +6 -2
- jarvis/jarvis_dev/main.py +6 -4
- jarvis/jarvis_git_squash/__init__.py +0 -0
- jarvis/jarvis_git_squash/main.py +81 -0
- jarvis/jarvis_lsp/cpp.py +1 -1
- jarvis/jarvis_lsp/go.py +1 -1
- jarvis/jarvis_lsp/registry.py +2 -2
- jarvis/jarvis_lsp/rust.py +1 -1
- jarvis/jarvis_multi_agent/__init__.py +1 -1
- jarvis/jarvis_platform/ai8.py +2 -1
- jarvis/jarvis_platform/base.py +19 -24
- jarvis/jarvis_platform/kimi.py +2 -3
- jarvis/jarvis_platform/ollama.py +3 -1
- jarvis/jarvis_platform/openai.py +1 -1
- jarvis/jarvis_platform/oyi.py +2 -1
- jarvis/jarvis_platform/registry.py +2 -1
- jarvis/jarvis_platform_manager/main.py +4 -6
- jarvis/jarvis_platform_manager/openai_test.py +0 -1
- jarvis/jarvis_rag/main.py +5 -2
- jarvis/jarvis_smart_shell/main.py +9 -4
- jarvis/jarvis_tools/ask_codebase.py +12 -7
- jarvis/jarvis_tools/ask_user.py +3 -2
- jarvis/jarvis_tools/base.py +21 -7
- jarvis/jarvis_tools/chdir.py +0 -1
- jarvis/jarvis_tools/code_review.py +13 -14
- jarvis/jarvis_tools/create_code_agent.py +2 -2
- jarvis/jarvis_tools/create_sub_agent.py +2 -2
- jarvis/jarvis_tools/execute_shell.py +3 -1
- jarvis/jarvis_tools/execute_shell_script.py +4 -4
- jarvis/jarvis_tools/file_operation.py +3 -2
- jarvis/jarvis_tools/git_commiter.py +5 -2
- jarvis/jarvis_tools/lsp_find_definition.py +1 -1
- jarvis/jarvis_tools/lsp_find_references.py +1 -1
- jarvis/jarvis_tools/lsp_get_diagnostics.py +19 -11
- jarvis/jarvis_tools/lsp_get_document_symbols.py +1 -1
- jarvis/jarvis_tools/lsp_prepare_rename.py +1 -1
- jarvis/jarvis_tools/lsp_validate_edit.py +1 -1
- jarvis/jarvis_tools/methodology.py +4 -1
- jarvis/jarvis_tools/rag.py +22 -15
- jarvis/jarvis_tools/read_code.py +4 -3
- jarvis/jarvis_tools/read_webpage.py +2 -1
- jarvis/jarvis_tools/registry.py +4 -1
- jarvis/jarvis_tools/{search.py → search_web.py} +5 -2
- jarvis/jarvis_tools/select_code_files.py +1 -1
- jarvis/jarvis_utils/__init__.py +19 -982
- jarvis/jarvis_utils/config.py +138 -0
- jarvis/jarvis_utils/embedding.py +201 -0
- jarvis/jarvis_utils/git_utils.py +120 -0
- jarvis/jarvis_utils/globals.py +82 -0
- jarvis/jarvis_utils/input.py +161 -0
- jarvis/jarvis_utils/methodology.py +128 -0
- jarvis/jarvis_utils/output.py +235 -0
- jarvis/jarvis_utils/utils.py +150 -0
- jarvis_ai_assistant-0.1.125.dist-info/METADATA +291 -0
- jarvis_ai_assistant-0.1.125.dist-info/RECORD +75 -0
- {jarvis_ai_assistant-0.1.124.dist-info → jarvis_ai_assistant-0.1.125.dist-info}/entry_points.txt +1 -0
- jarvis_ai_assistant-0.1.124.dist-info/METADATA +0 -460
- jarvis_ai_assistant-0.1.124.dist-info/RECORD +0 -65
- {jarvis_ai_assistant-0.1.124.dist-info → jarvis_ai_assistant-0.1.125.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.124.dist-info → jarvis_ai_assistant-0.1.125.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.124.dist-info → jarvis_ai_assistant-0.1.125.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import os
|
|
2
|
+
"""
|
|
3
|
+
Configuration Management Module
|
|
4
|
+
This module provides functions to retrieve various configuration settings for the Jarvis system.
|
|
5
|
+
All configurations are read from environment variables with fallback default values.
|
|
6
|
+
The module is organized into several categories:
|
|
7
|
+
- System Configuration
|
|
8
|
+
- Model Configuration
|
|
9
|
+
- Execution Configuration
|
|
10
|
+
- Text Processing Configuration
|
|
11
|
+
"""
|
|
12
|
+
def get_max_token_count() -> int:
|
|
13
|
+
"""
|
|
14
|
+
Get the maximum token count for API requests.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
int: Maximum token count, default is 131072 (128k)
|
|
18
|
+
"""
|
|
19
|
+
return int(os.getenv('JARVIS_MAX_TOKEN_COUNT', '131072')) # 默认128k
|
|
20
|
+
|
|
21
|
+
def get_thread_count() -> int:
|
|
22
|
+
"""
|
|
23
|
+
Get the number of threads to use for parallel processing.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
int: Thread count, default is 1
|
|
27
|
+
"""
|
|
28
|
+
return int(os.getenv('JARVIS_THREAD_COUNT', '1'))
|
|
29
|
+
def dont_use_local_model() -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Check if local models should be avoided.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
bool: True if local models should not be used, default is False
|
|
35
|
+
"""
|
|
36
|
+
return os.getenv('JARVIS_DONT_USE_LOCAL_MODEL', 'false') == 'true'
|
|
37
|
+
|
|
38
|
+
def is_auto_complete() -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Check if auto-completion is enabled.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
bool: True if auto-completion is enabled, default is False
|
|
44
|
+
"""
|
|
45
|
+
return os.getenv('JARVIS_AUTO_COMPLETE', 'false') == 'true'
|
|
46
|
+
|
|
47
|
+
def is_use_methodology() -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Check if methodology should be used.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
bool: True if methodology should be used, default is True
|
|
53
|
+
"""
|
|
54
|
+
return os.getenv('JARVIS_USE_METHODOLOGY', 'true') == 'true'
|
|
55
|
+
def is_record_methodology() -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Check if methodology should be recorded.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
bool: True if methodology should be recorded, default is True
|
|
61
|
+
"""
|
|
62
|
+
return os.getenv('JARVIS_RECORD_METHODOLOGY', 'true') == 'true'
|
|
63
|
+
def is_need_summary() -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Check if summary generation is required.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
bool: True if summary is needed, default is True
|
|
69
|
+
"""
|
|
70
|
+
return os.getenv('JARVIS_NEED_SUMMARY', 'true') == 'true'
|
|
71
|
+
def get_min_paragraph_length() -> int:
|
|
72
|
+
"""
|
|
73
|
+
Get the minimum paragraph length for text processing.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
int: Minimum length in characters, default is 50
|
|
77
|
+
"""
|
|
78
|
+
return int(os.getenv('JARVIS_MIN_PARAGRAPH_LENGTH', '50'))
|
|
79
|
+
def get_max_paragraph_length() -> int:
|
|
80
|
+
"""
|
|
81
|
+
Get the maximum paragraph length for text processing.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
int: Maximum length in characters, default is 12800
|
|
85
|
+
"""
|
|
86
|
+
return int(os.getenv('JARVIS_MAX_PARAGRAPH_LENGTH', '12800'))
|
|
87
|
+
def get_shell_name() -> str:
|
|
88
|
+
"""
|
|
89
|
+
Get the system shell name.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
str: Shell name (e.g., bash, zsh), default is bash
|
|
93
|
+
"""
|
|
94
|
+
return os.getenv('SHELL', 'bash')
|
|
95
|
+
def get_normal_platform_name() -> str:
|
|
96
|
+
"""
|
|
97
|
+
Get the platform name for normal operations.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: Platform name, default is 'kimi'
|
|
101
|
+
"""
|
|
102
|
+
return os.getenv('JARVIS_PLATFORM', 'kimi')
|
|
103
|
+
def get_normal_model_name() -> str:
|
|
104
|
+
"""
|
|
105
|
+
Get the model name for normal operations.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
str: Model name, default is 'kimi'
|
|
109
|
+
"""
|
|
110
|
+
return os.getenv('JARVIS_MODEL', 'kimi')
|
|
111
|
+
def get_codegen_platform_name() -> str:
|
|
112
|
+
return os.getenv('JARVIS_CODEGEN_PLATFORM', os.getenv('JARVIS_PLATFORM', 'kimi'))
|
|
113
|
+
def get_codegen_model_name() -> str:
|
|
114
|
+
return os.getenv('JARVIS_CODEGEN_MODEL', os.getenv('JARVIS_MODEL', 'kimi'))
|
|
115
|
+
def get_thinking_platform_name() -> str:
|
|
116
|
+
return os.getenv('JARVIS_THINKING_PLATFORM', os.getenv('JARVIS_PLATFORM', 'kimi'))
|
|
117
|
+
def get_thinking_model_name() -> str:
|
|
118
|
+
return os.getenv('JARVIS_THINKING_MODEL', os.getenv('JARVIS_MODEL', 'kimi'))
|
|
119
|
+
def get_cheap_platform_name() -> str:
|
|
120
|
+
return os.getenv('JARVIS_CHEAP_PLATFORM', os.getenv('JARVIS_PLATFORM', 'kimi'))
|
|
121
|
+
def get_cheap_model_name() -> str:
|
|
122
|
+
return os.getenv('JARVIS_CHEAP_MODEL', os.getenv('JARVIS_MODEL', 'kimi'))
|
|
123
|
+
def is_execute_tool_confirm() -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Check if tool execution requires confirmation.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
bool: True if confirmation is required, default is False
|
|
129
|
+
"""
|
|
130
|
+
return os.getenv('JARVIS_EXECUTE_TOOL_CONFIRM', 'false') == 'true'
|
|
131
|
+
def is_confirm_before_apply_patch() -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Check if patch application requires confirmation.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
bool: True if confirmation is required, default is False
|
|
137
|
+
"""
|
|
138
|
+
return os.getenv('JARVIS_CONFIRM_BEFORE_APPLY_PATCH', 'false') == 'true'
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import torch
|
|
4
|
+
from sentence_transformers import SentenceTransformer
|
|
5
|
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
|
6
|
+
from typing import List, Any, Tuple
|
|
7
|
+
from jarvis.jarvis_utils.output import PrettyOutput, OutputType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_context_token_count(text: str) -> int:
|
|
13
|
+
"""Get the token count of the text using the tokenizer.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
text: The input text to count tokens for
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
int: The number of tokens in the text
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
# Use a fast tokenizer that's good at general text
|
|
23
|
+
tokenizer = load_tokenizer()
|
|
24
|
+
chunks = split_text_into_chunks(text, 512)
|
|
25
|
+
return sum([len(tokenizer.encode(chunk)) for chunk in chunks]) # type: ignore
|
|
26
|
+
|
|
27
|
+
except Exception as e:
|
|
28
|
+
PrettyOutput.print(f"计算token失败: {str(e)}", OutputType.WARNING)
|
|
29
|
+
# Fallback to rough character-based estimate
|
|
30
|
+
return len(text) // 4 # Rough estimate of 4 chars per token
|
|
31
|
+
|
|
32
|
+
def load_embedding_model() -> SentenceTransformer:
|
|
33
|
+
"""
|
|
34
|
+
Load the sentence embedding model.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
SentenceTransformer: The loaded embedding model
|
|
38
|
+
"""
|
|
39
|
+
model_name = "BAAI/bge-m3"
|
|
40
|
+
cache_dir = os.path.expanduser("~/.cache/huggingface/hub")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
embedding_model = SentenceTransformer(
|
|
44
|
+
model_name,
|
|
45
|
+
cache_folder=cache_dir,
|
|
46
|
+
local_files_only=True
|
|
47
|
+
)
|
|
48
|
+
except Exception:
|
|
49
|
+
embedding_model = SentenceTransformer(
|
|
50
|
+
model_name,
|
|
51
|
+
cache_folder=cache_dir,
|
|
52
|
+
local_files_only=False
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return embedding_model
|
|
56
|
+
def get_embedding(embedding_model: Any, text: str) -> np.ndarray:
|
|
57
|
+
"""
|
|
58
|
+
Generate embedding vector for the given text.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
embedding_model: The embedding model to use
|
|
62
|
+
text: The input text to embed
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
np.ndarray: The embedding vector
|
|
66
|
+
"""
|
|
67
|
+
embedding = embedding_model.encode(text,
|
|
68
|
+
normalize_embeddings=True,
|
|
69
|
+
show_progress_bar=False)
|
|
70
|
+
return np.array(embedding, dtype=np.float32)
|
|
71
|
+
def get_embedding_batch(embedding_model: Any, texts: List[str]) -> np.ndarray:
|
|
72
|
+
"""
|
|
73
|
+
Generate embeddings for a batch of texts.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
embedding_model: The embedding model to use
|
|
77
|
+
texts: List of texts to embed
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
np.ndarray: Stacked embedding vectors
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
all_vectors = []
|
|
84
|
+
for text in texts:
|
|
85
|
+
vectors = get_embedding_with_chunks(embedding_model, text)
|
|
86
|
+
all_vectors.extend(vectors)
|
|
87
|
+
return np.vstack(all_vectors)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
PrettyOutput.print(f"批量嵌入失败: {str(e)}", OutputType.ERROR)
|
|
90
|
+
return np.zeros((0, embedding_model.get_sentence_embedding_dimension()), dtype=np.float32)
|
|
91
|
+
|
|
92
|
+
def split_text_into_chunks(text: str, max_length: int = 512) -> List[str]:
|
|
93
|
+
"""Split text into chunks with overlapping windows.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
text: The input text to split
|
|
97
|
+
max_length: Maximum length of each chunk
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List[str]: List of text chunks
|
|
101
|
+
"""
|
|
102
|
+
chunks = []
|
|
103
|
+
start = 0
|
|
104
|
+
while start < len(text):
|
|
105
|
+
end = start + max_length
|
|
106
|
+
# Find the nearest sentence boundary
|
|
107
|
+
if end < len(text):
|
|
108
|
+
while end > start and text[end] not in {'.', '!', '?', '\n'}:
|
|
109
|
+
end -= 1
|
|
110
|
+
if end == start: # No punctuation found, hard cut
|
|
111
|
+
end = start + max_length
|
|
112
|
+
chunk = text[start:end]
|
|
113
|
+
chunks.append(chunk)
|
|
114
|
+
# Overlap 20% of the window
|
|
115
|
+
start = end - int(max_length * 0.2)
|
|
116
|
+
return chunks
|
|
117
|
+
|
|
118
|
+
def get_embedding_with_chunks(embedding_model: Any, text: str) -> List[np.ndarray]:
|
|
119
|
+
"""
|
|
120
|
+
Generate embeddings for text chunks.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
embedding_model: The embedding model to use
|
|
124
|
+
text: The input text to process
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List[np.ndarray]: List of embedding vectors for each chunk
|
|
128
|
+
"""
|
|
129
|
+
chunks = split_text_into_chunks(text, 512)
|
|
130
|
+
if not chunks:
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
vectors = []
|
|
134
|
+
for chunk in chunks:
|
|
135
|
+
vector = get_embedding(embedding_model, chunk)
|
|
136
|
+
vectors.append(vector)
|
|
137
|
+
return vectors
|
|
138
|
+
def load_tokenizer() -> AutoTokenizer:
|
|
139
|
+
"""
|
|
140
|
+
Load the tokenizer for text processing.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
AutoTokenizer: The loaded tokenizer
|
|
144
|
+
"""
|
|
145
|
+
model_name = "gpt2"
|
|
146
|
+
cache_dir = os.path.expanduser("~/.cache/huggingface/hub")
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
150
|
+
model_name,
|
|
151
|
+
cache_dir=cache_dir,
|
|
152
|
+
local_files_only=True
|
|
153
|
+
)
|
|
154
|
+
except Exception:
|
|
155
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
156
|
+
model_name,
|
|
157
|
+
cache_dir=cache_dir,
|
|
158
|
+
local_files_only=False
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return tokenizer # type: ignore
|
|
162
|
+
def load_rerank_model() -> Tuple[AutoModelForSequenceClassification, AutoTokenizer]:
|
|
163
|
+
"""
|
|
164
|
+
Load the reranking model and tokenizer.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Tuple[AutoModelForSequenceClassification, AutoTokenizer]: The loaded model and tokenizer
|
|
168
|
+
"""
|
|
169
|
+
model_name = "BAAI/bge-reranker-v2-m3"
|
|
170
|
+
cache_dir = os.path.expanduser("~/.cache/huggingface/hub")
|
|
171
|
+
|
|
172
|
+
PrettyOutput.print(f"加载重排序模型: {model_name}...", OutputType.INFO)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
176
|
+
model_name,
|
|
177
|
+
cache_dir=cache_dir,
|
|
178
|
+
local_files_only=True
|
|
179
|
+
)
|
|
180
|
+
model = AutoModelForSequenceClassification.from_pretrained(
|
|
181
|
+
model_name,
|
|
182
|
+
cache_dir=cache_dir,
|
|
183
|
+
local_files_only=True
|
|
184
|
+
)
|
|
185
|
+
except Exception:
|
|
186
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
187
|
+
model_name,
|
|
188
|
+
cache_dir=cache_dir,
|
|
189
|
+
local_files_only=False
|
|
190
|
+
)
|
|
191
|
+
model = AutoModelForSequenceClassification.from_pretrained(
|
|
192
|
+
model_name,
|
|
193
|
+
cache_dir=cache_dir,
|
|
194
|
+
local_files_only=False
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if torch.cuda.is_available():
|
|
198
|
+
model = model.cuda()
|
|
199
|
+
model.eval()
|
|
200
|
+
|
|
201
|
+
return model, tokenizer # type: ignore
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Utilities Module
|
|
3
|
+
This module provides utilities for interacting with Git repositories.
|
|
4
|
+
It includes functions for:
|
|
5
|
+
- Finding the root directory of a Git repository
|
|
6
|
+
- Checking for uncommitted changes
|
|
7
|
+
- Retrieving commit history between two hashes
|
|
8
|
+
- Getting the latest commit hash
|
|
9
|
+
- Extracting modified line ranges from Git diffs
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
from typing import List, Tuple, Dict
|
|
15
|
+
from jarvis.jarvis_utils.output import PrettyOutput, OutputType
|
|
16
|
+
def find_git_root(start_dir="."):
|
|
17
|
+
"""Change to git root directory of the given path"""
|
|
18
|
+
os.chdir(start_dir)
|
|
19
|
+
git_root = os.popen("git rev-parse --show-toplevel").read().strip()
|
|
20
|
+
os.chdir(git_root)
|
|
21
|
+
return git_root
|
|
22
|
+
def has_uncommitted_changes():
|
|
23
|
+
"""Check if there are uncommitted changes in the git repository"""
|
|
24
|
+
# Add all changes silently
|
|
25
|
+
subprocess.run(["git", "add", "."], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
26
|
+
|
|
27
|
+
# Check working directory changes
|
|
28
|
+
working_changes = subprocess.run(["git", "diff", "--exit-code"],
|
|
29
|
+
stdout=subprocess.DEVNULL,
|
|
30
|
+
stderr=subprocess.DEVNULL).returncode != 0
|
|
31
|
+
|
|
32
|
+
# Check staged changes
|
|
33
|
+
staged_changes = subprocess.run(["git", "diff", "--cached", "--exit-code"],
|
|
34
|
+
stdout=subprocess.DEVNULL,
|
|
35
|
+
stderr=subprocess.DEVNULL).returncode != 0
|
|
36
|
+
|
|
37
|
+
# Reset changes silently
|
|
38
|
+
subprocess.run(["git", "reset"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
39
|
+
|
|
40
|
+
return working_changes or staged_changes
|
|
41
|
+
def get_commits_between(start_hash: str, end_hash: str) -> List[Tuple[str, str]]:
|
|
42
|
+
"""Get list of commits between two commit hashes
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
start_hash: Starting commit hash (exclusive)
|
|
46
|
+
end_hash: Ending commit hash (inclusive)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List[Tuple[str, str]]: List of (commit_hash, commit_message) tuples
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
# Use git log with pretty format to get hash and message
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
['git', 'log', f'{start_hash}..{end_hash}', '--pretty=format:%H|%s'],
|
|
55
|
+
stdout=subprocess.PIPE,
|
|
56
|
+
stderr=subprocess.PIPE,
|
|
57
|
+
text=True
|
|
58
|
+
)
|
|
59
|
+
if result.returncode != 0:
|
|
60
|
+
PrettyOutput.print(f"获取commit历史失败: {result.stderr}", OutputType.ERROR)
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
commits = []
|
|
64
|
+
for line in result.stdout.splitlines():
|
|
65
|
+
if '|' in line:
|
|
66
|
+
commit_hash, message = line.split('|', 1)
|
|
67
|
+
commits.append((commit_hash, message))
|
|
68
|
+
return commits
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
PrettyOutput.print(f"获取commit历史异常: {str(e)}", OutputType.ERROR)
|
|
72
|
+
return []
|
|
73
|
+
def get_latest_commit_hash() -> str:
|
|
74
|
+
"""Get the latest commit hash of the current git repository
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
str: The commit hash, or empty string if not in a git repo or error occurs
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
result = subprocess.run(
|
|
81
|
+
['git', 'rev-parse', 'HEAD'],
|
|
82
|
+
stdout=subprocess.PIPE,
|
|
83
|
+
stderr=subprocess.PIPE,
|
|
84
|
+
text=True
|
|
85
|
+
)
|
|
86
|
+
if result.returncode == 0:
|
|
87
|
+
return result.stdout.strip()
|
|
88
|
+
return ""
|
|
89
|
+
except Exception:
|
|
90
|
+
return ""
|
|
91
|
+
def get_modified_line_ranges() -> Dict[str, Tuple[int, int]]:
|
|
92
|
+
"""Get modified line ranges from git diff for all changed files.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Dictionary mapping file paths to tuple with (start_line, end_line) ranges
|
|
96
|
+
for modified sections. Line numbers are 1-based.
|
|
97
|
+
"""
|
|
98
|
+
# Get git diff for all files
|
|
99
|
+
diff_output = os.popen("git show").read()
|
|
100
|
+
|
|
101
|
+
# Parse the diff to get modified files and their line ranges
|
|
102
|
+
result = {}
|
|
103
|
+
current_file = None
|
|
104
|
+
|
|
105
|
+
for line in diff_output.splitlines():
|
|
106
|
+
# Match lines like "+++ b/path/to/file"
|
|
107
|
+
file_match = re.match(r"^\+\+\+ b/(.*)", line)
|
|
108
|
+
if file_match:
|
|
109
|
+
current_file = file_match.group(1)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Match lines like "@@ -100,5 +100,7 @@" where the + part shows new lines
|
|
113
|
+
range_match = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@", line)
|
|
114
|
+
if range_match and current_file:
|
|
115
|
+
start_line = int(range_match.group(1)) # Keep as 1-based
|
|
116
|
+
line_count = int(range_match.group(2)) if range_match.group(2) else 1
|
|
117
|
+
end_line = start_line + line_count - 1
|
|
118
|
+
result[current_file] = (start_line, end_line)
|
|
119
|
+
|
|
120
|
+
return result
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global Variables and Configuration Module
|
|
3
|
+
This module manages global state and configurations for the Jarvis system.
|
|
4
|
+
It includes:
|
|
5
|
+
- Global agent management
|
|
6
|
+
- Console configuration with custom theme
|
|
7
|
+
- Environment initialization
|
|
8
|
+
"""
|
|
9
|
+
from typing import Any, Set
|
|
10
|
+
import colorama
|
|
11
|
+
import os
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.theme import Theme
|
|
14
|
+
# Initialize colorama for cross-platform colored text
|
|
15
|
+
colorama.init()
|
|
16
|
+
# Disable tokenizers parallelism to avoid issues with multiprocessing
|
|
17
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
18
|
+
# Global agent management
|
|
19
|
+
global_agents: Set[str] = set()
|
|
20
|
+
current_agent_name: str = ""
|
|
21
|
+
# Configure rich console with custom theme
|
|
22
|
+
custom_theme = Theme({
|
|
23
|
+
"INFO": "yellow",
|
|
24
|
+
"WARNING": "yellow",
|
|
25
|
+
"ERROR": "red",
|
|
26
|
+
"SUCCESS": "green",
|
|
27
|
+
"SYSTEM": "cyan",
|
|
28
|
+
"CODE": "green",
|
|
29
|
+
"RESULT": "blue",
|
|
30
|
+
"PLANNING": "magenta",
|
|
31
|
+
"PROGRESS": "white",
|
|
32
|
+
"DEBUG": "blue",
|
|
33
|
+
"USER": "green",
|
|
34
|
+
"TOOL": "yellow",
|
|
35
|
+
})
|
|
36
|
+
console = Console(theme=custom_theme)
|
|
37
|
+
def make_agent_name(agent_name: str) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Generate a unique agent name by appending a suffix if necessary.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
agent_name: The base agent name
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
str: Unique agent name
|
|
46
|
+
"""
|
|
47
|
+
if agent_name in global_agents:
|
|
48
|
+
i = 1
|
|
49
|
+
while f"{agent_name}_{i}" in global_agents:
|
|
50
|
+
i += 1
|
|
51
|
+
return f"{agent_name}_{i}"
|
|
52
|
+
return agent_name
|
|
53
|
+
def set_agent(agent_name: str, agent: Any) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Set the current agent and add it to the global agents set.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
agent_name: The name of the agent
|
|
59
|
+
agent: The agent object
|
|
60
|
+
"""
|
|
61
|
+
global_agents.add(agent_name)
|
|
62
|
+
global current_agent_name
|
|
63
|
+
current_agent_name = agent_name
|
|
64
|
+
def get_agent_list() -> str:
|
|
65
|
+
"""
|
|
66
|
+
Get a formatted string representing the current agent status.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: Formatted string with agent count and current agent name
|
|
70
|
+
"""
|
|
71
|
+
return "[" + str(len(global_agents)) + "]" + current_agent_name if global_agents else ""
|
|
72
|
+
def delete_agent(agent_name: str) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Delete an agent from the global agents set.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
agent_name: The name of the agent to delete
|
|
78
|
+
"""
|
|
79
|
+
if agent_name in global_agents:
|
|
80
|
+
global_agents.remove(agent_name)
|
|
81
|
+
global current_agent_name
|
|
82
|
+
current_agent_name = ""
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input Handling Module
|
|
3
|
+
This module provides utilities for handling user input in the Jarvis system.
|
|
4
|
+
It includes:
|
|
5
|
+
- Single line input with history support
|
|
6
|
+
- Multi-line input with enhanced completion
|
|
7
|
+
- File path completion with fuzzy matching
|
|
8
|
+
- Custom key bindings for input control
|
|
9
|
+
"""
|
|
10
|
+
from prompt_toolkit import PromptSession
|
|
11
|
+
from prompt_toolkit.styles import Style as PromptStyle
|
|
12
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
13
|
+
from prompt_toolkit.completion import Completer, Completion, PathCompleter
|
|
14
|
+
from prompt_toolkit.document import Document
|
|
15
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
16
|
+
from fuzzywuzzy import process
|
|
17
|
+
from colorama import Fore, Style as ColoramaStyle
|
|
18
|
+
from ..jarvis_utils.output import PrettyOutput, OutputType
|
|
19
|
+
def get_single_line_input(tip: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Get single line input with history support.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
tip: The prompt message to display
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
str: The user's input
|
|
28
|
+
"""
|
|
29
|
+
session = PromptSession(history=None)
|
|
30
|
+
style = PromptStyle.from_dict({
|
|
31
|
+
'prompt': 'ansicyan',
|
|
32
|
+
})
|
|
33
|
+
return session.prompt(f"{tip}", style=style)
|
|
34
|
+
class FileCompleter(Completer):
|
|
35
|
+
"""
|
|
36
|
+
Custom completer for file paths with fuzzy matching.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
path_completer: Base path completer
|
|
40
|
+
max_suggestions: Maximum number of suggestions to show
|
|
41
|
+
min_score: Minimum matching score for suggestions
|
|
42
|
+
"""
|
|
43
|
+
def __init__(self):
|
|
44
|
+
"""Initialize the file completer with default settings."""
|
|
45
|
+
self.path_completer = PathCompleter()
|
|
46
|
+
self.max_suggestions = 10
|
|
47
|
+
self.min_score = 10
|
|
48
|
+
def get_completions(self, document: Document, complete_event) -> Completion: # type: ignore
|
|
49
|
+
"""
|
|
50
|
+
Generate completions for file paths with fuzzy matching.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
document: The current document being edited
|
|
54
|
+
complete_event: The completion event
|
|
55
|
+
|
|
56
|
+
Yields:
|
|
57
|
+
Completion: Suggested completions
|
|
58
|
+
"""
|
|
59
|
+
text = document.text_before_cursor
|
|
60
|
+
cursor_pos = document.cursor_position
|
|
61
|
+
# Find all @ positions in text
|
|
62
|
+
at_positions = [i for i, char in enumerate(text) if char == '@']
|
|
63
|
+
if not at_positions:
|
|
64
|
+
return
|
|
65
|
+
# Get the last @ position
|
|
66
|
+
current_at_pos = at_positions[-1]
|
|
67
|
+
# If cursor is not after the last @, don't complete
|
|
68
|
+
if cursor_pos <= current_at_pos:
|
|
69
|
+
return
|
|
70
|
+
# Check if there's a space after @
|
|
71
|
+
text_after_at = text[current_at_pos + 1:cursor_pos]
|
|
72
|
+
if ' ' in text_after_at:
|
|
73
|
+
return
|
|
74
|
+
# Get the text after the current @
|
|
75
|
+
file_path = text_after_at.strip()
|
|
76
|
+
# Calculate replacement length
|
|
77
|
+
replace_length = len(text_after_at) + 1
|
|
78
|
+
# Get all possible files using git ls-files
|
|
79
|
+
all_files = []
|
|
80
|
+
try:
|
|
81
|
+
import subprocess
|
|
82
|
+
result = subprocess.run(['git', 'ls-files'],
|
|
83
|
+
stdout=subprocess.PIPE,
|
|
84
|
+
stderr=subprocess.PIPE,
|
|
85
|
+
text=True)
|
|
86
|
+
if result.returncode == 0:
|
|
87
|
+
all_files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
# Generate completions
|
|
91
|
+
if not file_path:
|
|
92
|
+
scored_files = [(path, 100) for path in all_files[:self.max_suggestions]]
|
|
93
|
+
else:
|
|
94
|
+
scored_files_data = process.extract(file_path, all_files, limit=self.max_suggestions)
|
|
95
|
+
scored_files = [(m[0], m[1]) for m in scored_files_data]
|
|
96
|
+
scored_files.sort(key=lambda x: x[1], reverse=True)
|
|
97
|
+
scored_files = scored_files[:self.max_suggestions]
|
|
98
|
+
# Yield completions
|
|
99
|
+
for path, score in scored_files:
|
|
100
|
+
if not file_path or score > self.min_score:
|
|
101
|
+
display_text = path
|
|
102
|
+
if file_path and score < 100:
|
|
103
|
+
display_text = f"{path} ({score}%)"
|
|
104
|
+
yield Completion(
|
|
105
|
+
text=f"'{path}'",
|
|
106
|
+
start_position=-replace_length,
|
|
107
|
+
display=display_text,
|
|
108
|
+
display_meta="File"
|
|
109
|
+
) # type: ignore
|
|
110
|
+
def get_multiline_input(tip: str) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Get multi-line input with enhanced completion and confirmation.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
tip: The prompt message to display
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
str: The user's input, or empty string if canceled
|
|
119
|
+
"""
|
|
120
|
+
# Display input instructions
|
|
121
|
+
PrettyOutput.section("用户输入 - 使用 @ 触发文件补全,Tab 选择补全项,Ctrl+J 提交,按 Ctrl+C 取消输入", OutputType.USER)
|
|
122
|
+
print(f"{Fore.GREEN}{tip}{ColoramaStyle.RESET_ALL}")
|
|
123
|
+
# Configure key bindings
|
|
124
|
+
bindings = KeyBindings()
|
|
125
|
+
@bindings.add('enter')
|
|
126
|
+
def _(event):
|
|
127
|
+
"""Handle enter key for completion or new line."""
|
|
128
|
+
if event.current_buffer.complete_state:
|
|
129
|
+
event.current_buffer.apply_completion(event.current_buffer.complete_state.current_completion)
|
|
130
|
+
else:
|
|
131
|
+
event.current_buffer.insert_text('\n')
|
|
132
|
+
@bindings.add('c-j')
|
|
133
|
+
def _(event):
|
|
134
|
+
"""Handle Ctrl+J for submission."""
|
|
135
|
+
event.current_buffer.validate_and_handle()
|
|
136
|
+
# Configure prompt session
|
|
137
|
+
style = PromptStyle.from_dict({
|
|
138
|
+
'prompt': 'ansicyan',
|
|
139
|
+
})
|
|
140
|
+
try:
|
|
141
|
+
session = PromptSession(
|
|
142
|
+
history=None,
|
|
143
|
+
completer=FileCompleter(),
|
|
144
|
+
key_bindings=bindings,
|
|
145
|
+
complete_while_typing=True,
|
|
146
|
+
multiline=True,
|
|
147
|
+
vi_mode=False,
|
|
148
|
+
mouse_support=False
|
|
149
|
+
)
|
|
150
|
+
prompt = FormattedText([
|
|
151
|
+
('class:prompt', '>>> ')
|
|
152
|
+
])
|
|
153
|
+
# Get input
|
|
154
|
+
text = session.prompt(
|
|
155
|
+
prompt,
|
|
156
|
+
style=style,
|
|
157
|
+
).strip()
|
|
158
|
+
return text
|
|
159
|
+
except KeyboardInterrupt:
|
|
160
|
+
PrettyOutput.print("输入已取消", OutputType.INFO)
|
|
161
|
+
return ""
|