jarvis-ai-assistant 0.1.64__py3-none-any.whl → 0.1.66__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_codebase/__init__.py +0 -0
- jarvis/jarvis_codebase/main.py +342 -0
- jarvis/jarvis_coder/main.py +24 -361
- jarvis/main.py +0 -2
- jarvis/models/ai8.py +1 -2
- jarvis/models/openai.py +0 -1
- jarvis/models/oyi.py +1 -4
- jarvis/tools/__init__.py +4 -0
- jarvis/tools/codebase_qa.py +70 -0
- jarvis/utils.py +7 -0
- {jarvis_ai_assistant-0.1.64.dist-info → jarvis_ai_assistant-0.1.66.dist-info}/METADATA +38 -3
- jarvis_ai_assistant-0.1.66.dist-info/RECORD +32 -0
- {jarvis_ai_assistant-0.1.64.dist-info → jarvis_ai_assistant-0.1.66.dist-info}/entry_points.txt +1 -0
- jarvis_ai_assistant-0.1.64.dist-info/RECORD +0 -29
- {jarvis_ai_assistant-0.1.64.dist-info → jarvis_ai_assistant-0.1.66.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.64.dist-info → jarvis_ai_assistant-0.1.66.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.64.dist-info → jarvis_ai_assistant-0.1.66.dist-info}/top_level.txt +0 -0
jarvis/__init__.py
CHANGED
|
File without changes
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import sqlite3
|
|
4
|
+
import time
|
|
5
|
+
import numpy as np
|
|
6
|
+
import faiss
|
|
7
|
+
from typing import List, Tuple, Optional
|
|
8
|
+
from jarvis.models.registry import PlatformRegistry
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
from threading import Lock
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from jarvis.utils import OutputType, PrettyOutput, find_git_root
|
|
13
|
+
from jarvis.utils import load_env_from_file
|
|
14
|
+
import argparse
|
|
15
|
+
from sentence_transformers import SentenceTransformer
|
|
16
|
+
|
|
17
|
+
class CodeBase:
|
|
18
|
+
def __init__(self, root_dir: str, thread_count: int = 10):
|
|
19
|
+
load_env_from_file()
|
|
20
|
+
self.root_dir = root_dir
|
|
21
|
+
os.chdir(self.root_dir)
|
|
22
|
+
self.thread_count = thread_count
|
|
23
|
+
self.cheap_platform = os.environ.get("JARVIS_CHEAP_PLATFORM") or os.environ.get("JARVIS_PLATFORM") or "kimi"
|
|
24
|
+
self.cheap_model = os.environ.get("JARVIS_CHEAP_MODEL") or os.environ.get("JARVIS_MODEL") or "kimi"
|
|
25
|
+
self.normal_platform = os.environ.get("JARVIS_PLATFORM") or "kimi"
|
|
26
|
+
self.normal_model = os.environ.get("JARVIS_MODEL") or "kimi"
|
|
27
|
+
self.embedding_model_name = os.environ.get("JARVIS_EMBEDDING_MODEL") or "BAAI/bge-large-zh-v1.5"
|
|
28
|
+
if not self.cheap_platform or not self.cheap_model or not self.embedding_model_name or not self.normal_platform or not self.normal_model:
|
|
29
|
+
raise ValueError("JARVIS_CHEAP_PLATFORM or JARVIS_CHEAP_MODEL or JARVIS_EMBEDDING_MODEL or JARVIS_PLATFORM or JARVIS_MODEL is not set")
|
|
30
|
+
|
|
31
|
+
PrettyOutput.print(f"廉价模型使用平台: {self.cheap_platform} 模型: {self.cheap_model}", output_type=OutputType.INFO)
|
|
32
|
+
PrettyOutput.print(f"分析模型使用平台: {self.normal_platform} 模型: {self.normal_model}", output_type=OutputType.INFO)
|
|
33
|
+
PrettyOutput.print(f"嵌入模型: {self.embedding_model_name}", output_type=OutputType.INFO)
|
|
34
|
+
PrettyOutput.print(f"检索算法:分层导航小世界算法", output_type=OutputType.INFO)
|
|
35
|
+
|
|
36
|
+
# 初始化数据目录
|
|
37
|
+
self.data_dir = os.path.join(self.root_dir, ".jarvis-codebase")
|
|
38
|
+
if not os.path.exists(self.data_dir):
|
|
39
|
+
os.makedirs(self.data_dir)
|
|
40
|
+
|
|
41
|
+
# 初始化嵌入模型,使用系统默认缓存目录
|
|
42
|
+
try:
|
|
43
|
+
PrettyOutput.print("正在加载/下载模型,请稍候...", output_type=OutputType.INFO)
|
|
44
|
+
self.embedding_model = SentenceTransformer(self.embedding_model_name)
|
|
45
|
+
|
|
46
|
+
# 强制完全加载所有模型组件
|
|
47
|
+
test_text = """
|
|
48
|
+
这是一段测试文本,用于确保模型完全加载。
|
|
49
|
+
包含多行内容,以模拟实际使用场景。
|
|
50
|
+
"""
|
|
51
|
+
# 预热模型,确保所有组件都被加载
|
|
52
|
+
self.embedding_model.encode([test_text],
|
|
53
|
+
convert_to_tensor=True,
|
|
54
|
+
normalize_embeddings=True)
|
|
55
|
+
PrettyOutput.print("模型加载完成", output_type=OutputType.SUCCESS)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
PrettyOutput.print(f"加载模型失败: {str(e)}", output_type=OutputType.ERROR)
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
self.vector_dim = self.embedding_model.get_sentence_embedding_dimension()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
self.db_path = os.path.join(self.data_dir, "codebase.db")
|
|
64
|
+
if not os.path.exists(self.db_path):
|
|
65
|
+
self.create_db()
|
|
66
|
+
self.git_file_list = self.get_git_file_list()
|
|
67
|
+
self.platform_registry = PlatformRegistry().get_global_platform_registry()
|
|
68
|
+
self.index_path = os.path.join(self.data_dir, "vectors.index")
|
|
69
|
+
self.index = None
|
|
70
|
+
if os.path.exists(self.index_path):
|
|
71
|
+
PrettyOutput.print("正在加载向量数据库", output_type=OutputType.INFO)
|
|
72
|
+
self.index = faiss.read_index(self.index_path)
|
|
73
|
+
|
|
74
|
+
def get_git_file_list(self):
|
|
75
|
+
return os.popen("git ls-files").read().splitlines()
|
|
76
|
+
|
|
77
|
+
def get_db_connection(self):
|
|
78
|
+
"""创建并返回一个新的数据库连接"""
|
|
79
|
+
return sqlite3.connect(self.db_path)
|
|
80
|
+
|
|
81
|
+
def clean_db(self) -> bool:
|
|
82
|
+
"""清理数据库和向量索引中的过期记录"""
|
|
83
|
+
db = self.get_db_connection()
|
|
84
|
+
try:
|
|
85
|
+
# 获取所有数据库记录
|
|
86
|
+
all_records = db.execute("SELECT path FROM codebase").fetchall()
|
|
87
|
+
files_to_delete = []
|
|
88
|
+
|
|
89
|
+
# 找出需要删除的文件
|
|
90
|
+
for row in all_records:
|
|
91
|
+
if row[0] not in self.git_file_list:
|
|
92
|
+
files_to_delete.append(row[0])
|
|
93
|
+
|
|
94
|
+
if not files_to_delete:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
for file_path in files_to_delete:
|
|
98
|
+
db.execute("DELETE FROM codebase WHERE path = ?", (file_path,))
|
|
99
|
+
|
|
100
|
+
db.commit()
|
|
101
|
+
|
|
102
|
+
PrettyOutput.print(f"清理了 {len(files_to_delete)} 个文件的记录",
|
|
103
|
+
output_type=OutputType.INFO)
|
|
104
|
+
return True
|
|
105
|
+
finally:
|
|
106
|
+
db.close()
|
|
107
|
+
|
|
108
|
+
def create_db(self):
|
|
109
|
+
db = self.get_db_connection()
|
|
110
|
+
try:
|
|
111
|
+
db.execute("CREATE TABLE IF NOT EXISTS codebase (path TEXT, md5 TEXT ,description TEXT)")
|
|
112
|
+
db.commit()
|
|
113
|
+
finally:
|
|
114
|
+
db.close()
|
|
115
|
+
|
|
116
|
+
def is_text_file(self, file_path: str):
|
|
117
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
118
|
+
try:
|
|
119
|
+
f.read()
|
|
120
|
+
return True
|
|
121
|
+
except UnicodeDecodeError:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def make_description(self, file_path: str) -> str:
|
|
125
|
+
model = self.platform_registry.create_platform(self.cheap_platform)
|
|
126
|
+
model.set_model_name(self.cheap_model)
|
|
127
|
+
model.set_suppress_output(True)
|
|
128
|
+
content = open(file_path, "r", encoding="utf-8").read()
|
|
129
|
+
prompt = f"""请分析以下代码文件,并生成一个详细的描述。描述应该包含以下要点:
|
|
130
|
+
|
|
131
|
+
1. 主要功能和用途
|
|
132
|
+
2. 关键类和方法的作用
|
|
133
|
+
3. 重要的依赖和技术特征(如使用了什么框架、算法、设计模式等)
|
|
134
|
+
4. 代码处理的主要数据类型和数据结构
|
|
135
|
+
5. 关键业务逻辑和处理流程
|
|
136
|
+
6. 特殊功能点和亮点特性
|
|
137
|
+
|
|
138
|
+
请用简洁专业的语言描述,突出代码的技术特征和功能特点,以便后续进行相似代码检索。
|
|
139
|
+
|
|
140
|
+
文件路径:{file_path}
|
|
141
|
+
代码内容:
|
|
142
|
+
{content}
|
|
143
|
+
"""
|
|
144
|
+
response = model.chat(prompt)
|
|
145
|
+
return response
|
|
146
|
+
|
|
147
|
+
def get_embedding(self, text: str) -> np.ndarray:
|
|
148
|
+
"""使用 transformers 模型获取文本的向量表示"""
|
|
149
|
+
# 对长文本进行截断
|
|
150
|
+
max_length = 512 # 或其他合适的长度
|
|
151
|
+
text = ' '.join(text.split()[:max_length])
|
|
152
|
+
|
|
153
|
+
# 获取嵌入向量
|
|
154
|
+
embedding = self.embedding_model.encode(text,
|
|
155
|
+
normalize_embeddings=True, # L2归一化
|
|
156
|
+
show_progress_bar=False)
|
|
157
|
+
return np.array(embedding, dtype=np.float32)
|
|
158
|
+
|
|
159
|
+
def vectorize_file(self, file_path: str, description: str) -> np.ndarray:
|
|
160
|
+
"""将文件内容和描述向量化"""
|
|
161
|
+
try:
|
|
162
|
+
# 组合文件信息
|
|
163
|
+
combined_text = f"""
|
|
164
|
+
文件路径: {file_path}
|
|
165
|
+
文件描述: {description}
|
|
166
|
+
"""
|
|
167
|
+
return self.get_embedding(combined_text)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
PrettyOutput.print(f"Error vectorizing file {file_path}: {str(e)}",
|
|
170
|
+
output_type=OutputType.ERROR)
|
|
171
|
+
return np.zeros(self.vector_dim, dtype=np.float32)
|
|
172
|
+
|
|
173
|
+
def process_file(self, file):
|
|
174
|
+
"""处理单个文件的辅助方法"""
|
|
175
|
+
db = self.get_db_connection()
|
|
176
|
+
try:
|
|
177
|
+
if not self.is_text_file(file):
|
|
178
|
+
return None
|
|
179
|
+
md5 = hashlib.md5(open(file, "rb").read()).hexdigest()
|
|
180
|
+
if db.execute("SELECT path FROM codebase WHERE md5 = ?", (md5,)).fetchone():
|
|
181
|
+
return None
|
|
182
|
+
description = self.make_description(file)
|
|
183
|
+
return (file, md5, description)
|
|
184
|
+
finally:
|
|
185
|
+
db.close()
|
|
186
|
+
|
|
187
|
+
def gen_vector_db_from_sqlite(self):
|
|
188
|
+
self.index = faiss.IndexHNSWFlat(self.vector_dim, 16)
|
|
189
|
+
self.index.hnsw.efConstruction = 40
|
|
190
|
+
self.index.hnsw.efSearch = 16
|
|
191
|
+
db = self.get_db_connection()
|
|
192
|
+
try:
|
|
193
|
+
all_records = db.execute("SELECT path, description FROM codebase").fetchall()
|
|
194
|
+
for row in all_records:
|
|
195
|
+
file, description = row
|
|
196
|
+
PrettyOutput.print(f"正在向量化文件: {file}", output_type=OutputType.INFO)
|
|
197
|
+
vector = self.vectorize_file(file, description)
|
|
198
|
+
vector = vector.reshape(1, -1)
|
|
199
|
+
self.index.add(vector)
|
|
200
|
+
faiss.write_index(self.index, self.index_path)
|
|
201
|
+
finally:
|
|
202
|
+
db.close()
|
|
203
|
+
|
|
204
|
+
def generate_codebase(self):
|
|
205
|
+
updated =self.clean_db()
|
|
206
|
+
db_lock = Lock()
|
|
207
|
+
processed_files = [] # 用于跟踪已处理的文件
|
|
208
|
+
|
|
209
|
+
def process_and_save(file):
|
|
210
|
+
result = self.process_file(file)
|
|
211
|
+
if result:
|
|
212
|
+
file, md5, description = result
|
|
213
|
+
db = self.get_db_connection()
|
|
214
|
+
try:
|
|
215
|
+
with db_lock:
|
|
216
|
+
db.execute("DELETE FROM codebase WHERE path = ?", (file,))
|
|
217
|
+
db.execute("INSERT INTO codebase (path, md5, description) VALUES (?, ?, ?)",
|
|
218
|
+
(file, md5, description))
|
|
219
|
+
db.commit()
|
|
220
|
+
PrettyOutput.print(f"索引文件: {file}", output_type=OutputType.INFO)
|
|
221
|
+
processed_files.append(file)
|
|
222
|
+
finally:
|
|
223
|
+
db.close()
|
|
224
|
+
|
|
225
|
+
# 使用 ThreadPoolExecutor 并等待所有任务完成
|
|
226
|
+
with ThreadPoolExecutor(max_workers=self.thread_count) as executor:
|
|
227
|
+
futures = [executor.submit(process_and_save, file) for file in self.git_file_list]
|
|
228
|
+
# 等待所有任务完成
|
|
229
|
+
concurrent.futures.wait(futures)
|
|
230
|
+
|
|
231
|
+
if updated or len(processed_files) > 0:
|
|
232
|
+
PrettyOutput.print("有新的文件被删除或添加,正在重新生成向量数据库", output_type=OutputType.INFO)
|
|
233
|
+
self.gen_vector_db_from_sqlite()
|
|
234
|
+
else:
|
|
235
|
+
PrettyOutput.print("没有新的文件被删除或添加,跳过向量数据库生成", output_type=OutputType.INFO)
|
|
236
|
+
|
|
237
|
+
PrettyOutput.print(f"成功索引 {len(processed_files)} 个文件", output_type=OutputType.INFO)
|
|
238
|
+
|
|
239
|
+
def search_similar(self, query: str, top_k: int = 5) -> List[Tuple[str, float, str]]:
|
|
240
|
+
"""搜索与查询最相似的文件
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
query: 查询文本
|
|
244
|
+
top_k: 返回结果数量
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of (file_path, similarity_score, description) tuples
|
|
248
|
+
"""
|
|
249
|
+
# 获取查询文本的向量表示
|
|
250
|
+
query_vector = self.get_embedding(query)
|
|
251
|
+
query_vector = query_vector.reshape(1, -1)
|
|
252
|
+
|
|
253
|
+
# 搜索最相似的向量
|
|
254
|
+
distances, indices = self.index.search(query_vector, top_k)
|
|
255
|
+
|
|
256
|
+
# 获取对应的文件信息
|
|
257
|
+
db = self.get_db_connection()
|
|
258
|
+
try:
|
|
259
|
+
results = []
|
|
260
|
+
for i, distance in zip(indices[0], distances[0]):
|
|
261
|
+
if i == -1: # faiss返回-1表示无效结果
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# 将numpy.int64转换为Python int
|
|
265
|
+
offset = int(i)
|
|
266
|
+
# 获取文件路径和描述
|
|
267
|
+
cursor = db.execute("SELECT path, description FROM codebase LIMIT 1 OFFSET ?", (offset,))
|
|
268
|
+
row = cursor.fetchone()
|
|
269
|
+
if row:
|
|
270
|
+
path, description = row
|
|
271
|
+
# 将distance转换为相似度分数(0-1之间)
|
|
272
|
+
similarity = 1.0 / (1.0 + float(distance)) # 确保使用Python float
|
|
273
|
+
results.append((path, similarity, description))
|
|
274
|
+
|
|
275
|
+
return results
|
|
276
|
+
finally:
|
|
277
|
+
db.close()
|
|
278
|
+
|
|
279
|
+
def ask_codebase(self, query: str, top_k: int = 5) -> List[Tuple[str, float, str]]:
|
|
280
|
+
"""Ask a question about the codebase"""
|
|
281
|
+
# 使用搜索函数获取相似文件
|
|
282
|
+
results = self.search_similar(query, top_k)
|
|
283
|
+
PrettyOutput.print(f"找到的关联文件: ", output_type=OutputType.INFO)
|
|
284
|
+
for path, score, _ in results:
|
|
285
|
+
PrettyOutput.print(f"文件: {path} 关联度: {score:.3f}", output_type=OutputType.INFO)
|
|
286
|
+
|
|
287
|
+
prompt = f"""你是一个代码专家,请根据以下文件信息回答用户的问题:
|
|
288
|
+
"""
|
|
289
|
+
for path, _, _ in results:
|
|
290
|
+
content = open(path, "r", encoding="utf-8").read()
|
|
291
|
+
prompt += f"""
|
|
292
|
+
文件路径: {path}
|
|
293
|
+
文件内容:
|
|
294
|
+
{content}
|
|
295
|
+
========================================
|
|
296
|
+
"""
|
|
297
|
+
prompt += f"""
|
|
298
|
+
用户问题: {query}
|
|
299
|
+
|
|
300
|
+
请用专业的语言回答用户的问题,如果给出的文件内容不足以回答用户的问题,请告诉用户,绝对不要胡编乱造。
|
|
301
|
+
"""
|
|
302
|
+
model = self.platform_registry.create_platform(self.normal_platform)
|
|
303
|
+
model.set_model_name(self.normal_model)
|
|
304
|
+
response = model.chat(prompt)
|
|
305
|
+
return response
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def main():
|
|
309
|
+
parser = argparse.ArgumentParser(description='Codebase management and search tool')
|
|
310
|
+
parser.add_argument('--search', type=str, help='Search query to find similar code files')
|
|
311
|
+
parser.add_argument('--top-k', type=int, default=5, help='Number of results to return (default: 5)')
|
|
312
|
+
parser.add_argument('--ask', type=str, help='Ask a question about the codebase')
|
|
313
|
+
args = parser.parse_args()
|
|
314
|
+
|
|
315
|
+
current_dir = find_git_root()
|
|
316
|
+
codebase = CodeBase(current_dir)
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
codebase.generate_codebase()
|
|
320
|
+
PrettyOutput.print("\nCodebase generation completed", output_type=OutputType.SUCCESS)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
PrettyOutput.print(f"Error during codebase generation: {str(e)}", output_type=OutputType.ERROR)
|
|
323
|
+
|
|
324
|
+
if args.search:
|
|
325
|
+
results = codebase.search_similar(args.search, args.top_k)
|
|
326
|
+
if not results:
|
|
327
|
+
PrettyOutput.print("No similar files found", output_type=OutputType.WARNING)
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
PrettyOutput.print("\nSearch Results:", output_type=OutputType.INFO)
|
|
331
|
+
for path, score, desc in results:
|
|
332
|
+
PrettyOutput.print("\n" + "="*50, output_type=OutputType.INFO)
|
|
333
|
+
PrettyOutput.print(f"File: {path}", output_type=OutputType.INFO)
|
|
334
|
+
PrettyOutput.print(f"Similarity: {score:.3f}", output_type=OutputType.INFO)
|
|
335
|
+
PrettyOutput.print(f"Description: {desc[100:]}", output_type=OutputType.INFO)
|
|
336
|
+
|
|
337
|
+
if args.ask:
|
|
338
|
+
codebase.ask_codebase(args.ask, args.top_k)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == "__main__":
|
|
342
|
+
exit(main())
|
jarvis/jarvis_coder/main.py
CHANGED
|
@@ -9,8 +9,9 @@ from typing import Dict, Any, List, Optional, Tuple
|
|
|
9
9
|
|
|
10
10
|
import yaml
|
|
11
11
|
from jarvis.models.base import BasePlatform
|
|
12
|
-
from jarvis.utils import OutputType, PrettyOutput, get_multiline_input, load_env_from_file
|
|
12
|
+
from jarvis.utils import OutputType, PrettyOutput, find_git_root, get_multiline_input, load_env_from_file
|
|
13
13
|
from jarvis.models.registry import PlatformRegistry
|
|
14
|
+
from jarvis.jarvis_codebase.main import CodeBase
|
|
14
15
|
from prompt_toolkit import PromptSession
|
|
15
16
|
from prompt_toolkit.completion import WordCompleter, Completer, Completion
|
|
16
17
|
from prompt_toolkit.formatted_text import FormattedText
|
|
@@ -23,12 +24,15 @@ index_lock = threading.Lock()
|
|
|
23
24
|
class JarvisCoder:
|
|
24
25
|
def __init__(self, root_dir: str, language: str):
|
|
25
26
|
"""初始化代码修改工具"""
|
|
27
|
+
|
|
28
|
+
self.platform = os.environ.get("JARVIS_CODEGEN_PLATFORM") or os.environ.get("JARVIS_PLATFORM")
|
|
29
|
+
self.model = os.environ.get("JARVIS_CODEGEN_MODEL") or os.environ.get("JARVIS_MODEL")
|
|
26
30
|
|
|
27
|
-
self.root_dir = root_dir
|
|
28
|
-
self.platform = os.environ.get("JARVIS_CODEGEN_PLATFORM")
|
|
29
|
-
self.model = os.environ.get("JARVIS_CODEGEN_MODEL")
|
|
30
31
|
|
|
31
|
-
self.
|
|
32
|
+
if not self.platform or not self.model:
|
|
33
|
+
raise ValueError("JARVIS_CODEGEN_PLATFORM or JARVIS_CODEGEN_MODEL is not set")
|
|
34
|
+
|
|
35
|
+
self.root_dir = find_git_root(root_dir)
|
|
32
36
|
if not self.root_dir:
|
|
33
37
|
self.root_dir = root_dir
|
|
34
38
|
|
|
@@ -46,10 +50,6 @@ class JarvisCoder:
|
|
|
46
50
|
if not os.path.exists(self.jarvis_dir):
|
|
47
51
|
os.makedirs(self.jarvis_dir)
|
|
48
52
|
|
|
49
|
-
self.index_db_path = os.path.join(self.jarvis_dir, "index.db")
|
|
50
|
-
if not os.path.exists(self.index_db_path):
|
|
51
|
-
self._create_index_db()
|
|
52
|
-
|
|
53
53
|
self.record_dir = os.path.join(self.jarvis_dir, "record")
|
|
54
54
|
if not os.path.exists(self.record_dir):
|
|
55
55
|
os.makedirs(self.record_dir)
|
|
@@ -70,6 +70,9 @@ class JarvisCoder:
|
|
|
70
70
|
os.system(f"git add .")
|
|
71
71
|
os.system(f"git commit -m 'commit before code edit'")
|
|
72
72
|
|
|
73
|
+
# 4. 初始化代码库
|
|
74
|
+
self._codebase = CodeBase(self.root_dir)
|
|
75
|
+
|
|
73
76
|
def _new_model(self):
|
|
74
77
|
"""获取大模型"""
|
|
75
78
|
model = PlatformRegistry().get_global_platform_registry().create_platform(self.platform)
|
|
@@ -114,341 +117,6 @@ class JarvisCoder:
|
|
|
114
117
|
time.sleep(delay)
|
|
115
118
|
delay *= 2 # 指数退避
|
|
116
119
|
|
|
117
|
-
def _get_key_info(self, file_path: str, content: str) -> Optional[Dict[str, Any]]:
|
|
118
|
-
"""获取文件的关键信息
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
file_path: 文件路径
|
|
122
|
-
content: 文件内容
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
Optional[Dict[str, Any]]: 文件信息,包含文件描述
|
|
126
|
-
"""
|
|
127
|
-
model = self._new_model() # 创建新的模型实例
|
|
128
|
-
model.set_suppress_output(True)
|
|
129
|
-
|
|
130
|
-
prompt = f"""你是一个资深程序员,请根据文件内容,生成文件的关键信息,要求如下,除了代码,不要输出任何内容:
|
|
131
|
-
|
|
132
|
-
1. 文件路径: {file_path}
|
|
133
|
-
2. 文件内容:(<CONTENT_START>和<CONTENT_END>之间的部分)
|
|
134
|
-
<CONTENT_START>
|
|
135
|
-
{content}
|
|
136
|
-
<CONTENT_END>
|
|
137
|
-
3. 关键信息: 请生成这个文件的主要功能和作用描述,包含的特征符号(函数和类、变量等),不超过100字
|
|
138
|
-
"""
|
|
139
|
-
try:
|
|
140
|
-
return model.chat(prompt)
|
|
141
|
-
except Exception as e:
|
|
142
|
-
PrettyOutput.print(f"解析文件信息失败: {str(e)}", OutputType.ERROR)
|
|
143
|
-
return None
|
|
144
|
-
finally:
|
|
145
|
-
# 确保清理模型资源
|
|
146
|
-
try:
|
|
147
|
-
model.delete_chat()
|
|
148
|
-
except:
|
|
149
|
-
pass
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _get_file_md5(self, file_path: str) -> str:
|
|
154
|
-
"""获取文件MD5"""
|
|
155
|
-
return hashlib.md5(open(file_path, "rb").read()).hexdigest()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _create_index_db(self):
|
|
159
|
-
"""创建索引数据库"""
|
|
160
|
-
with index_lock:
|
|
161
|
-
if not os.path.exists(self.index_db_path):
|
|
162
|
-
PrettyOutput.print("Index database does not exist, creating...", OutputType.INFO)
|
|
163
|
-
index_db = sqlite3.connect(self.index_db_path)
|
|
164
|
-
index_db.execute(
|
|
165
|
-
"CREATE TABLE files (file_path TEXT PRIMARY KEY, file_md5 TEXT, file_description TEXT)")
|
|
166
|
-
index_db.commit()
|
|
167
|
-
index_db.close()
|
|
168
|
-
PrettyOutput.print("Index database created", OutputType.SUCCESS)
|
|
169
|
-
# commit
|
|
170
|
-
os.chdir(self.root_dir)
|
|
171
|
-
os.system(f"git add .gitignore -f")
|
|
172
|
-
os.system(f"git commit -m 'add index database'")
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _find_file_by_md5(self, file_md5: str) -> Optional[str]:
|
|
176
|
-
"""根据文件MD5查找文件路径"""
|
|
177
|
-
with index_lock:
|
|
178
|
-
index_db = sqlite3.connect(self.index_db_path)
|
|
179
|
-
cursor = index_db.cursor()
|
|
180
|
-
cursor.execute(
|
|
181
|
-
"SELECT file_path FROM files WHERE file_md5 = ?", (file_md5,))
|
|
182
|
-
result = cursor.fetchone()
|
|
183
|
-
index_db.close()
|
|
184
|
-
return result[0] if result else None
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def _update_file_path(self, file_path: str, file_md5: str):
|
|
188
|
-
"""更新文件路径"""
|
|
189
|
-
with index_lock:
|
|
190
|
-
index_db = sqlite3.connect(self.index_db_path)
|
|
191
|
-
cursor = index_db.cursor()
|
|
192
|
-
cursor.execute(
|
|
193
|
-
"UPDATE files SET file_path = ? WHERE file_md5 = ?", (file_path, file_md5))
|
|
194
|
-
index_db.commit()
|
|
195
|
-
index_db.close()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def _insert_info(self, file_path: str, file_md5: str, file_description: str):
|
|
199
|
-
"""插入文件信息"""
|
|
200
|
-
with index_lock:
|
|
201
|
-
index_db = sqlite3.connect(self.index_db_path)
|
|
202
|
-
cursor = index_db.cursor()
|
|
203
|
-
cursor.execute("DELETE FROM files WHERE file_path = ?", (file_path,))
|
|
204
|
-
cursor.execute("INSERT INTO files (file_path, file_md5, file_description) VALUES (?, ?, ?)",
|
|
205
|
-
(file_path, file_md5, file_description))
|
|
206
|
-
index_db.commit()
|
|
207
|
-
index_db.close()
|
|
208
|
-
|
|
209
|
-
def _is_text_file(self, file_path: str) -> bool:
|
|
210
|
-
"""判断文件是否是文本文件"""
|
|
211
|
-
try:
|
|
212
|
-
with open(file_path, 'rb') as f:
|
|
213
|
-
# 读取文件前1024个字节
|
|
214
|
-
chunk = f.read(1024)
|
|
215
|
-
# 检查是否包含空字节
|
|
216
|
-
if b'\x00' in chunk:
|
|
217
|
-
return False
|
|
218
|
-
# 尝试解码为文本
|
|
219
|
-
chunk.decode('utf-8')
|
|
220
|
-
return True
|
|
221
|
-
except:
|
|
222
|
-
return False
|
|
223
|
-
|
|
224
|
-
def _index_project(self):
|
|
225
|
-
"""建立代码库索引"""
|
|
226
|
-
import threading
|
|
227
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
228
|
-
|
|
229
|
-
git_files = os.popen("git ls-files").read().splitlines()
|
|
230
|
-
|
|
231
|
-
index_db = sqlite3.connect(self.index_db_path)
|
|
232
|
-
cursor = index_db.cursor()
|
|
233
|
-
cursor.execute("SELECT file_path FROM files")
|
|
234
|
-
db_files = [row[0] for row in cursor.fetchall()]
|
|
235
|
-
for db_file in db_files:
|
|
236
|
-
if not os.path.exists(db_file):
|
|
237
|
-
cursor.execute("DELETE FROM files WHERE file_path = ?", (db_file,))
|
|
238
|
-
PrettyOutput.print(f"删除不存在的文件记录: {db_file}", OutputType.INFO)
|
|
239
|
-
index_db.commit()
|
|
240
|
-
index_db.close()
|
|
241
|
-
|
|
242
|
-
def process_file(file_path: str):
|
|
243
|
-
"""处理单个文件的索引任务"""
|
|
244
|
-
if not self._is_text_file(file_path):
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
# 计算文件MD5
|
|
248
|
-
file_md5 = self._get_file_md5(file_path)
|
|
249
|
-
|
|
250
|
-
# 查找文件
|
|
251
|
-
file_path_in_db = self._find_file_by_md5(file_md5)
|
|
252
|
-
if file_path_in_db:
|
|
253
|
-
PrettyOutput.print(
|
|
254
|
-
f"文件 {file_path} 重复,跳过", OutputType.INFO)
|
|
255
|
-
if file_path_in_db != file_path:
|
|
256
|
-
self._update_file_path(file_path, file_md5)
|
|
257
|
-
PrettyOutput.print(
|
|
258
|
-
f"文件 {file_path} 重复,更新路径为 {file_path}", OutputType.INFO)
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
|
262
|
-
file_content = f.read()
|
|
263
|
-
key_info = self._get_key_info(file_path, file_content)
|
|
264
|
-
if not key_info:
|
|
265
|
-
PrettyOutput.print(
|
|
266
|
-
f"文件 {file_path} 索引失败", OutputType.INFO)
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
self._insert_info(file_path, file_md5, key_info)
|
|
270
|
-
PrettyOutput.print(
|
|
271
|
-
f"文件 {file_path} 已建立索引", OutputType.INFO)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
# 使用线程池处理文件索引
|
|
275
|
-
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
276
|
-
futures = [executor.submit(process_file, file_path) for file_path in git_files]
|
|
277
|
-
for future in as_completed(futures):
|
|
278
|
-
try:
|
|
279
|
-
future.result()
|
|
280
|
-
except Exception as e:
|
|
281
|
-
PrettyOutput.print(f"处理文件时发生错误: {str(e)}", OutputType.ERROR)
|
|
282
|
-
|
|
283
|
-
PrettyOutput.print("项目索引完成", OutputType.INFO)
|
|
284
|
-
|
|
285
|
-
def _get_files_from_db(self) -> List[Tuple[str, str]]:
|
|
286
|
-
"""从数据库获取所有文件信息
|
|
287
|
-
|
|
288
|
-
Returns:
|
|
289
|
-
List[Tuple[str, str]]: [(file_path, file_description), ...]
|
|
290
|
-
"""
|
|
291
|
-
try:
|
|
292
|
-
index_db = sqlite3.connect(self.index_db_path)
|
|
293
|
-
cursor = index_db.cursor()
|
|
294
|
-
cursor.execute("SELECT file_path, file_description FROM files")
|
|
295
|
-
all_files = cursor.fetchall()
|
|
296
|
-
index_db.close()
|
|
297
|
-
return all_files
|
|
298
|
-
except sqlite3.Error as e:
|
|
299
|
-
PrettyOutput.print(f"数据库操作失败: {str(e)}", OutputType.ERROR)
|
|
300
|
-
return []
|
|
301
|
-
|
|
302
|
-
def _analyze_files_in_batches(self, all_files: List[Tuple[str, str]], feature: str, batch_size: int = 100) -> List[Dict]:
|
|
303
|
-
"""批量分析文件相关性
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
all_files: 所有文件列表
|
|
307
|
-
feature: 需求描述
|
|
308
|
-
batch_size: 批处理大小
|
|
309
|
-
|
|
310
|
-
Returns:
|
|
311
|
-
List[Dict]: 带评分的文件列表
|
|
312
|
-
"""
|
|
313
|
-
batch_results = []
|
|
314
|
-
|
|
315
|
-
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
316
|
-
futures = []
|
|
317
|
-
for i in range(0, len(all_files), batch_size):
|
|
318
|
-
batch_files = all_files[i:i + batch_size]
|
|
319
|
-
prompt = self._create_batch_analysis_prompt(batch_files, feature)
|
|
320
|
-
model = self._new_model()
|
|
321
|
-
model.set_suppress_output(True)
|
|
322
|
-
futures.append(executor.submit(self._call_model_with_retry, model, prompt))
|
|
323
|
-
|
|
324
|
-
for future in as_completed(futures):
|
|
325
|
-
success, response = future.result()
|
|
326
|
-
if not success:
|
|
327
|
-
continue
|
|
328
|
-
|
|
329
|
-
batch_start = futures.index(future) * batch_size
|
|
330
|
-
batch_end = min(batch_start + batch_size, len(all_files))
|
|
331
|
-
current_batch = all_files[batch_start:batch_end]
|
|
332
|
-
|
|
333
|
-
results = self._process_batch_response(response, current_batch)
|
|
334
|
-
batch_results.extend(results)
|
|
335
|
-
|
|
336
|
-
return batch_results
|
|
337
|
-
|
|
338
|
-
def _create_batch_analysis_prompt(self, batch_files: List[Tuple[str, str]], feature: str) -> str:
|
|
339
|
-
"""创建批量分析的提示词
|
|
340
|
-
|
|
341
|
-
Args:
|
|
342
|
-
batch_files: 批次文件列表
|
|
343
|
-
feature: 需求描述
|
|
344
|
-
|
|
345
|
-
Returns:
|
|
346
|
-
str: 提示词
|
|
347
|
-
"""
|
|
348
|
-
prompt = """你是资深程序员,请根据需求描述,从以下文件路径中选出最相关的文件,按相关度从高到低排序。
|
|
349
|
-
|
|
350
|
-
相关度打分标准(0-9分):
|
|
351
|
-
- 9分:文件名直接包含需求中的关键词,且文件功能与需求完全匹配
|
|
352
|
-
- 7-8分:文件名包含需求相关词,或文件功能与需求高度相关
|
|
353
|
-
- 5-6分:文件名暗示与需求有关,或文件功能与需求部分相关
|
|
354
|
-
- 3-4分:文件可能需要小幅修改以配合需求
|
|
355
|
-
- 1-2分:文件与需求关系较远,但可能需要少量改动
|
|
356
|
-
- 0分:文件与需求完全无关
|
|
357
|
-
|
|
358
|
-
请输出yaml格式,仅输出以下格式内容:
|
|
359
|
-
<RELEVANT_FILES_START>
|
|
360
|
-
file1.py: 9
|
|
361
|
-
file2.py: 7
|
|
362
|
-
<RELEVANT_FILES_END>
|
|
363
|
-
|
|
364
|
-
文件列表:
|
|
365
|
-
"""
|
|
366
|
-
for file_path, _ in batch_files:
|
|
367
|
-
prompt += f"- {file_path}\n"
|
|
368
|
-
prompt += f"\n需求描述: {feature}\n"
|
|
369
|
-
prompt += "\n注意:\n1. 只输出最相关的文件,不超过5个\n2. 根据上述打分标准判断相关性\n3. 相关度必须是0-9的整数"
|
|
370
|
-
|
|
371
|
-
return prompt
|
|
372
|
-
|
|
373
|
-
def _process_batch_response(self, response: str, batch_files: List[Tuple[str, str]]) -> List[Dict]:
|
|
374
|
-
"""处理批量分析的响应
|
|
375
|
-
|
|
376
|
-
Args:
|
|
377
|
-
response: 模型响应
|
|
378
|
-
batch_files: 批次文件列表
|
|
379
|
-
|
|
380
|
-
Returns:
|
|
381
|
-
List[Dict]: 处理后的文件列表
|
|
382
|
-
"""
|
|
383
|
-
try:
|
|
384
|
-
response = response.replace("<RELEVANT_FILES_START>", "").replace("<RELEVANT_FILES_END>", "")
|
|
385
|
-
result = yaml.safe_load(response)
|
|
386
|
-
|
|
387
|
-
batch_files_dict = {f[0]: f[1] for f in batch_files}
|
|
388
|
-
results = []
|
|
389
|
-
for file_path, score in result.items():
|
|
390
|
-
if isinstance(file_path, str) and isinstance(score, int):
|
|
391
|
-
score = max(0, min(9, score)) # Ensure score is between 0-9
|
|
392
|
-
if file_path in batch_files_dict:
|
|
393
|
-
results.append({
|
|
394
|
-
"file_path": file_path,
|
|
395
|
-
"file_description": batch_files_dict[file_path],
|
|
396
|
-
"score": score
|
|
397
|
-
})
|
|
398
|
-
return results
|
|
399
|
-
except Exception as e:
|
|
400
|
-
PrettyOutput.print(f"处理批次文件失败: {str(e)}", OutputType.ERROR)
|
|
401
|
-
return []
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def _process_content_response(self, response: str, top_files: List[Dict]) -> List[Dict]:
|
|
405
|
-
"""处理内容分析的响应"""
|
|
406
|
-
try:
|
|
407
|
-
response = response.replace("<FILE_RELATION_START>", "").replace("<FILE_RELATION_END>", "")
|
|
408
|
-
file_relation = yaml.safe_load(response)
|
|
409
|
-
if not file_relation:
|
|
410
|
-
return top_files[:5]
|
|
411
|
-
|
|
412
|
-
score = [[] for _ in range(10)] # 创建10个空列表,对应0-9分
|
|
413
|
-
for file_id, relation in file_relation.items():
|
|
414
|
-
id = int(file_id)
|
|
415
|
-
relation = max(0, min(9, relation)) # 确保范围在0-9之间
|
|
416
|
-
score[relation].append(top_files[id])
|
|
417
|
-
|
|
418
|
-
files = []
|
|
419
|
-
for scores in reversed(score): # 从高分到低分遍历
|
|
420
|
-
files.extend(scores)
|
|
421
|
-
if len(files) >= 5: # 直接取相关性最高的5个文件
|
|
422
|
-
break
|
|
423
|
-
|
|
424
|
-
return files[:5]
|
|
425
|
-
except Exception as e:
|
|
426
|
-
PrettyOutput.print(f"处理文件关系失败: {str(e)}", OutputType.ERROR)
|
|
427
|
-
return top_files[:5]
|
|
428
|
-
|
|
429
|
-
def _find_related_files(self, feature: str) -> List[Dict]:
|
|
430
|
-
"""根据需求描述,查找相关文件
|
|
431
|
-
|
|
432
|
-
Args:
|
|
433
|
-
feature: 需求描述
|
|
434
|
-
|
|
435
|
-
Returns:
|
|
436
|
-
List[Dict]: 相关文件列表
|
|
437
|
-
"""
|
|
438
|
-
# 1. 从数据库获取所有文件
|
|
439
|
-
all_files = self._get_files_from_db()
|
|
440
|
-
if not all_files:
|
|
441
|
-
return []
|
|
442
|
-
|
|
443
|
-
# 2. 批量分析文件相关性
|
|
444
|
-
batch_results = self._analyze_files_in_batches(all_files, feature)
|
|
445
|
-
|
|
446
|
-
# 3. 排序并获取前5个文件
|
|
447
|
-
batch_results.sort(key=lambda x: x["score"], reverse=True)
|
|
448
|
-
return batch_results[:5]
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
120
|
def _remake_patch(self, prompt: str) -> List[str]:
|
|
453
121
|
success, response = self._call_model_with_retry(self.main_model, prompt, max_retries=5) # 增加重试次数
|
|
454
122
|
if not success:
|
|
@@ -487,7 +155,7 @@ file2.py: 7
|
|
|
487
155
|
文件列表如下:
|
|
488
156
|
"""
|
|
489
157
|
for i, file in enumerate(related_files):
|
|
490
|
-
prompt += f"""{i}. {file["file_path"]}
|
|
158
|
+
prompt += f"""{i}. {file["file_path"]}\n"""
|
|
491
159
|
prompt += f"""文件内容:\n"""
|
|
492
160
|
prompt += f"<FILE_CONTENT_START>\n"
|
|
493
161
|
prompt += f'{file["file_content"]}\n'
|
|
@@ -499,6 +167,7 @@ file2.py: 7
|
|
|
499
167
|
1、仅输出补丁内容,不要输出任何其他内容,每个补丁必须用<PATCH_START>和<PATCH_END>标记
|
|
500
168
|
2、如果在大段代码中有零星修改,生成多个补丁
|
|
501
169
|
3、要替换的内容,一定要与文件内容完全一致,不要有任何多余或者缺失的内容
|
|
170
|
+
4、每个patch不超过20行,超出20行,请生成多个patch
|
|
502
171
|
"""
|
|
503
172
|
|
|
504
173
|
success, response = self._call_model_with_retry(self.main_model, prompt)
|
|
@@ -629,28 +298,23 @@ file2.py: 7
|
|
|
629
298
|
|
|
630
299
|
PrettyOutput.print(f"已保存修改记录: {record_path}", OutputType.SUCCESS)
|
|
631
300
|
|
|
632
|
-
def _find_git_root_dir(self, root_dir: str) -> str:
|
|
633
|
-
"""查找git根目录"""
|
|
634
|
-
while not os.path.exists(os.path.join(root_dir, ".git")):
|
|
635
|
-
root_dir = os.path.dirname(root_dir)
|
|
636
|
-
if root_dir == "/":
|
|
637
|
-
return None
|
|
638
|
-
return root_dir
|
|
639
301
|
|
|
640
302
|
|
|
641
303
|
def _prepare_execution(self) -> None:
|
|
642
304
|
"""准备执行环境"""
|
|
643
305
|
self.main_model = self._new_model()
|
|
644
|
-
self.
|
|
306
|
+
self._codebase.generate_codebase()
|
|
645
307
|
|
|
646
308
|
def _load_related_files(self, feature: str) -> List[Dict]:
|
|
647
309
|
"""加载相关文件内容"""
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
310
|
+
ret = []
|
|
311
|
+
related_files = self._codebase.search_similar(feature, top_k=5)
|
|
312
|
+
for file, score, _ in related_files:
|
|
313
|
+
PrettyOutput.print(f"相关文件: {file} 相关度: {score:.3f}", OutputType.INFO)
|
|
314
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
315
|
+
content = f.read()
|
|
316
|
+
ret.append({"file_path": file, "file_content": content})
|
|
317
|
+
return ret
|
|
654
318
|
|
|
655
319
|
def _handle_patch_application(self, related_files: List[Dict], patches: List[str], feature: str) -> Dict[str, Any]:
|
|
656
320
|
"""处理补丁应用流程"""
|
|
@@ -703,7 +367,6 @@ file2.py: 7
|
|
|
703
367
|
os.system(f"git add .")
|
|
704
368
|
os.system(f"git commit -m '{feature}'")
|
|
705
369
|
self._save_edit_record(feature, patches)
|
|
706
|
-
self._index_project()
|
|
707
370
|
|
|
708
371
|
def _revert_changes(self) -> None:
|
|
709
372
|
"""回退所有修改"""
|
|
@@ -780,7 +443,7 @@ def main():
|
|
|
780
443
|
if result["stderr"]:
|
|
781
444
|
PrettyOutput.print(result["stderr"], OutputType.ERROR)
|
|
782
445
|
if result["error"]:
|
|
783
|
-
PrettyOutput.print(f"错误类型: {type(result['error']).__name__}", OutputType.
|
|
446
|
+
PrettyOutput.print(f"错误类型: {type(result['error']).__name__}", OutputType.WARNING)
|
|
784
447
|
|
|
785
448
|
except KeyboardInterrupt:
|
|
786
449
|
print("\n用户中断执行")
|
jarvis/main.py
CHANGED
|
@@ -117,7 +117,6 @@ def main():
|
|
|
117
117
|
PlatformRegistry.get_global_platform_registry().set_global_platform_name(platform)
|
|
118
118
|
|
|
119
119
|
if args.model:
|
|
120
|
-
PrettyOutput.print(f"用户传入了模型参数,更换模型: {args.model}", OutputType.USER)
|
|
121
120
|
os.environ["JARVIS_MODEL"] = args.model
|
|
122
121
|
|
|
123
122
|
try:
|
|
@@ -126,7 +125,6 @@ def main():
|
|
|
126
125
|
|
|
127
126
|
# 如果用户传入了模型参数,则更换当前模型为用户指定的模型
|
|
128
127
|
if args.model:
|
|
129
|
-
PrettyOutput.print(f"用户传入了模型参数,更换模型: {args.model}", OutputType.USER)
|
|
130
128
|
agent.model.set_model_name(args.model)
|
|
131
129
|
|
|
132
130
|
# 欢迎信息
|
jarvis/models/ai8.py
CHANGED
|
@@ -64,11 +64,10 @@ class AI8Model(BasePlatform):
|
|
|
64
64
|
|
|
65
65
|
PrettyOutput.print("使用AI8_MODEL环境变量配置模型", OutputType.SUCCESS)
|
|
66
66
|
|
|
67
|
-
self.model_name = os.getenv("
|
|
67
|
+
self.model_name = os.getenv("JARVIS_MODEL") or "deepseek-chat"
|
|
68
68
|
if self.model_name not in self.models:
|
|
69
69
|
PrettyOutput.print(f"警告: 当前选择的模型 {self.model_name} 不在可用列表中", OutputType.WARNING)
|
|
70
70
|
|
|
71
|
-
PrettyOutput.print(f"当前使用模型: {self.model_name}", OutputType.SYSTEM)
|
|
72
71
|
|
|
73
72
|
def set_model_name(self, model_name: str):
|
|
74
73
|
"""设置模型名称"""
|
jarvis/models/openai.py
CHANGED
|
@@ -33,7 +33,6 @@ class OpenAIModel(BasePlatform):
|
|
|
33
33
|
self.base_url = os.getenv("OPENAI_API_BASE", "https://api.deepseek.com")
|
|
34
34
|
self.model_name = os.getenv("OPENAI_MODEL_NAME") or os.getenv("JARVIS_MODEL") or "deepseek-chat"
|
|
35
35
|
|
|
36
|
-
PrettyOutput.print(f"当前使用模型: {self.model_name}", OutputType.SYSTEM)
|
|
37
36
|
|
|
38
37
|
self.client = OpenAI(
|
|
39
38
|
api_key=self.api_key,
|
jarvis/models/oyi.py
CHANGED
|
@@ -25,8 +25,6 @@ class OyiModel(BasePlatform):
|
|
|
25
25
|
else:
|
|
26
26
|
PrettyOutput.print("获取模型列表失败", OutputType.WARNING)
|
|
27
27
|
|
|
28
|
-
PrettyOutput.print("使用OYI_MODEL环境变量配置模型", OutputType.SUCCESS)
|
|
29
|
-
|
|
30
28
|
self.messages = []
|
|
31
29
|
self.system_message = ""
|
|
32
30
|
self.conversation = None
|
|
@@ -37,11 +35,10 @@ class OyiModel(BasePlatform):
|
|
|
37
35
|
if not self.token:
|
|
38
36
|
raise Exception("OYI_API_KEY is not set")
|
|
39
37
|
|
|
40
|
-
self.model_name = os.getenv("
|
|
38
|
+
self.model_name = os.getenv("JARVIS_MODEL") or "deepseek-chat"
|
|
41
39
|
if self.model_name not in [m.split()[0] for m in available_models]:
|
|
42
40
|
PrettyOutput.print(f"警告: 当前选择的模型 {self.model_name} 不在可用列表中", OutputType.WARNING)
|
|
43
41
|
|
|
44
|
-
PrettyOutput.print(f"当前使用模型: {self.model_name}", OutputType.SYSTEM)
|
|
45
42
|
|
|
46
43
|
def set_model_name(self, model_name: str):
|
|
47
44
|
"""设置模型名称"""
|
jarvis/tools/__init__.py
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
from jarvis.jarvis_codebase.main import CodeBase
|
|
4
|
+
from jarvis.utils import find_git_root, PrettyOutput, OutputType
|
|
5
|
+
|
|
6
|
+
class CodebaseQATool:
|
|
7
|
+
"""代码库问答工具,用于回答关于代码库的问题"""
|
|
8
|
+
|
|
9
|
+
name = "codebase_qa"
|
|
10
|
+
description = "回答关于代码库的问题,可以查询和理解代码的功能、结构和实现细节"
|
|
11
|
+
parameters = {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"dir": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "项目根目录"
|
|
17
|
+
},
|
|
18
|
+
"question": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "关于代码库的问题"
|
|
21
|
+
},
|
|
22
|
+
"top_k": {
|
|
23
|
+
"type": "integer",
|
|
24
|
+
"description": "搜索相关文件的数量",
|
|
25
|
+
"default": 5
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"required": ["question"]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
32
|
+
"""执行代码问答"""
|
|
33
|
+
try:
|
|
34
|
+
dir = params.get("dir")
|
|
35
|
+
question = params["question"]
|
|
36
|
+
top_k = params.get("top_k", 5)
|
|
37
|
+
|
|
38
|
+
# 初始化代码库
|
|
39
|
+
current_dir = os.getcwd()
|
|
40
|
+
root_dir = find_git_root(dir or current_dir)
|
|
41
|
+
if not root_dir:
|
|
42
|
+
return {
|
|
43
|
+
"success": False,
|
|
44
|
+
"stdout": "",
|
|
45
|
+
"stderr": "错误:当前目录不在Git仓库中",
|
|
46
|
+
"error": "NotInGitRepository"
|
|
47
|
+
}
|
|
48
|
+
os.chdir(root_dir)
|
|
49
|
+
codebase = CodeBase(root_dir)
|
|
50
|
+
# 执行问答
|
|
51
|
+
response = codebase.ask_codebase(question, top_k)
|
|
52
|
+
os.chdir(current_dir)
|
|
53
|
+
return {
|
|
54
|
+
"success": True,
|
|
55
|
+
"stdout": response,
|
|
56
|
+
"stderr": "",
|
|
57
|
+
"error": None
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
PrettyOutput.print(f"代码问答出错: {str(e)}", output_type=OutputType.ERROR)
|
|
62
|
+
return {
|
|
63
|
+
"success": False,
|
|
64
|
+
"stdout": "",
|
|
65
|
+
"stderr": f"执行代码问答时发生错误: {str(e)}",
|
|
66
|
+
"error": str(type(e).__name__)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def register():
|
|
70
|
+
return CodebaseQATool()
|
jarvis/utils.py
CHANGED
|
@@ -200,3 +200,10 @@ def while_true(func, sleep_time: float = 0.1):
|
|
|
200
200
|
PrettyOutput.print(f"执行失败,{sleep_time}s后重试...", OutputType.WARNING)
|
|
201
201
|
time.sleep(sleep_time)
|
|
202
202
|
return ret
|
|
203
|
+
|
|
204
|
+
def find_git_root(dir="."):
|
|
205
|
+
curr_dir = os.getcwd()
|
|
206
|
+
os.chdir(dir)
|
|
207
|
+
ret = os.popen("git rev-parse --show-toplevel").read().strip()
|
|
208
|
+
os.chdir(curr_dir)
|
|
209
|
+
return ret
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: jarvis-ai-assistant
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.66
|
|
4
4
|
Summary: Jarvis: An AI assistant that uses tools to interact with the system
|
|
5
5
|
Home-page: https://github.com/skyfireitdiy/Jarvis
|
|
6
6
|
Author: skyfire
|
|
@@ -44,6 +44,9 @@ Requires-Dist: colorama>=0.4.6
|
|
|
44
44
|
Requires-Dist: prompt_toolkit>=3.0.0
|
|
45
45
|
Requires-Dist: openai>=1.20.0
|
|
46
46
|
Requires-Dist: playwright>=1.41.1
|
|
47
|
+
Requires-Dist: numpy>=1.26.0
|
|
48
|
+
Requires-Dist: faiss-cpu>=1.8.1
|
|
49
|
+
Requires-Dist: sentence-transformers>=2.2.2
|
|
47
50
|
Provides-Extra: dev
|
|
48
51
|
Requires-Dist: pytest; extra == "dev"
|
|
49
52
|
Requires-Dist: black; extra == "dev"
|
|
@@ -120,8 +123,12 @@ Jarvis supports configuration through environment variables that can be set in t
|
|
|
120
123
|
|---------|------|--------|------|
|
|
121
124
|
| JARVIS_PLATFORM | AI platform to use, supports kimi/openai/ai8 etc | kimi | Yes |
|
|
122
125
|
| JARVIS_MODEL | Model name to use | - | No |
|
|
126
|
+
|
|
123
127
|
| JARVIS_CODEGEN_PLATFORM | AI platform for code generation | Same as JARVIS_PLATFORM | No |
|
|
124
128
|
| JARVIS_CODEGEN_MODEL | Model name for code generation | Same as JARVIS_MODEL | No |
|
|
129
|
+
| JARVIS_CHEAP_PLATFORM | AI platform for cheap operations | Same as JARVIS_PLATFORM | No |
|
|
130
|
+
| JARVIS_CHEAP_MODEL | Model name for cheap operations | Same as JARVIS_MODEL | No |
|
|
131
|
+
| JARVIS_EMBEDDING_MODEL | Embedding model for code analysis | BAAI/bge-large-zh-v1.5 | No |
|
|
125
132
|
| OPENAI_API_KEY | API key for OpenAI platform | - | Required for OpenAI |
|
|
126
133
|
| OPENAI_API_BASE | Base URL for OpenAI API | https://api.deepseek.com | No |
|
|
127
134
|
| OPENAI_MODEL_NAME | Model name for OpenAI | deepseek-chat | No |
|
|
@@ -139,15 +146,26 @@ Jarvis supports configuration through environment variables that can be set in t
|
|
|
139
146
|
jarvis
|
|
140
147
|
```
|
|
141
148
|
|
|
149
|
+
|
|
142
150
|
### With Specific Model
|
|
143
151
|
```bash
|
|
144
152
|
jarvis -p kimi # Use Kimi platform
|
|
145
153
|
jarvis -p openai # Use OpenAI platform
|
|
146
154
|
```
|
|
147
155
|
|
|
148
|
-
###
|
|
156
|
+
### Code Modification
|
|
157
|
+
```bash
|
|
158
|
+
jarvis coder --feature "Add new feature" # Modify code to add new feature
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Codebase Search
|
|
149
162
|
```bash
|
|
150
|
-
jarvis
|
|
163
|
+
jarvis codebase --search "database connection" # Search codebase
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Codebase Question
|
|
167
|
+
```bash
|
|
168
|
+
jarvis codebase --ask "How to use the database?" # Ask about codebase
|
|
151
169
|
```
|
|
152
170
|
|
|
153
171
|
### Keep Chat History
|
|
@@ -157,6 +175,7 @@ jarvis --keep-history # Don't delete chat session after completion
|
|
|
157
175
|
|
|
158
176
|
## 🛠️ Tools
|
|
159
177
|
|
|
178
|
+
|
|
160
179
|
### Built-in Tools
|
|
161
180
|
|
|
162
181
|
| Tool | Description |
|
|
@@ -166,27 +185,43 @@ jarvis --keep-history # Don't delete chat session after completion
|
|
|
166
185
|
| generate_tool | AI-powered tool generation and integration |
|
|
167
186
|
| methodology | Experience accumulation and methodology management |
|
|
168
187
|
| create_sub_agent | Create specialized sub-agents for specific tasks |
|
|
188
|
+
| coder | Automatic code modification and generation tool |
|
|
189
|
+
| codebase | Codebase management and search tool |
|
|
169
190
|
|
|
170
191
|
### Tool Locations
|
|
171
192
|
- Built-in tools: `src/jarvis/tools/`
|
|
172
193
|
- User tools: `~/.jarvis_tools/`
|
|
173
194
|
|
|
195
|
+
|
|
174
196
|
### Key Features
|
|
175
197
|
|
|
176
198
|
#### 1. Self-Extending Capabilities
|
|
177
199
|
- Tool generation through natural language description
|
|
178
200
|
- Automatic code generation and integration
|
|
179
201
|
- Dynamic capability expansion through sub-agents
|
|
202
|
+
- Automatic code modification with version control
|
|
203
|
+
- Codebase indexing and semantic search
|
|
180
204
|
|
|
181
205
|
#### 2. Methodology Learning
|
|
182
206
|
- Automatic experience accumulation from interactions
|
|
183
207
|
- Pattern recognition and methodology extraction
|
|
184
208
|
- Continuous refinement through usage
|
|
209
|
+
- Code modification history tracking
|
|
210
|
+
- Codebase analysis and documentation generation
|
|
185
211
|
|
|
186
212
|
#### 3. Adaptive Problem Solving
|
|
187
213
|
- Context-aware sub-agent creation
|
|
188
214
|
- Dynamic tool composition
|
|
189
215
|
- Learning from execution feedback
|
|
216
|
+
- Codebase-aware problem solving
|
|
217
|
+
- Multi-model collaboration for complex tasks
|
|
218
|
+
|
|
219
|
+
#### 4. Code Intelligence
|
|
220
|
+
- Automatic codebase indexing
|
|
221
|
+
- Semantic code search
|
|
222
|
+
- Code modification with git integration
|
|
223
|
+
- Code analysis and documentation
|
|
224
|
+
- Multi-model code generation
|
|
190
225
|
|
|
191
226
|
## 🎯 Extending Jarvis
|
|
192
227
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
jarvis/__init__.py,sha256=XwQusgz9_jvC5jEMwbV21qnY-dFIO3yH5W9FEENNxsU,50
|
|
2
|
+
jarvis/agent.py,sha256=kl6pwNrluzb-9eZKgwmsk5Jh4CpWi4F8B3RvEQNvc5U,14921
|
|
3
|
+
jarvis/main.py,sha256=7EcSlxa5JFFXBujzKDWdNtwX6axLhFFdJMc2GxTjfdk,6295
|
|
4
|
+
jarvis/utils.py,sha256=bjC0PAR58RvcXHgabIFmNmYL1L_GhhiEwMFytWurcN4,7499
|
|
5
|
+
jarvis/jarvis_codebase/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
jarvis/jarvis_codebase/main.py,sha256=bncfOXKFthcr21cV9tONXzcchSWaaYXmo0o11R5hPzc,14799
|
|
7
|
+
jarvis/jarvis_coder/main.py,sha256=mK68MJyOerVnY7Fr9ibQ1swQLVIWyBAFmmVj08SHKyk,21825
|
|
8
|
+
jarvis/models/__init__.py,sha256=mrOt67nselz_H1gX9wdAO4y2DY5WPXzABqJbr5Des8k,63
|
|
9
|
+
jarvis/models/ai8.py,sha256=vgy-r_3HHxGMAalZrA65VWHC1PuwBTYgtprSgHkCbrk,12557
|
|
10
|
+
jarvis/models/base.py,sha256=ShV1H8Unee4RMaiFO4idROQA0Hc6wu4dyeRPX5fcszk,1433
|
|
11
|
+
jarvis/models/kimi.py,sha256=1iTB0Z_WOmCML3Ufsge6jmeKOYvccr7I5lS3JUXymU4,17611
|
|
12
|
+
jarvis/models/openai.py,sha256=ayaBWAN5VexMcKVrjEPDNB-Q9wx0sCV9Z4BCrvwYJ9w,4315
|
|
13
|
+
jarvis/models/oyi.py,sha256=X2c5SWDIuQDCCFBcEKbzIWEz3I34eOAi0d1XAFgxlpw,15001
|
|
14
|
+
jarvis/models/registry.py,sha256=hJyaROiOF_TkbtIXsjOD8-ArOvAvtxviawyqBFfLV6s,7617
|
|
15
|
+
jarvis/tools/__init__.py,sha256=xmROdzJTZz6JDLLuAbwVLjUD4xfUUYb6D1Ssu_desaE,183
|
|
16
|
+
jarvis/tools/base.py,sha256=EGRGbdfbLXDLwtyoWdvp9rlxNX7bzc20t0Vc2VkwIEY,652
|
|
17
|
+
jarvis/tools/codebase_qa.py,sha256=AEpusYxyWtALVVwPk1DMUH9cVI73mE1e3WFHJXDpXto,2333
|
|
18
|
+
jarvis/tools/coder.py,sha256=ZJfPInKms4Hj3-eQlBwamVsvZ-2nlZ-4jsqJ-tJc6mg,2040
|
|
19
|
+
jarvis/tools/file_ops.py,sha256=h8g0eT9UvlJf4kt0DLXvdSsjcPj7x19lxWdDApeDfpg,3842
|
|
20
|
+
jarvis/tools/generator.py,sha256=vVP3eN5cCDpRXf_fn0skETkPXAW1XZFWx9pt2_ahK48,5999
|
|
21
|
+
jarvis/tools/methodology.py,sha256=UG6s5VYRcd9wrKX4cg6f7zJhet5AIcthFGMOAdevBiw,5175
|
|
22
|
+
jarvis/tools/registry.py,sha256=mlOAmUq3yzRz-7yvwrrCwbe5Lmw8eh1v8-_Fa5sezwI,7209
|
|
23
|
+
jarvis/tools/search.py,sha256=1EqOVvLhg2Csh-i03-XeCrusbyfmH69FZ8khwZt8Tow,6131
|
|
24
|
+
jarvis/tools/shell.py,sha256=UPKshPyOaUwTngresUw-ot1jHjQIb4wCY5nkJqa38lU,2520
|
|
25
|
+
jarvis/tools/sub_agent.py,sha256=rEtAmSVY2ZjFOZEKr5m5wpACOQIiM9Zr_3dT92FhXYU,2621
|
|
26
|
+
jarvis/tools/webpage.py,sha256=d3w3Jcjcu1ESciezTkz3n3Zf-rp_l91PrVoDEZnckOo,2391
|
|
27
|
+
jarvis_ai_assistant-0.1.66.dist-info/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
|
|
28
|
+
jarvis_ai_assistant-0.1.66.dist-info/METADATA,sha256=O3UdZLfScFyhR3ARPYaKu1d5hemg45HFN6bZKclTfYk,12374
|
|
29
|
+
jarvis_ai_assistant-0.1.66.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
30
|
+
jarvis_ai_assistant-0.1.66.dist-info/entry_points.txt,sha256=QNUeqmUJd7nHufel2FO7cRttS1uKFfnbIyObv8eVyOY,140
|
|
31
|
+
jarvis_ai_assistant-0.1.66.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
|
|
32
|
+
jarvis_ai_assistant-0.1.66.dist-info/RECORD,,
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
jarvis/__init__.py,sha256=wMtxQxTakpumAd-K51g16_sMIW8so_8Pv15IEYnhfwI,50
|
|
2
|
-
jarvis/agent.py,sha256=kl6pwNrluzb-9eZKgwmsk5Jh4CpWi4F8B3RvEQNvc5U,14921
|
|
3
|
-
jarvis/main.py,sha256=gXXtnrkkvGwEswJL6qiYjVrg3bpzye-GJeAe0Nf2B9o,6509
|
|
4
|
-
jarvis/utils.py,sha256=JlkuC9RtspXH2VWDmj9nR0vnb8ie1gIsKc4vC7WRco8,7321
|
|
5
|
-
jarvis/jarvis_coder/main.py,sha256=TosDDiaYSjDpzKPNKcygxZ3XXWbcnvBmIIMn3UMBc60,35102
|
|
6
|
-
jarvis/models/__init__.py,sha256=mrOt67nselz_H1gX9wdAO4y2DY5WPXzABqJbr5Des8k,63
|
|
7
|
-
jarvis/models/ai8.py,sha256=AMBZRS9hKW1Ts_YoHMMelhT0CRMeMzHtVH31z0FpP-Q,12671
|
|
8
|
-
jarvis/models/base.py,sha256=ShV1H8Unee4RMaiFO4idROQA0Hc6wu4dyeRPX5fcszk,1433
|
|
9
|
-
jarvis/models/kimi.py,sha256=1iTB0Z_WOmCML3Ufsge6jmeKOYvccr7I5lS3JUXymU4,17611
|
|
10
|
-
jarvis/models/openai.py,sha256=7oOxrL6EM7iaqJgZsaFvVmyzY0ars4BP3EKAUlX1RPw,4403
|
|
11
|
-
jarvis/models/oyi.py,sha256=mW-uhUZbts2L_oI8WueZUTEmrLY1CiBHn4UV2HP7ZCE,15214
|
|
12
|
-
jarvis/models/registry.py,sha256=hJyaROiOF_TkbtIXsjOD8-ArOvAvtxviawyqBFfLV6s,7617
|
|
13
|
-
jarvis/tools/__init__.py,sha256=Kj1bKj34lwRDKMKHLOrLyQElf2lHbqA2tDgP359eaDo,71
|
|
14
|
-
jarvis/tools/base.py,sha256=EGRGbdfbLXDLwtyoWdvp9rlxNX7bzc20t0Vc2VkwIEY,652
|
|
15
|
-
jarvis/tools/coder.py,sha256=ZJfPInKms4Hj3-eQlBwamVsvZ-2nlZ-4jsqJ-tJc6mg,2040
|
|
16
|
-
jarvis/tools/file_ops.py,sha256=h8g0eT9UvlJf4kt0DLXvdSsjcPj7x19lxWdDApeDfpg,3842
|
|
17
|
-
jarvis/tools/generator.py,sha256=vVP3eN5cCDpRXf_fn0skETkPXAW1XZFWx9pt2_ahK48,5999
|
|
18
|
-
jarvis/tools/methodology.py,sha256=UG6s5VYRcd9wrKX4cg6f7zJhet5AIcthFGMOAdevBiw,5175
|
|
19
|
-
jarvis/tools/registry.py,sha256=mlOAmUq3yzRz-7yvwrrCwbe5Lmw8eh1v8-_Fa5sezwI,7209
|
|
20
|
-
jarvis/tools/search.py,sha256=1EqOVvLhg2Csh-i03-XeCrusbyfmH69FZ8khwZt8Tow,6131
|
|
21
|
-
jarvis/tools/shell.py,sha256=UPKshPyOaUwTngresUw-ot1jHjQIb4wCY5nkJqa38lU,2520
|
|
22
|
-
jarvis/tools/sub_agent.py,sha256=rEtAmSVY2ZjFOZEKr5m5wpACOQIiM9Zr_3dT92FhXYU,2621
|
|
23
|
-
jarvis/tools/webpage.py,sha256=d3w3Jcjcu1ESciezTkz3n3Zf-rp_l91PrVoDEZnckOo,2391
|
|
24
|
-
jarvis_ai_assistant-0.1.64.dist-info/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
|
|
25
|
-
jarvis_ai_assistant-0.1.64.dist-info/METADATA,sha256=akHo9a0miOBYWvtITaAuo1G3PZxiE2yPSZxuh0QE1C4,11213
|
|
26
|
-
jarvis_ai_assistant-0.1.64.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
27
|
-
jarvis_ai_assistant-0.1.64.dist-info/entry_points.txt,sha256=ieRI4ilnGNx1R6LlzT2P510mJ27lhLesVZToezDjSd8,89
|
|
28
|
-
jarvis_ai_assistant-0.1.64.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
|
|
29
|
-
jarvis_ai_assistant-0.1.64.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|