jarvis-ai-assistant 0.1.183__py3-none-any.whl → 0.1.185__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.
jarvis/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """Jarvis AI Assistant"""
3
3
 
4
- __version__ = "0.1.183"
4
+ __version__ = "0.1.185"
@@ -4,6 +4,8 @@ import datetime
4
4
  import platform
5
5
  from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union
6
6
 
7
+ from jarvis.jarvis_platform.base import BasePlatform
8
+
7
9
  # 第三方库导入
8
10
  from yaspin import yaspin # type: ignore
9
11
 
@@ -166,7 +168,9 @@ class Agent:
166
168
  multiline_inputer: Optional[Callable[[str], str]] = None,
167
169
  use_methodology: Optional[bool] = None,
168
170
  use_analysis: Optional[bool] = None,
171
+ files: List[str] = [],
169
172
  ):
173
+ self.files = files
170
174
  """初始化Jarvis Agent实例
171
175
 
172
176
  参数:
@@ -218,8 +222,9 @@ class Agent:
218
222
  multiline_inputer if multiline_inputer else get_multiline_input
219
223
  )
220
224
 
225
+ # 如果有上传文件,自动禁用方法论
221
226
  self.use_methodology = (
222
- use_methodology if use_methodology is not None else is_use_methodology()
227
+ False if files else (use_methodology if use_methodology is not None else is_use_methodology())
223
228
  )
224
229
  self.use_analysis = (
225
230
  use_analysis if use_analysis is not None else is_use_analysis()
@@ -766,27 +771,31 @@ arguments:
766
771
  try:
767
772
  set_agent(self.name, self)
768
773
 
774
+ for handler in self.input_handler:
775
+ user_input, _ = handler(user_input, self)
776
+
769
777
  self.prompt = f"{user_input}"
770
778
 
771
- if self.first and self.use_methodology:
772
- # 先尝试上传方法轮
773
- platform = self.model if hasattr(self.model, "upload_files") else None
774
- if platform and upload_methodology(platform):
775
- self.prompt = f"{user_input}\n\n方法论已上传到平台,请参考平台上的方法论内容"
776
- else:
777
- msg = user_input
778
- for handler in self.input_handler:
779
- msg, _ = handler(msg, self)
780
- # 上传失败则回退到本地加载
781
- self.prompt = f"{user_input}\n\n以下是历史类似问题的执行经验,可参考:\n{load_methodology(msg, self.get_tool_registry())}"
779
+ if self.first:
780
+ # 如果有上传文件,先上传文件
781
+ if self.files and isinstance(self.model, BasePlatform) and hasattr(self.model, "upload_files"):
782
+ self.model.upload_files(self.files)
783
+ self.prompt = f"{user_input}\n\n已上传{len(self.files)}个文件到平台"
784
+
785
+ # 如果启用方法论且没有上传文件,上传方法论
786
+ elif self.use_methodology:
787
+ platform = self.model if hasattr(self.model, "upload_files") else None
788
+ if platform and upload_methodology(platform):
789
+ self.prompt = f"{user_input}\n\n方法论已上传到平台,请参考平台上的方法论内容"
790
+ else:
791
+ # 上传失败则回退到本地加载
792
+ self.prompt = f"{user_input}\n\n以下是历史类似问题的执行经验,可参考:\n{load_methodology(user_input, self.get_tool_registry())}"
782
793
 
783
794
  self.first = False
784
795
 
785
796
  self.conversation_length = get_context_token_count(self.prompt)
786
797
  while True:
787
798
  try:
788
- # 如果对话历史长度超过限制,在提示中添加提醒
789
-
790
799
  current_response = self._call_model(self.prompt, True)
791
800
  self.prompt = ""
792
801
  self.conversation_length += get_context_token_count(
@@ -95,7 +95,7 @@ def file_input_handler(user_input: str, agent: Any) -> Tuple[str, bool]:
95
95
  if files:
96
96
  with yaspin(text="正在读取文件...", color="cyan") as spinner:
97
97
  old_prompt = prompt
98
- result = FileOperationTool().execute({"operation":"read","files": files})
98
+ result = FileOperationTool().execute({"operation":"read","files": files, "agent": agent})
99
99
  if result["success"]:
100
100
  spinner.text = "文件读取完成"
101
101
  spinner.ok("✅")
@@ -1,19 +1,25 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import json
3
3
  import os
4
- from typing import Any, Dict, Generator, List, Tuple
4
+ import time
5
5
  import uuid
6
+ from typing import Any, Dict, Generator, List, Tuple
6
7
 
7
8
  import requests
9
+ from yaspin import yaspin
10
+ from yaspin.spinners import Spinners
8
11
 
9
12
  from jarvis.jarvis_platform.base import BasePlatform
10
- from jarvis.jarvis_utils.output import PrettyOutput, OutputType
13
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
11
14
  from jarvis.jarvis_utils.utils import while_success
12
15
 
13
16
 
14
17
  class TongyiPlatform(BasePlatform):
15
18
  """Tongyi platform implementation"""
16
19
 
20
+ # Supported image formats
21
+ IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"}
22
+
17
23
  platform_name = "tongyi"
18
24
 
19
25
  def __init__(self):
@@ -25,7 +31,8 @@ class TongyiPlatform(BasePlatform):
25
31
  self.msg_id = ""
26
32
  self.model_name = ""
27
33
  self.uploaded_file_info = []
28
-
34
+ self.system_message = "" # System message for initialization
35
+ self.first_chat = True # Flag for first chat
29
36
 
30
37
  def _get_base_headers(self):
31
38
  return {
@@ -46,12 +53,12 @@ class TongyiPlatform(BasePlatform):
46
53
  "Referer": "https://www.tongyi.com/qianwen",
47
54
  "Accept-Encoding": "gzip, deflate, br, zstd",
48
55
  "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
49
- "Cookie": self.cookies
56
+ "Cookie": self.cookies,
50
57
  }
51
58
 
52
59
  def set_model_name(self, model_name: str):
53
60
  """Set model name
54
-
61
+
55
62
  Args:
56
63
  model_name: Model name to use
57
64
  """
@@ -67,37 +74,63 @@ class TongyiPlatform(BasePlatform):
67
74
  headers = self._get_base_headers()
68
75
 
69
76
  headers["accept"] = "text/event-stream"
70
-
77
+
71
78
  # Prepare contents array with message
72
- contents = [{
73
- "content": message,
74
- "contentType": "text",
75
- "role": "user",
76
- "ext": {
77
- "searchType": "",
78
- "pptGenerate": False,
79
- "deepThink": False,
80
- "deepResearch": False
79
+ contents = [
80
+ {
81
+ "content": message,
82
+ "contentType": "text",
83
+ "role": "user",
84
+ "ext": {
85
+ "searchType": "",
86
+ "pptGenerate": False,
87
+ "deepThink": False,
88
+ "deepResearch": False,
89
+ },
81
90
  }
82
- }]
91
+ ]
92
+
93
+ # Add system message if it's first chat
94
+ if self.first_chat and self.system_message:
95
+ contents.insert(
96
+ 0,
97
+ {
98
+ "content": self.system_message,
99
+ "contentType": "text",
100
+ "role": "system",
101
+ "ext": {
102
+ "searchType": "",
103
+ "pptGenerate": False,
104
+ "deepThink": False,
105
+ "deepResearch": False,
106
+ },
107
+ },
108
+ )
109
+ self.first_chat = False
83
110
 
84
111
  # Add uploaded files to contents if available and clear after use
85
112
  if self.uploaded_file_info:
86
113
  for file_info in self.uploaded_file_info:
87
- contents.append({
88
- "role": "user",
89
- "contentType": "file",
90
- "content": file_info["url"],
91
- "ext": {
92
- "fileSize": file_info.get("fileSize", 0),
93
- "batchId": file_info.get("batchId", ""),
94
- "docId": file_info.get("docId", "")
114
+ # Determine content type based on fileKey extension
115
+ file_ext = os.path.splitext(file_info["fileKey"])[1].lower()
116
+ is_image = file_ext in self.IMAGE_EXTENSIONS
117
+
118
+ contents.append(
119
+ {
120
+ "role": "user",
121
+ "contentType": "image" if is_image else "file",
122
+ "content": file_info["url"],
123
+ "ext": {
124
+ "fileSize": file_info.get("fileSize", 0),
125
+ "batchId": file_info.get("batchId", ""),
126
+ "docId": file_info.get("docId", ""),
127
+ },
95
128
  }
96
- })
129
+ )
97
130
  # Clear uploaded file info after using it
98
131
  self.uploaded_file_info = []
99
132
 
100
- payload = {
133
+ payload: Dict[str, Any] = {
101
134
  "model": "",
102
135
  "action": "next",
103
136
  "mode": "chat",
@@ -115,13 +148,18 @@ class TongyiPlatform(BasePlatform):
115
148
  "specifiedModel": "",
116
149
  "deepThink": True if self.model_name == "Thinking" else False,
117
150
  "deepResearch": False,
118
- "fileUploadBatchId": self.uploaded_file_info[0]["batchId"] if self.uploaded_file_info else ""
151
+ "fileUploadBatchId": self.uploaded_file_info[0]["batchId"]
152
+ if self.uploaded_file_info
153
+ else "",
119
154
  },
120
- "contents": contents
155
+ "contents": contents,
121
156
  }
122
157
 
123
158
  try:
124
- response = while_success(lambda: requests.post(url, headers=headers, json=payload, stream=True), sleep_time=5)
159
+ response = while_success(
160
+ lambda: requests.post(url, headers=headers, json=payload, stream=True),
161
+ sleep_time=5,
162
+ )
125
163
  if response.status_code != 200:
126
164
  raise Exception(f"HTTP {response.status_code}: {response.text}")
127
165
  msg_id = ""
@@ -132,7 +170,7 @@ class TongyiPlatform(BasePlatform):
132
170
  for line in response.iter_lines():
133
171
  if not line:
134
172
  continue
135
- line_str = line.decode('utf-8')
173
+ line_str = line.decode("utf-8")
136
174
  if not line_str.startswith("data: "):
137
175
  continue
138
176
 
@@ -143,7 +181,7 @@ class TongyiPlatform(BasePlatform):
143
181
  msg_id = data["msgId"]
144
182
  if "sessionId" in data:
145
183
  session_id = data["sessionId"]
146
-
184
+
147
185
  if "contents" in data and len(data["contents"]) > 0:
148
186
  for content in data["contents"]:
149
187
  if content.get("contentType") == "think":
@@ -151,13 +189,17 @@ class TongyiPlatform(BasePlatform):
151
189
  yield "<think>\n\n"
152
190
  in_thinking = True
153
191
  if content.get("incremental"):
154
- tmp_content = json.loads(content.get("content"))["content"]
192
+ tmp_content = json.loads(content.get("content"))[
193
+ "content"
194
+ ]
155
195
  thinking_content += tmp_content
156
196
  yield tmp_content
157
197
  else:
158
- tmp_content = json.loads(content.get("content"))["content"]
198
+ tmp_content = json.loads(content.get("content"))[
199
+ "content"
200
+ ]
159
201
  if len(thinking_content) < len(tmp_content):
160
- yield tmp_content[len(thinking_content):]
202
+ yield tmp_content[len(thinking_content) :]
161
203
  thinking_content = tmp_content
162
204
  else:
163
205
  # thinking_content = "aaa</thi"
@@ -171,7 +213,9 @@ class TongyiPlatform(BasePlatform):
171
213
  # print("--------------------------------")
172
214
  # print(tmp_content)
173
215
  # print("--------------------------------")
174
- yield "\r\n</think>\n"[len(thinking_content)-len(tmp_content):]
216
+ yield "\r\n</think>\n"[
217
+ len(thinking_content) - len(tmp_content) :
218
+ ]
175
219
  thinking_content = tmp_content
176
220
  in_thinking = False
177
221
  elif content.get("contentType") == "text":
@@ -184,10 +228,9 @@ class TongyiPlatform(BasePlatform):
184
228
  else:
185
229
  tmp_content = content.get("content")
186
230
  if len(text_content) < len(tmp_content):
187
- yield tmp_content[len(text_content):]
231
+ yield tmp_content[len(text_content) :]
188
232
  text_content = tmp_content
189
233
 
190
-
191
234
  except json.JSONDecodeError:
192
235
  continue
193
236
 
@@ -198,10 +241,10 @@ class TongyiPlatform(BasePlatform):
198
241
 
199
242
  except Exception as e:
200
243
  raise Exception(f"Chat failed: {str(e)}")
201
-
244
+
202
245
  def _get_upload_token(self) -> Dict[str, Any]:
203
246
  """Get upload token from Tongyi API
204
-
247
+
205
248
  Returns:
206
249
  Dict[str, Any]: Upload token information including accessId, bucketName, etc.
207
250
  """
@@ -210,161 +253,205 @@ class TongyiPlatform(BasePlatform):
210
253
  payload = {}
211
254
 
212
255
  try:
213
- response = while_success(lambda: requests.post(url, headers=headers, json=payload), sleep_time=5)
256
+ response = while_success(
257
+ lambda: requests.post(url, headers=headers, json=payload), sleep_time=5
258
+ )
214
259
  if response.status_code != 200:
215
260
  raise Exception(f"HTTP {response.status_code}: {response.text}")
216
-
261
+
217
262
  result = response.json()
218
263
  if not result.get("success"):
219
264
  raise Exception(f"Failed to get upload token: {result.get('errorMsg')}")
220
-
265
+
221
266
  return result.get("data", {})
222
-
267
+
223
268
  except Exception as e:
224
269
  raise Exception(f"Failed to get upload token: {str(e)}")
225
-
226
270
 
227
271
  def upload_files(self, file_list: List[str]) -> bool:
228
272
  """Upload files to Tongyi platform and get download links
229
-
273
+
230
274
  Args:
231
275
  file_list: List of file paths to upload
232
-
276
+
233
277
  Returns:
234
278
  List[Dict[str, str]]: List of dictionaries containing file info and download URLs
235
279
  """
236
280
  try:
237
281
  upload_token = self._get_upload_token()
238
282
  uploaded_files = []
239
-
283
+
240
284
  for file_path in file_list:
241
- if not os.path.exists(file_path):
242
- PrettyOutput.print(f"File not found: {file_path}", OutputType.ERROR)
243
- return False
244
-
245
- # Get file name and content type
246
285
  file_name = os.path.basename(file_path)
247
- content_type = self._get_content_type(file_path)
248
-
249
- # Prepare form data
250
- form_data = {
251
- 'OSSAccessKeyId': upload_token['accessId'],
252
- 'policy': upload_token['policy'],
253
- 'signature': upload_token['signature'],
254
- 'key': f"{upload_token['dir']}{file_name}",
255
- 'dir': upload_token['dir'],
256
- 'success_action_status': '200'
257
- }
258
-
259
- # Prepare files
260
- files = {
261
- 'file': (file_name, open(file_path, 'rb'), content_type)
262
- }
263
-
264
- # Upload file
265
- response = requests.post(
266
- upload_token['host'],
267
- data=form_data,
268
- files=files
269
- )
270
-
271
- if response.status_code != 200:
272
- PrettyOutput.print(f"Failed to upload {file_name}: HTTP {response.status_code}", OutputType.ERROR)
273
- return False
274
-
275
- uploaded_files.append({
276
- 'fileKey': file_name,
277
- 'fileType': 'file',
278
- 'dir': upload_token['dir']
279
- })
280
-
281
- # Get download links for uploaded files
282
- url = "https://api.tongyi.com/dialog/downloadLink/batch"
283
- headers = self._get_base_headers()
284
- payload = {
285
- "fileKeys": [f['fileKey'] for f in uploaded_files],
286
- "fileType": "file",
287
- "dir": upload_token['dir']
288
- }
289
-
290
- response = requests.post(url, headers=headers, json=payload)
291
- if response.status_code != 200:
292
- PrettyOutput.print(f"Failed to get download links: HTTP {response.status_code}", OutputType.ERROR)
293
- return False
294
-
295
- result = response.json()
296
- if not result.get("success"):
297
- PrettyOutput.print(f"Failed to get download links: {result.get('errorMsg')}", OutputType.ERROR)
298
- return False
299
-
300
- # Add files to chat
301
- self.uploaded_file_info = result.get("data", {}).get("results", [])
302
- for file_info in self.uploaded_file_info:
303
- add_url = "https://api.tongyi.com/assistant/api/chat/file/add"
304
- add_payload = {
305
- "workSource": "chat",
306
- "terminal": "web",
307
- "workCode": "0",
308
- "channel": "home",
309
- "workType": "file",
310
- "module": "uploadhistory",
311
- "workName": file_info["fileKey"],
312
- "workId": file_info["docId"],
313
- "workResourcePath": file_info["url"],
314
- "sessionId": "",
315
- "batchId": str(uuid.uuid4()).replace('-', '')[:32], # Generate random batchId
316
- "fileSize": os.path.getsize(file_path)
317
- }
318
-
319
- add_response = requests.post(add_url, headers=headers, json=add_payload)
320
- if add_response.status_code != 200:
321
- PrettyOutput.print(f"Failed to add file to chat: HTTP {add_response.status_code}", OutputType.ERROR)
322
- continue
323
-
324
- add_result = add_response.json()
325
- if not add_result.get("success"):
326
- PrettyOutput.print(f"Failed to add file to chat: {add_result.get('errorMsg')}", OutputType.ERROR)
327
- continue
328
-
329
- file_info.update(add_result.get("data", {}))
330
-
286
+ with yaspin(Spinners.dots, text=f"上传文件 {file_name}") as spinner:
287
+ try:
288
+ if not os.path.exists(file_path):
289
+ spinner.text = f"文件不存在: {file_path}"
290
+ spinner.fail("❌")
291
+ return False
292
+
293
+ # Get file name and content type
294
+ content_type = self._get_content_type(file_path)
295
+
296
+ spinner.text = f"准备上传文件: {file_name}"
297
+
298
+ # Prepare form data
299
+ form_data = {
300
+ "OSSAccessKeyId": upload_token["accessId"],
301
+ "policy": upload_token["policy"],
302
+ "signature": upload_token["signature"],
303
+ "key": f"{upload_token['dir']}{file_name}",
304
+ "dir": upload_token["dir"],
305
+ "success_action_status": "200",
306
+ }
307
+
308
+ # Prepare files
309
+ files = {
310
+ "file": (file_name, open(file_path, "rb"), content_type)
311
+ }
312
+
313
+ spinner.text = f"正在上传文件: {file_name}"
314
+
315
+ # Upload file
316
+ response = requests.post(
317
+ upload_token["host"], data=form_data, files=files
318
+ )
319
+
320
+ if response.status_code != 200:
321
+ spinner.text = (
322
+ f"上传失败 {file_name}: HTTP {response.status_code}"
323
+ )
324
+ spinner.fail("")
325
+ return False
326
+
327
+ # Determine file type based on extension
328
+ file_ext = os.path.splitext(file_path)[1].lower()
329
+ is_image = file_ext in self.IMAGE_EXTENSIONS
330
+
331
+ uploaded_files.append(
332
+ {
333
+ "fileKey": file_name,
334
+ "fileType": "image" if is_image else "file",
335
+ "dir": upload_token["dir"],
336
+ }
337
+ )
338
+
339
+ spinner.text = f"获取下载链接: {file_name}"
340
+
341
+ # Get download links for uploaded files
342
+ url = "https://api.tongyi.com/dialog/downloadLink/batch"
343
+ headers = self._get_base_headers()
344
+ payload = {
345
+ "fileKeys": [f["fileKey"] for f in uploaded_files],
346
+ "fileType": "image" if any(f["fileType"] == "image" for f in uploaded_files) else "file",
347
+ "dir": upload_token["dir"],
348
+ }
349
+
350
+ response = requests.post(url, headers=headers, json=payload)
351
+ if response.status_code != 200:
352
+ spinner.text = f"获取下载链接失败: HTTP {response.status_code}"
353
+ spinner.fail("")
354
+ return False
355
+
356
+ result = response.json()
357
+ if not result.get("success"):
358
+ spinner.text = f"获取下载链接失败: {result.get('errorMsg')}"
359
+ spinner.fail("❌")
360
+ return False
361
+
362
+ # Add files to chat
363
+ self.uploaded_file_info = result.get("data", {}).get(
364
+ "results", []
365
+ )
366
+ for file_info in self.uploaded_file_info:
367
+ spinner.text = f"添加文件到对话: {file_name}"
368
+ add_url = (
369
+ "https://api.tongyi.com/assistant/api/chat/file/add"
370
+ )
371
+ add_payload = {
372
+ "workSource": "chat",
373
+ "terminal": "web",
374
+ "workCode": "0",
375
+ "channel": "home",
376
+ "workType": "file",
377
+ "module": "uploadhistory",
378
+ "workName": file_info["fileKey"],
379
+ "workId": file_info["docId"],
380
+ "workResourcePath": file_info["url"],
381
+ "sessionId": "",
382
+ "batchId": str(uuid.uuid4()).replace("-", "")[
383
+ :32
384
+ ], # Generate random batchId
385
+ "fileSize": os.path.getsize(file_path),
386
+ }
387
+
388
+ add_response = requests.post(
389
+ add_url, headers=headers, json=add_payload
390
+ )
391
+ if add_response.status_code != 200:
392
+ spinner.text = (
393
+ f"添加文件到对话失败: HTTP {add_response.status_code}"
394
+ )
395
+ spinner.fail("❌")
396
+ continue
397
+
398
+ add_result = add_response.json()
399
+ if not add_result.get("success"):
400
+ spinner.text = (
401
+ f"添加文件到对话失败: {add_result.get('errorMsg')}"
402
+ )
403
+ spinner.fail("❌")
404
+ continue
405
+
406
+ file_info.update(add_result.get("data", {}))
407
+
408
+ spinner.text = f"文件 {file_name} 上传成功"
409
+ spinner.ok("✅")
410
+ time.sleep(1) # 短暂暂停以便用户看到成功状态
411
+
412
+ except Exception as e:
413
+ spinner.text = f"上传文件 {file_name} 时出错: {str(e)}"
414
+ spinner.fail("❌")
415
+ return False
331
416
  return True
332
-
417
+
333
418
  except Exception as e:
334
419
  PrettyOutput.print(f"Error uploading files: {str(e)}", OutputType.ERROR)
335
420
  return False
336
-
421
+
337
422
  def _get_content_type(self, file_path: str) -> str:
338
423
  """Get content type for file
339
-
424
+
340
425
  Args:
341
426
  file_path: Path to file
342
-
427
+
343
428
  Returns:
344
429
  str: Content type
345
430
  """
346
431
  ext = os.path.splitext(file_path)[1].lower()
347
432
  content_types = {
348
- '.txt': 'text/plain',
349
- '.md': 'text/markdown',
350
- '.doc': 'application/msword',
351
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
352
- '.xls': 'application/vnd.ms-excel',
353
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
354
- '.pdf': 'application/pdf',
355
- '.png': 'image/png',
356
- '.jpg': 'image/jpeg',
357
- '.jpeg': 'image/jpeg',
358
- '.gif': 'image/gif',
359
- '.mp4': 'video/mp4',
360
- '.mp3': 'audio/mpeg',
361
- '.wav': 'audio/wav'
433
+ ".txt": "text/plain",
434
+ ".md": "text/markdown",
435
+ ".doc": "application/msword",
436
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
437
+ ".xls": "application/vnd.ms-excel",
438
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
439
+ ".pdf": "application/pdf",
440
+ ".epub": "application/epub+zip",
441
+ ".mobi": "application/x-mobipocket-ebook",
442
+ ".jpg": "image/jpeg",
443
+ ".jpeg": "image/jpeg",
444
+ ".png": "image/png",
445
+ ".gif": "image/gif",
446
+ ".webp": "image/webp",
447
+ ".bmp": "image/bmp",
448
+ ".tiff": "image/tiff",
362
449
  }
363
- return content_types.get(ext, 'application/octet-stream')
450
+ return content_types.get(ext, "application/octet-stream")
364
451
 
365
452
  def name(self) -> str:
366
453
  """Get platform name
367
-
454
+
368
455
  Returns:
369
456
  str: Platform name
370
457
  """
@@ -372,7 +459,7 @@ class TongyiPlatform(BasePlatform):
372
459
 
373
460
  def delete_chat(self) -> bool:
374
461
  """Delete chat history
375
-
462
+
376
463
  Returns:
377
464
  bool: True if deletion successful, False otherwise
378
465
  """
@@ -381,18 +468,22 @@ class TongyiPlatform(BasePlatform):
381
468
 
382
469
  url = "https://api.tongyi.com/dialog/session/delete"
383
470
  headers = self._get_base_headers()
384
- payload = {
385
- "sessionId": self.session_id
386
- }
471
+ payload: Dict[str, Any] = {"sessionId": self.session_id}
387
472
 
388
473
  try:
389
- response = while_success(lambda: requests.post(url, headers=headers, json=payload), sleep_time=5)
474
+ response = while_success(
475
+ lambda: requests.post(url, headers=headers, json=payload), sleep_time=5
476
+ )
390
477
  if response.status_code != 200:
391
- PrettyOutput.print(f"Failed to delete chat: HTTP {response.status_code}", OutputType.ERROR)
478
+ PrettyOutput.print(
479
+ f"Failed to delete chat: HTTP {response.status_code}",
480
+ OutputType.ERROR,
481
+ )
392
482
  return False
393
483
  self.request_id = ""
394
484
  self.session_id = ""
395
485
  self.msg_id = ""
486
+ self.first_chat = True # Reset first_chat flag
396
487
  return True
397
488
  except Exception as e:
398
489
  PrettyOutput.print(f"Error deleting chat: {str(e)}", OutputType.ERROR)
@@ -400,7 +491,7 @@ class TongyiPlatform(BasePlatform):
400
491
 
401
492
  def set_system_message(self, message: str):
402
493
  """Set system message
403
-
494
+
404
495
  Args:
405
496
  message: System message to set
406
497
  """
@@ -408,7 +499,7 @@ class TongyiPlatform(BasePlatform):
408
499
 
409
500
  def get_model_list(self) -> List[Tuple[str, str]]:
410
501
  """Get available model list
411
-
502
+
412
503
  Returns:
413
504
  List[Tuple[str, str]]: List of (model_id, model_name) tuples
414
505
  """
@@ -421,7 +512,7 @@ class TongyiPlatform(BasePlatform):
421
512
 
422
513
  def support_web(self) -> bool:
423
514
  """Check if platform supports web functionality
424
-
515
+
425
516
  Returns:
426
517
  bool: True if web is supported, False otherwise
427
518
  """
@@ -157,7 +157,7 @@ class FileSearchReplaceTool:
157
157
 
158
158
  if file_exists and agent:
159
159
  files = agent.get_user_data("files")
160
- if not files or files.get(file_path, None) is None:
160
+ if not files or file_path not in files:
161
161
  return {
162
162
  "success": False,
163
163
  "stdout": "",
@@ -169,6 +169,21 @@ class FileSearchReplaceTool:
169
169
  success, temp_content = fast_edit(file_path, changes, spinner)
170
170
  if not success:
171
171
  success, temp_content = slow_edit(file_path, yaml.safe_dump(changes, allow_unicode=True), spinner)
172
+ if not success:
173
+ spinner.text = f"文件 {file_path} 处理失败"
174
+ spinner.fail("❌")
175
+ return {
176
+ "success": False,
177
+ "stdout": "",
178
+ "stderr": temp_content
179
+ }
180
+ else:
181
+ spinner.text = f"文件 {file_path} 内容生成完成"
182
+ spinner.ok("✅")
183
+ else:
184
+ spinner.text = f"文件 {file_path} 内容生成完成"
185
+ spinner.ok("✅")
186
+
172
187
 
173
188
  # 只有当所有替换操作都成功时,才写回文件
174
189
  if success and (temp_content != original_content or not file_exists):
@@ -184,13 +199,6 @@ class FileSearchReplaceTool:
184
199
  stdout_message = f"文件 {file_path} {action} 完成"
185
200
  stdout_messages.append(stdout_message)
186
201
  PrettyOutput.print(stdout_message, OutputType.SUCCESS)
187
- elif success:
188
- stdout_message = f"文件 {file_path} 没有找到需要替换的内容"
189
- stdout_messages.append(stdout_message)
190
- PrettyOutput.print(stdout_message, OutputType.INFO)
191
- else:
192
- stdout_message = f"文件 {file_path} 修改失败"
193
- stdout_messages.append(stdout_message)
194
202
 
195
203
  except Exception as e:
196
204
  stderr_message = f"处理文件 {file_path} 时出错: {str(e)}"
@@ -344,7 +352,7 @@ def slow_edit(filepath: str, patch_content: str, spinner: Yaspin) -> Tuple[bool,
344
352
  if upload_success:
345
353
  response = model.chat_until_success(main_prompt)
346
354
  else:
347
- return False, ""
355
+ return False, "文件上传失败"
348
356
 
349
357
  # 解析差异化补丁
350
358
  diff_blocks = re.finditer(ot("DIFF")+r'\s*>{4,} SEARCH\n?(.*?)\n?={4,}\n?(.*?)\s*<{4,} REPLACE\n?'+ct("DIFF"),
@@ -357,17 +365,17 @@ def slow_edit(filepath: str, patch_content: str, spinner: Yaspin) -> Tuple[bool,
357
365
  "replace": match.group(2).strip()
358
366
  })
359
367
 
360
- success, modified_content = fast_edit(filepath, patches, spinner)
368
+ success, modified_content_or_err = fast_edit(filepath, patches, spinner)
361
369
  if success:
362
- return True, modified_content
370
+ return True, modified_content_or_err
363
371
  spinner.text = f"文件 {filepath} 修改失败"
364
372
  spinner.fail("❌")
365
- return False, ""
373
+ return False, f"文件修改失败: {modified_content_or_err}"
366
374
 
367
375
  except Exception as e:
368
376
  spinner.text = f"文件修改失败: {str(e)}"
369
377
  spinner.fail("❌")
370
- return False, ""
378
+ return False, f"文件修改失败: {str(e)}"
371
379
 
372
380
 
373
381
  def fast_edit(filepath: str, patches: List[Dict[str,str]], spinner: Yaspin) -> Tuple[bool, str]:
@@ -410,6 +418,7 @@ def fast_edit(filepath: str, patches: List[Dict[str,str]], spinner: Yaspin) -> T
410
418
  modified_content = file_content
411
419
  patch_count = 0
412
420
  success = True
421
+ err_msg = ""
413
422
  for patch in patches:
414
423
  search_text = patch["search"]
415
424
  replace_text = patch["replace"]
@@ -419,6 +428,7 @@ def fast_edit(filepath: str, patches: List[Dict[str,str]], spinner: Yaspin) -> T
419
428
  # 如果有多处,报错
420
429
  if modified_content.count(search_text) > 1:
421
430
  success = False
431
+ err_msg = f"搜索文本 {search_text} 在文件中存在多处,请检查补丁内容"
422
432
  break
423
433
  # 应用替换
424
434
  modified_content = modified_content.replace(
@@ -426,10 +436,11 @@ def fast_edit(filepath: str, patches: List[Dict[str,str]], spinner: Yaspin) -> T
426
436
  spinner.write(f"✅ 补丁 #{patch_count} 应用成功")
427
437
  else:
428
438
  success = False
439
+ err_msg = f"搜索文本 {search_text} 在文件中不存在,请检查补丁内容"
429
440
  break
430
441
  if not success:
431
442
  revert_file(filepath)
432
- return False, ""
443
+ return False, err_msg
433
444
 
434
445
 
435
446
  spinner.text = f"文件 {filepath} 修改完成,应用了 {patch_count} 个补丁"
@@ -88,18 +88,9 @@ class generate_new_tool:
88
88
  "stderr": f"工具 '{tool_name}' 已经存在于 {tool_file_path}"
89
89
  }
90
90
 
91
- # 验证并处理工具代码
92
- processed_code, error_msg = self._validate_and_process_code(tool_name, tool_code)
93
- if error_msg:
94
- return {
95
- "success": False,
96
- "stdout": "",
97
- "stderr": error_msg
98
- }
99
-
100
91
  # 写入工具文件
101
92
  with open(tool_file_path, "w", encoding="utf-8") as f:
102
- f.write(processed_code)
93
+ f.write(tool_code)
103
94
 
104
95
  # 注册新工具到当前的工具注册表
105
96
  success_message = f"工具 '{tool_name}' 已成功生成在 {tool_file_path}"
@@ -152,87 +143,4 @@ class generate_new_tool:
152
143
  "success": False,
153
144
  "stdout": "",
154
145
  "stderr": error_msg
155
- }
156
-
157
- def _validate_and_process_code(self, tool_name: str, tool_code: str) -> Tuple[str, str]:
158
- """
159
- 验证并处理工具代码
160
-
161
- 参数:
162
- tool_name: 工具名称
163
- tool_code: 工具代码
164
-
165
- 返回:
166
- Tuple[str, str]: (处理后的代码, 错误信息)
167
- """
168
- # 检查工具代码中是否包含类定义
169
- if f"class {tool_name}" not in tool_code:
170
- # 尝试找到任何类定义
171
- class_match = re.search(r"class\s+(\w+)", tool_code)
172
- if class_match:
173
- old_class_name = class_match.group(1)
174
- # 替换类名为工具名
175
- tool_code = tool_code.replace(f"class {old_class_name}", f"class {tool_name}")
176
- tool_code = tool_code.replace(f'name = "{old_class_name}"', f'name = "{tool_name}"')
177
- else:
178
- # 没有找到类定义,返回错误
179
- return "", f"工具代码中缺少类定义 'class {tool_name}'"
180
-
181
- # 检查工具代码中是否包含必要的属性和方法
182
- missing_components = []
183
-
184
- if f'name = "{tool_name}"' not in tool_code and f"name = '{tool_name}'" not in tool_code:
185
- # 尝试查找任何name属性并修复
186
- name_match = re.search(r'name\s*=\s*["\'](\w+)["\']', tool_code)
187
- if name_match:
188
- old_name = name_match.group(1)
189
- tool_code = re.sub(r'name\s*=\s*["\'](\w+)["\']', f'name = "{tool_name}"', tool_code)
190
- else:
191
- missing_components.append(f"name = \"{tool_name}\"")
192
-
193
- if "description = " not in tool_code:
194
- missing_components.append("description 属性")
195
-
196
- if "parameters = " not in tool_code:
197
- missing_components.append("parameters 属性")
198
-
199
- if "def execute(self, args:" not in tool_code:
200
- missing_components.append("execute 方法")
201
-
202
- if "def check(" not in tool_code:
203
- # 添加默认的check方法
204
- class_match = re.search(r"class\s+(\w+).*?:", tool_code, re.DOTALL)
205
- if class_match:
206
- indent = " " # 默认缩进
207
- # 找到类定义后的第一个属性
208
- first_attr_match = re.search(r"class\s+(\w+).*?:(.*?)(\w+\s*=)", tool_code, re.DOTALL)
209
- if first_attr_match:
210
- # 获取属性前的缩进
211
- attr_indent = re.search(r"\n([ \t]*)\w+\s*=", first_attr_match.group(2))
212
- if attr_indent:
213
- indent = attr_indent.group(1)
214
-
215
- check_method = f"\n{indent}@staticmethod\n{indent}def check() -> bool:\n{indent} \"\"\"检查工具是否可用\"\"\"\n{indent} return True\n"
216
-
217
- # 在类定义后插入check方法
218
- pattern = r"(class\s+(\w+).*?:.*?)(\n\s*\w+\s*=|\n\s*@|\n\s*def)"
219
- replacement = r"\1" + check_method + r"\3"
220
- tool_code = re.sub(pattern, replacement, tool_code, 1, re.DOTALL)
221
-
222
- # 如果缺少必要组件,返回错误信息
223
- if missing_components:
224
- return "", f"工具代码中缺少以下必要组件: {', '.join(missing_components)}"
225
-
226
- # 确保代码有正确的Python文件头部
227
- if not tool_code.startswith("# -*- coding:") and not tool_code.startswith("# coding="):
228
- tool_code = "# -*- coding: utf-8 -*-\n" + tool_code
229
-
230
- # 确保导入了必要的模块
231
- if "from typing import Dict, Any" not in tool_code:
232
- imports_pos = tool_code.find("\n\n")
233
- if imports_pos > 0:
234
- tool_code = tool_code[:imports_pos] + "\nfrom typing import Dict, Any" + tool_code[imports_pos:]
235
- else:
236
- tool_code = "from typing import Dict, Any\n\n" + tool_code
237
-
238
- return tool_code, ""
146
+ }
@@ -100,6 +100,7 @@ arguments:
100
100
  - 假设工具结果
101
101
  - 创建虚构对话
102
102
  - 在没有所需信息的情况下继续
103
+ - yaml 格式错误
103
104
  </common_errors>
104
105
  </tool_system_guide>
105
106
  """
@@ -532,7 +533,7 @@ class ToolRegistry(OutputHandlerProtocol):
532
533
  else:
533
534
  return (
534
535
  {},
535
- f"""工具调用格式错误,请检查工具调用格式。
536
+ f"""工具调用格式错误,请检查工具调用格式(缺少name、arguments、want字段)。
536
537
 
537
538
  {tool_call_help}""",
538
539
  )
@@ -541,6 +542,8 @@ class ToolRegistry(OutputHandlerProtocol):
541
542
  {},
542
543
  f"""工具调用格式错误,请检查工具调用格式。
543
544
 
545
+ {e}
546
+
544
547
  {tool_call_help}""",
545
548
  )
546
549
  if len(ret) > 1:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jarvis-ai-assistant
3
- Version: 0.1.183
3
+ Version: 0.1.185
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
@@ -85,7 +85,7 @@ Dynamic: requires-python
85
85
 
86
86
  ## 🌟 核心特色 <a id="core-features"></a>
87
87
 
88
- - 🆓 零成本接入:无缝集成元宝、Kimi等优质模型,无需支付API费用,同时保留强大的文件处理、搜索和推理能力
88
+ - 🆓 零成本接入:无缝集成腾讯元宝(推荐首选)、Kimi等优质模型,无需支付API费用,同时保留强大的文件处理、搜索和推理能力
89
89
  - 🛠️ 工具驱动:内置丰富工具集,涵盖脚本执行、代码开发、网页搜索、终端操作等核心功能
90
90
  - 👥 人机协作:支持实时交互,用户可随时介入指导,确保AI行为符合预期
91
91
  - 🔌 高度可扩展:支持自定义工具和平台,轻松集成MCP协议
@@ -117,9 +117,9 @@ pip3 install jarvis-ai-assistant
117
117
 
118
118
  将以下配置写入到`~/.jarvis/config.yaml`文件中。
119
119
 
120
- #### 腾讯元宝
120
+ #### 腾讯元宝 (推荐首选)
121
121
  ```yaml
122
- JARVIS_PLATFORM: yuanbao
122
+ JARVIS_PLATFORM: yuanbao # 推荐使用腾讯元宝平台,适配性最佳
123
123
  JARVIS_MODEL: deep_seek_v3
124
124
  JARVIS_THINKING_PLATFORM: yuanbao
125
125
  JARVIS_THINKING_MODEL: deep_seek
@@ -1,7 +1,7 @@
1
- jarvis/__init__.py,sha256=KysQNyO0AUjUMLzRISa3Wf1q9ffBDepVKiOh2ztirYg,74
2
- jarvis/jarvis_agent/__init__.py,sha256=AxT_2n-IQkbtoQlAS3SJ0tsvcUenWD7_Xrc-RZZCWiA,30352
1
+ jarvis/__init__.py,sha256=ctjj6eadc4jmCb3OWiljzQVxwTvbZ3urmkzwzSyoUYQ,74
2
+ jarvis/jarvis_agent/__init__.py,sha256=Mho0roePyuitHFjVCbK0_v9FPgEarcpvgIvY3sQKLws,30843
3
3
  jarvis/jarvis_agent/builtin_input_handler.py,sha256=f4DaEHPakXcAbgykFP-tiOQP6fh_yGFlZx_h91_j2tQ,1529
4
- jarvis/jarvis_agent/file_input_handler.py,sha256=LDNXoTtyjhyBmfzDnAdbWZ2BWdu4q-r6thSKRK8Iwjk,4187
4
+ jarvis/jarvis_agent/file_input_handler.py,sha256=7u8pXWD7F9mmiJkr9XO83mhFu40FSRoYQm55DbZHgQo,4203
5
5
  jarvis/jarvis_agent/jarvis.py,sha256=UkNMVUlSNKV6y3v12eAhqc_gIDB6Obxrwk5f7-sQeiQ,6137
6
6
  jarvis/jarvis_agent/main.py,sha256=GkjMTIbsd56nkVuRwD_tU_PZWyzixZZhMjVOCd0SzOA,2669
7
7
  jarvis/jarvis_agent/output_handler.py,sha256=7qori-RGrQmdiFepoEe3oPPKJIvRt90l_JDmvCoa4zA,1219
@@ -52,7 +52,7 @@ jarvis/jarvis_platform/human.py,sha256=xwaTZ1zdrAYZZFXxkbHvUdECwCGsic0kgAFUncUr4
52
52
  jarvis/jarvis_platform/kimi.py,sha256=b3EpnmHseZwrfCc8sMmvwLJ6Jg2FWf8ATItSDz5G3eQ,11978
53
53
  jarvis/jarvis_platform/openai.py,sha256=VyX3bR1rGxrJdWOtUBf8PgSL9n06KaNbOewL1urzOnk,4741
54
54
  jarvis/jarvis_platform/registry.py,sha256=3djxE8AB4gwrdAOvRSL0612Rt_CcsaZhzZ0_oXHu6xk,7820
55
- jarvis/jarvis_platform/tongyi.py,sha256=m44aZHZ1oCbYdlSMuG3qYPFZbHW4e3VlaFZ2i3H7xrE,16927
55
+ jarvis/jarvis_platform/tongyi.py,sha256=NhE8ssvTI_XoDkXhaGtrUJG6q6-0OApWvh8d7wuFZXg,20944
56
56
  jarvis/jarvis_platform/yuanbao.py,sha256=FDi-D9Jnw_MiwI0skPNMYz874o6GhWhdNRdZg-ECoUA,20632
57
57
  jarvis/jarvis_platform_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
58
  jarvis/jarvis_platform_manager/main.py,sha256=OXWj18SqiV0Gl75YT6D9wspCCB4Nes04EY-ShI9kbpU,25677
@@ -65,15 +65,15 @@ jarvis/jarvis_tools/chdir.py,sha256=DNKVFrWqu6t_sZ2ipv99s6802QR4cSGlqKlmaI--arE,
65
65
  jarvis/jarvis_tools/code_plan.py,sha256=gWR0lzY62x2PxWKoMRBqW6jq7zQuO8vhpjC4TcHSYjk,7685
66
66
  jarvis/jarvis_tools/create_code_agent.py,sha256=-nHfo5O5pDIG5IX3w1ClQafGvGcdI2_w75-KGrD-gUQ,3458
67
67
  jarvis/jarvis_tools/create_sub_agent.py,sha256=lyFrrg4V0yXULmU3vldwGp_euZjwZzJcRU6mJ20zejY,3023
68
- jarvis/jarvis_tools/edit_file.py,sha256=gxnVijz-mOHpb9A7WTPIqCwmZHInSHwu_Psa_GvNWRQ,16724
68
+ jarvis/jarvis_tools/edit_file.py,sha256=czpJOY7Y7Q-SH3UiGfgfB1lRcqtzuIZjLBLZzozcl0A,17330
69
69
  jarvis/jarvis_tools/execute_script.py,sha256=IA1SkcnwBB9PKG2voBNx5N9GXL303OC7OOtdqRfqWOk,6428
70
70
  jarvis/jarvis_tools/file_analyzer.py,sha256=7ILHkUFm8pPZn1y_s4uT0kaWHP-EmlHnpkovDdA1yRE,4872
71
71
  jarvis/jarvis_tools/file_operation.py,sha256=WloC1-oPJLwgICu4WBc9f7XA8N_Ggl73QQ5CxM2XTlE,9464
72
- jarvis/jarvis_tools/generate_new_tool.py,sha256=k1Vt88kI1bYi1OwxvJqFKr3Ewwwv7lOegYNmZ-1F7x0,10283
72
+ jarvis/jarvis_tools/generate_new_tool.py,sha256=dLfOliIUm0ovLrHcZAhKm7lqhxwACv8mnGxxGtLJ--o,5960
73
73
  jarvis/jarvis_tools/methodology.py,sha256=m7cQmVhhQpUUl_uYTVvcW0JBovQLx5pWTXh_8K77HsU,5237
74
74
  jarvis/jarvis_tools/read_code.py,sha256=pL2SwZDsJbJMXo4stW96quFsLgbtPVIAW-h4sDKsLtM,6274
75
75
  jarvis/jarvis_tools/read_webpage.py,sha256=PFAYuKjay9j6phWzyuZ99ZfNaHJljmRWAgS0bsvbcvE,2219
76
- jarvis/jarvis_tools/registry.py,sha256=WvYPiaUrleFqeXvwRkxM-6TNs1sWm61mpg1MFVo_kas,25113
76
+ jarvis/jarvis_tools/registry.py,sha256=qdA0fgWeh_UG-_Rt5COnHTcKmOsYBiIU69uX4cLvfWI,25199
77
77
  jarvis/jarvis_tools/rewrite_file.py,sha256=3V2l7kG5DG9iRimBce-1qCRuJPL0QM32SBTzOl2zCqM,7004
78
78
  jarvis/jarvis_tools/search_web.py,sha256=rzxrCOTEo-MmLQrKI4k-AbfidUfJUeCPK4f5ZJy48G8,952
79
79
  jarvis/jarvis_tools/virtual_tty.py,sha256=8E_n-eC-RRPTqYx6BI5Q2RnorY8dbhKFBfAjIiRQROA,16397
@@ -91,9 +91,9 @@ jarvis/jarvis_utils/methodology.py,sha256=A8pE8ZqNHvGKaDO4TFtg7Oz-hAXPBcQfhmSPWM
91
91
  jarvis/jarvis_utils/output.py,sha256=QboL42GtG_dnvd1O64sl8o72mEBhXNRADPXQMXgDE7Q,9661
92
92
  jarvis/jarvis_utils/tag.py,sha256=YJHmuedLb7_AiqvKQetHr4R1FxyzIh7HN0RRkWMmYbU,429
93
93
  jarvis/jarvis_utils/utils.py,sha256=atSK-2cUr7_tOIFsQzJnuQxebi7aFN4jtmaoXEaV4jM,10692
94
- jarvis_ai_assistant-0.1.183.dist-info/licenses/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
95
- jarvis_ai_assistant-0.1.183.dist-info/METADATA,sha256=RRkBRiDEeBnEfQQFAufn9jE2RV7BA90P5o5G_PnJUp0,15836
96
- jarvis_ai_assistant-0.1.183.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
97
- jarvis_ai_assistant-0.1.183.dist-info/entry_points.txt,sha256=Gy3DOP1PYLMK0GCj4rrP_9lkOyBQ39EK_lKGUSwn41E,869
98
- jarvis_ai_assistant-0.1.183.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
99
- jarvis_ai_assistant-0.1.183.dist-info/RECORD,,
94
+ jarvis_ai_assistant-0.1.185.dist-info/licenses/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
95
+ jarvis_ai_assistant-0.1.185.dist-info/METADATA,sha256=_DBSrubK80hRPGRBLXCpPy7r7tgcEhMM_RDS82mvaFw,15923
96
+ jarvis_ai_assistant-0.1.185.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
97
+ jarvis_ai_assistant-0.1.185.dist-info/entry_points.txt,sha256=Gy3DOP1PYLMK0GCj4rrP_9lkOyBQ39EK_lKGUSwn41E,869
98
+ jarvis_ai_assistant-0.1.185.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
99
+ jarvis_ai_assistant-0.1.185.dist-info/RECORD,,