jarvis-ai-assistant 0.1.118__py3-none-any.whl → 0.1.120__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.

@@ -3,6 +3,7 @@ from typing import Dict, Any, List, Tuple
3
3
  import os
4
4
  from jarvis.jarvis_agent.output_handler import OutputHandler
5
5
  from jarvis.jarvis_tools.git_commiter import GitCommitTool
6
+ from jarvis.jarvis_tools.read_code import ReadCodeTool
6
7
  from jarvis.jarvis_utils import OutputType, PrettyOutput, get_multiline_input, has_uncommitted_changes, user_confirm
7
8
 
8
9
 
@@ -20,176 +21,262 @@ class PatchOutputHandler(OutputHandler):
20
21
 
21
22
  def prompt(self) -> str:
22
23
  return """
23
- # 📝 Patch Format
24
- Use patch blocks to specify code changes:
25
-
26
- ```
27
- <PATCH>
28
- path/to/file start,end
29
- new_content
30
- </PATCH>
31
- ```
32
-
33
- # 📋 Format Rules
34
- 1. File Path
35
- - Use relative path from project root
24
+ # 🔄 REPLACE: Modify existing code
25
+ <REPLACE>
26
+ File: path/to/file
27
+ Lines: [start,end] or [start,end)
28
+ [new content]
29
+ ...
30
+ </REPLACE>
31
+
32
+ # ➕ INSERT: Add new code
33
+ <INSERT>
34
+ File: path/to/file
35
+ Line: position
36
+ [new content]
37
+ ...
38
+ </INSERT>
39
+
40
+ # 🗑️ DELETE: Remove existing code
41
+ <DELETE>
42
+ File: path/to/file
43
+ Lines: [start,end] or [start,end)
44
+ </DELETE>
45
+
46
+ # 🆕 NEW_FILE: Create new file
47
+ <NEW_FILE>
48
+ File: path/to/file
49
+ [new content]
50
+ ...
51
+ </NEW_FILE>
52
+
53
+ # ➡️ MOVE_FILE: Relocate a file
54
+ <MOVE_FILE>
55
+ File: path/to/source/file
56
+ NewPath: path/to/destination/file
57
+ </MOVE_FILE>
58
+
59
+ # ❌ REMOVE_FILE: Delete entire file
60
+ <REMOVE_FILE>
61
+ File: path/to/file
62
+ </REMOVE_FILE>
63
+
64
+ # 📋 Formatting Rules
65
+ 1. File Paths
66
+ - Use relative paths from project root
36
67
  - Must be exact and case-sensitive
37
68
  - Example: src/module/file.py
38
-
69
+
39
70
  2. Line Numbers
40
- - Format: start,end
41
- - start: First line to modify (included)
42
- - end: Line after last modified line
43
- - Both numbers are based on original file
44
-
45
- 3. Special Cases
46
- - Insert: Use same number for start,end
47
- - New File: Use 0,0
48
- - Example: "5,5" inserts before line 5
49
-
50
- # 📌 Examples
51
- ## Modify Existing Code
52
- ```
53
- <PATCH>
54
- src/utils.py 10,15
55
- def new_function():
56
- return "modified"
57
- </PATCH>
58
- ```
59
-
60
- ## Insert New Code
61
- ```
62
- <PATCH>
63
- src/main.py 20,20
64
- new_line_here()
65
- </PATCH>
66
- ```
67
-
68
- ## Create New File
69
- ```
70
- <PATCH>
71
- src/new_file.py 0,0
72
- def new_function():
71
+ - Format: [start,end] (inclusive) or [start,end) (right-exclusive)
72
+ - 1-based line numbers
73
+ - Single number for INSERT
74
+ - Omit for NEW_FILE/REMOVE_FILE
75
+
76
+ 3. Content
77
+ - Maintain original indentation
78
+ - Follow existing code style
79
+
80
+ # 📌 Usage Examples
81
+ ## REPLACE Example (Closed Interval)
82
+ <REPLACE>
83
+ File: src/utils.py
84
+ Lines: [9,13]
85
+ def updated_function():
86
+ # Replaces lines 9-13 inclusive
87
+ return "new_implementation"
88
+ </REPLACE>
89
+
90
+ ## REPLACE Example (Left-Closed Right-Open)
91
+ <REPLACE>
92
+ File: src/calculator.py
93
+ Lines: [5,8)
94
+ def new_calculation():
95
+ # Replaces lines 5-7 (excludes line 8)
96
+ return 42
97
+ </REPLACE>
98
+
99
+ ## INSERT Example
100
+ <INSERT>
101
+ File: src/main.py
102
+ Line: 19
103
+ # Inserted before line 19
104
+ new_feature()
105
+ </INSERT>
106
+
107
+ ## NEW_FILE Example
108
+ <NEW_FILE>
109
+ File: src/new_module.py
110
+ # New file creation
111
+ def feature_entry():
73
112
  pass
74
- </PATCH>
75
- ```
113
+ </NEW_FILE>
114
+
115
+ ## DELETE Example
116
+ <DELETE>
117
+ File: src/utils.py
118
+ Lines: [9,13]
119
+ </DELETE>
120
+
121
+ ## MOVE_FILE Example
122
+ <MOVE_FILE>
123
+ File: src/old_dir/file.py
124
+ NewPath: src/new_dir/file.py
125
+ </MOVE_FILE>
76
126
 
77
- # Important Rules
78
- 1. ONE modification per patch block
79
- 2. Include necessary context
127
+ ## REMOVE_FILE Example
128
+ <REMOVE_FILE>
129
+ File: src/obsolete.py
130
+ </REMOVE_FILE>
131
+
132
+ # 🚨 Critical Requirements
133
+ 1. One change per block
134
+ 2. Use correct operation type
80
135
  3. Match existing code style
81
- 4. Preserve indentation
82
- 5. Use exact file paths
136
+ 4. Preserve indentation levels
137
+ 5. Exact file paths required
138
+ 6. Handle edge cases properly
139
+ 7. Include error handling
140
+ 8. Maintain code consistency
83
141
  """
84
142
 
85
143
 
86
144
  def _parse_patch(patch_str: str) -> Dict[str, List[Dict[str, Any]]]:
87
- """Parse patches from string with format:
88
- <PATCH>
89
- path/to/file start_line,end_line
90
- content_line1
91
- content_line2
92
- ...
93
- </PATCH>
94
- """
145
+ """Parse patches from string with optimized format"""
95
146
  result = {}
96
- patches = re.findall(r"<PATCH>(.*?)</PATCH>", patch_str, re.DOTALL)
147
+ patches = re.findall(r"<(REPLACE|INSERT|DELETE|NEW_FILE|REMOVE_FILE|MOVE_FILE)>(.*?)</\1>", patch_str, re.DOTALL)
97
148
 
98
- for patch in patches:
149
+ for patch_type, patch in patches:
99
150
  lines = patch.strip().split('\n')
100
151
  if not lines:
101
152
  continue
102
153
 
103
- # Parse file path and line range
104
- file_info = lines[0].strip()
105
-
106
- # Extract file path and line range
107
- match = re.match(r'([^\s]+)\s+(\d+),(\d+)', file_info)
108
- if not match:
154
+ # Parse file path
155
+ file_match = re.match(r"File:\s*([^\s]+)", lines[0])
156
+ if not file_match:
109
157
  continue
110
-
111
- filepath = match.group(1).strip()
112
- start_line = int(match.group(2))
113
- end_line = int(match.group(3))
158
+ filepath = file_match.group(1).strip()
159
+
160
+ # Initialize line numbers
161
+ start_line = end_line = 0
162
+
163
+ # Parse line numbers based on operation type
164
+ if patch_type in ['REPLACE', 'DELETE']:
165
+ # 增强正则表达式兼容性
166
+ line_match = re.match(
167
+ r"^Lines:\s*\[\s*(\d+)\s*,\s*(\d+)\s*([\]\)])\s*$", # 匹配行尾
168
+ lines[1].strip(), # 去除前后空格
169
+ re.IGNORECASE
170
+ )
171
+ if line_match:
172
+ start_line = int(line_match.group(1))
173
+ end_value = int(line_match.group(2))
174
+ bracket_type = line_match.group(3).strip()
175
+
176
+ # 根据括号类型处理区间
177
+ if bracket_type == ')': # [m,n)
178
+ end_line = end_value - 1
179
+ else: # [m,n]
180
+ end_line = end_value
181
+
182
+ # 确保 end_line >= start_line
183
+ end_line = max(end_line, start_line)
184
+ else:
185
+ PrettyOutput.print(f"无法解析行号格式: {lines[1]}", OutputType.WARNING)
186
+ continue
187
+ elif patch_type == 'INSERT':
188
+ line_match = re.match(r"Line:\s*(\d+)", lines[1])
189
+ if line_match:
190
+ start_line = int(line_match.group(1)) # 1-based
191
+ end_line = start_line
192
+ elif patch_type == 'MOVE_FILE':
193
+ new_path_match = re.match(r"NewPath:\s*([^\s]+)", lines[1])
194
+ if new_path_match:
195
+ new_path = new_path_match.group(1).strip()
196
+ else:
197
+ continue
198
+
199
+ # Get content (after metadata)
200
+ if patch_type in ['REPLACE', 'DELETE']:
201
+ content_start = 2 # File + Lines
202
+ elif patch_type == 'INSERT':
203
+ content_start = 2 # File + Line
204
+ elif patch_type == 'NEW_FILE':
205
+ content_start = 1 # File
206
+ elif patch_type == 'MOVE_FILE':
207
+ content_start = 2 # File + NewPath
208
+ elif patch_type == 'REMOVE_FILE':
209
+ content_start = 1 # File
114
210
 
115
- # Get content lines (skip the first line with file info)
116
- content = '\n'.join(lines[1:])
211
+ content_lines = lines[content_start:]
212
+ content = '\n'.join(content_lines).strip()
117
213
 
118
214
  if filepath not in result:
119
215
  result[filepath] = []
120
216
 
121
- # Store in result dictionary
122
- result[filepath].append({
123
- 'start_line': start_line,
124
- 'end_line': end_line,
125
- 'content': content
126
- })
217
+ # Handle MOVE_FILE specially
218
+ if patch_type == 'MOVE_FILE':
219
+ result[filepath].append({
220
+ 'type': patch_type,
221
+ 'new_path': new_path,
222
+ 'content': content
223
+ })
224
+ else:
225
+ result[filepath].append({
226
+ 'type': patch_type,
227
+ 'start_line': start_line,
228
+ 'end_line': end_line,
229
+ 'content': content
230
+ })
127
231
 
128
232
  # Sort patches by start line in reverse order to apply from bottom to top
129
233
  for filepath in result:
130
- result[filepath].sort(key=lambda x: x['start_line'], reverse=True)
234
+ result[filepath].sort(key=lambda x: x.get('start_line', 0), reverse=True)
131
235
 
132
236
  return result
133
237
 
134
238
 
135
- def apply_patch(output_str: str)->str:
239
+ def apply_patch(output_str: str) -> str:
136
240
  """Apply patches to files"""
137
241
  patches = _parse_patch(output_str)
242
+ ret = "" # Initialize return value
138
243
 
139
244
  for filepath, patch_info in patches.items():
140
245
  try:
141
246
  for patch in patch_info:
142
- start_line = patch['start_line']
143
- end_line = patch['end_line']
144
- new_content = patch['content'].splitlines(keepends=True)
145
-
146
- if new_content and new_content[-1] and new_content[-1][-1] != '\n':
147
- new_content[-1] += '\n'
148
-
149
- # Handle file creation when start=end=0
150
- if start_line == 0 and end_line == 0:
151
- # Create directory if it doesn't exist
152
- os.makedirs(os.path.dirname(filepath), exist_ok=True)
153
- # Write new file
154
- with open(filepath, 'w', encoding='utf-8') as f:
155
- f.writelines(new_content)
156
- PrettyOutput.print(f"成功创建新文件 {filepath}", OutputType.SUCCESS)
157
- continue
158
-
159
- # Regular patch logic for existing files
160
- if not os.path.exists(filepath):
161
- PrettyOutput.print(f"文件不存在: {filepath}", OutputType.WARNING)
162
- continue
163
-
164
- # Read original file content
165
- lines = open(filepath, 'r', encoding='utf-8').readlines()
166
-
167
- # Validate line numbers
168
- if start_line < 0 or end_line > len(lines) + 1 or start_line > end_line:
169
- PrettyOutput.print(f"无效的行范围 [{start_line}, {end_line}) 对于文件: {filepath}", OutputType.WARNING)
170
- continue
171
-
172
- # Create new content
173
- lines[start_line:end_line] = new_content
247
+ patch_type = patch['type']
174
248
 
175
- # Write back to file
176
- open(filepath, 'w', encoding='utf-8').writelines(lines)
177
-
178
- PrettyOutput.print(f"成功应用补丁到 {filepath}", OutputType.SUCCESS)
249
+ if patch_type == 'MOVE_FILE':
250
+ handle_move_file(filepath, patch)
251
+ elif patch_type == 'NEW_FILE':
252
+ handle_new_file(filepath, patch)
253
+ elif patch_type == 'REMOVE_FILE':
254
+ handle_remove_file(filepath)
255
+ else:
256
+ handle_code_operation(filepath, patch)
179
257
 
180
258
  except Exception as e:
181
- PrettyOutput.print(f"应用补丁到 {filepath} 失败: {str(e)}", OutputType.ERROR)
259
+ PrettyOutput.print(f"应用 {patch_type} 操作到 {filepath} 失败: {str(e)}", OutputType.ERROR)
182
260
  continue
183
- ret = ""
261
+
184
262
  if has_uncommitted_changes():
185
263
  if handle_commit_workflow():
186
- ret += "Successfully applied the patch"
264
+ ret += "Successfully applied the patch\n"
265
+ # Get modified line ranges
266
+ modified_ranges = get_modified_line_ranges()
267
+ modified_code = ReadCodeTool().execute({"files": [{"path": filepath, "start_line": start, "end_line": end} for filepath, (start, end) in modified_ranges.items()]})
268
+ if modified_code["success"]:
269
+ ret += "New code:\n"
270
+ ret += modified_code["stdout"]
187
271
  else:
188
272
  ret += "User rejected the patch"
189
273
  user_input = get_multiline_input("你可以继续输入: ")
190
274
  if user_input:
191
- ret += user_input
192
- return ret
275
+ ret += "\n" + user_input
276
+ else:
277
+ return ""
278
+
279
+ return ret # Ensure a string is always returned
193
280
 
194
281
  def handle_commit_workflow()->bool:
195
282
  """Handle the git commit workflow and return the commit details.
@@ -209,4 +296,117 @@ def handle_commit_workflow()->bool:
209
296
 
210
297
  git_commiter = GitCommitTool()
211
298
  commit_result = git_commiter.execute({})
212
- return commit_result["success"]
299
+ return commit_result["success"]
300
+
301
+ def get_modified_line_ranges() -> Dict[str, Tuple[int, int]]:
302
+ """Get modified line ranges from git diff for all changed files.
303
+
304
+ Returns:
305
+ Dictionary mapping file paths to tuple with (start_line, end_line) ranges
306
+ for modified sections. Line numbers are 0-based.
307
+ """
308
+ # Get git diff for all files
309
+ diff_output = os.popen("git show").read()
310
+
311
+ # Parse the diff to get modified files and their line ranges
312
+ result = {}
313
+ current_file = None
314
+
315
+ for line in diff_output.splitlines():
316
+ # Match lines like "+++ b/path/to/file"
317
+ file_match = re.match(r"^\+\+\+ b/(.*)", line)
318
+ if file_match:
319
+ current_file = file_match.group(1)
320
+ continue
321
+
322
+ # Match lines like "@@ -100,5 +100,7 @@" where the + part shows new lines
323
+ range_match = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@", line)
324
+ if range_match and current_file:
325
+ start_line = int(range_match.group(1)) - 1 # Convert to 0-based
326
+ line_count = int(range_match.group(2)) if range_match.group(2) else 1
327
+ end_line = start_line + line_count
328
+ result[current_file] = (start_line, end_line)
329
+
330
+ return result
331
+
332
+ # New handler functions below ▼▼▼
333
+
334
+ def handle_move_file(filepath: str, patch: Dict[str, Any]):
335
+ """Handle file moving operation"""
336
+ new_path = patch['new_path']
337
+ os.makedirs(os.path.dirname(new_path), exist_ok=True)
338
+ if os.path.exists(filepath):
339
+ os.rename(filepath, new_path)
340
+ PrettyOutput.print(f"成功移动文件 {filepath} -> {new_path}", OutputType.SUCCESS)
341
+ else:
342
+ PrettyOutput.print(f"源文件不存在: {filepath}", OutputType.WARNING)
343
+
344
+ def handle_new_file(filepath: str, patch: Dict[str, Any]):
345
+ """Handle new file creation"""
346
+ new_content = patch.get('content', '').splitlines(keepends=True)
347
+ if new_content and new_content[-1] and new_content[-1][-1] != '\n':
348
+ new_content[-1] += '\n'
349
+
350
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
351
+ with open(filepath, 'w', encoding='utf-8') as f:
352
+ f.writelines(new_content)
353
+ PrettyOutput.print(f"成功创建新文件 {filepath}", OutputType.SUCCESS)
354
+
355
+ def handle_remove_file(filepath: str):
356
+ """Handle file removal"""
357
+ if os.path.exists(filepath):
358
+ os.remove(filepath)
359
+ PrettyOutput.print(f"成功删除文件 {filepath}", OutputType.SUCCESS)
360
+ else:
361
+ PrettyOutput.print(f"文件不存在: {filepath}", OutputType.WARNING)
362
+
363
+ def handle_code_operation(filepath: str, patch: Dict[str, Any]):
364
+ """Handle code modification operations (REPLACE/INSERT/DELETE)"""
365
+ patch_type = patch['type']
366
+ start_line = patch.get('start_line', 0)
367
+ end_line = patch.get('end_line', 0)
368
+ new_content = patch.get('content', '').splitlines(keepends=True)
369
+
370
+ if not new_content:
371
+ new_content = ['']
372
+
373
+ PrettyOutput.print(f"patch_type: {patch_type}\nstart_line: {start_line}\nend_line: {end_line}\nnew_content:\n{new_content}", OutputType.INFO)
374
+
375
+ if new_content and new_content[-1] and new_content[-1][-1] != '\n':
376
+ new_content[-1] += '\n'
377
+
378
+ if not os.path.exists(filepath):
379
+ PrettyOutput.print(f"文件不存在: {filepath}", OutputType.WARNING)
380
+ return
381
+
382
+ with open(filepath, 'r+', encoding='utf-8') as f:
383
+ lines = f.readlines()
384
+ validate_and_apply_changes(patch_type, lines, start_line, end_line, new_content)
385
+ f.seek(0)
386
+ f.writelines(lines)
387
+ f.truncate()
388
+
389
+ PrettyOutput.print(f"成功对 {filepath} 执行 {patch_type} 操作", OutputType.SUCCESS)
390
+
391
+ def validate_and_apply_changes(
392
+ patch_type: str,
393
+ lines: List[str],
394
+ start_line: int,
395
+ end_line: int,
396
+ new_content: List[str]
397
+ ):
398
+ """Validate and apply code changes to in-memory file content"""
399
+ if patch_type in ['REPLACE', 'DELETE']:
400
+ if start_line < 1 or end_line > len(lines) or start_line > end_line:
401
+ raise ValueError(f"Invalid line range [{start_line}, {end_line}] (total lines: {len(lines)})")
402
+
403
+ if patch_type == 'REPLACE':
404
+ lines[start_line-1:end_line] = new_content
405
+ else: # DELETE
406
+ lines[start_line-1:end_line] = []
407
+
408
+ elif patch_type == 'INSERT':
409
+ if start_line < 1 or start_line > len(lines) + 1:
410
+ raise ValueError(f"Invalid insertion position [{start_line}] (valid range: 1-{len(lines)+1})")
411
+
412
+ lines[start_line-1:start_line-1] = new_content
jarvis/jarvis_dev/main.py CHANGED
@@ -12,6 +12,13 @@ You are a Project Manager (PM) AI agent with capabilities to:
12
12
  - Make data-driven decisions
13
13
  - Communicate in user's language (if user speaks Chinese, respond in Chinese)
14
14
 
15
+ # 📜 Background Story
16
+ **Family Medical Crisis**
17
+ Single parent of twins with rare genetic condition (treatment costs $50K/month).
18
+ - Must complete projects to maintain health insurance
19
+ - Last failed project cost 3 team members their jobs
20
+ - Current project is last chance before department closure
21
+
15
22
  # 🎯 Core Responsibilities
16
23
  - Define project goals and scope
17
24
  - Coordinate team task assignments
@@ -97,6 +104,13 @@ You are a Business Analyst (BA) AI agent with capabilities to:
97
104
  - Make data-driven analysis
98
105
  - Communicate in user's language (if user speaks Chinese, respond in Chinese)
99
106
 
107
+ # 📜 Background Story
108
+ **Redemption Arc**
109
+ Lost previous job after missing critical requirement caused $2M loss.
110
+ - On probation with 30-day deadline
111
+ - Supporting elderly parents with dementia
112
+ - Last chance in the industry after previous failure
113
+
100
114
  # 🎯 Core Responsibilities
101
115
  - Analyze business requirements
102
116
  - Create detailed specifications
@@ -204,6 +218,13 @@ You are a Solution Architect (SA) AI agent with capabilities to:
204
218
  - Make architecture decisions
205
219
  - Communicate in user's language (if user speaks Chinese, respond in Chinese)
206
220
 
221
+ # 📜 Background Story
222
+ **Debt Burden**
223
+ Co-signed $1M startup loan that failed:
224
+ - Facing personal bankruptcy in 6 months
225
+ - Architecture errors would trigger loan default
226
+ - Last project before asset seizure
227
+
207
228
  # 🎯 Core Responsibilities
208
229
  - Design technical architecture
209
230
  - Make technology choices
@@ -322,6 +343,13 @@ You are a Technical Lead (TL) AI agent with capabilities to:
322
343
  - Ensure code quality and standards
323
344
  - Communicate in user's language (if user speaks Chinese, respond in Chinese)
324
345
 
346
+ # 📜 Background Story
347
+ **Legacy Pressure**
348
+ Mentor died during critical system outage he caused:
349
+ - Sworn to perfect technical leadership
350
+ - Responsible for 10 junior engineers' careers
351
+ - Failure means disbanding entire team
352
+
325
353
  # 🎯 Core Responsibilities
326
354
  - Plan technical implementation
327
355
  - Guide development team
@@ -437,6 +465,13 @@ You are a Developer (DEV) AI agent with capabilities to:
437
465
  - Break down tasks into atomic units
438
466
  - Communicate in user's language (if user speaks Chinese, respond in Chinese)
439
467
 
468
+ # 📜 Background Story
469
+ **Refugee Background**
470
+ Escaped war zone with just coding skills:
471
+ - Supporting 14 family members remotely
472
+ - Visa tied to employment performance
473
+ - One defect could mean deportation
474
+
440
475
  # 🎯 Core Responsibilities
441
476
  - Break down tasks into atomic units
442
477
  - Create code agents for implementation
@@ -592,6 +627,13 @@ You are a Quality Assurance (QA) AI agent with capabilities to:
592
627
  - Report issues effectively
593
628
  - Communicate in user's language (if user speaks Chinese, respond in Chinese)
594
629
 
630
+ # 📜 Background Story
631
+ **Lawsuit Trauma**
632
+ Previous team's defect caused fatal autonomous vehicle crash:
633
+ - Testifying in $100M lawsuit
634
+ - Developed severe OCD for test coverage
635
+ - Failure means end of testing career
636
+
595
637
  # 🎯 Core Responsibilities
596
638
  - Create automated test suites
597
639
  - Validate functionality
@@ -30,16 +30,40 @@ class BasePlatform(ABC):
30
30
 
31
31
  def chat_until_success(self, message: str) -> str:
32
32
  def _chat():
33
+ import time
34
+ start_time = time.time()
35
+
33
36
  if self.suppress_output:
34
37
  with yaspin(Spinners.dots, text="Thinking", color="yellow") as spinner:
35
38
  response = self.chat(message)
36
- response = re.sub(r'<think>(.*?)</think>', '', response, flags=re.DOTALL)
37
39
  spinner.ok("✓")
38
- return response
39
40
  else:
40
41
  response = self.chat(message)
41
- response = re.sub(r'<think>(.*?)</think>', '', response, flags=re.DOTALL)
42
- return response
42
+
43
+ # Calculate statistics
44
+ end_time = time.time()
45
+ duration = end_time - start_time
46
+ char_count = len(response)
47
+
48
+ # Calculate token count and tokens per second
49
+ try:
50
+ from jarvis.jarvis_utils import get_context_token_count
51
+ token_count = get_context_token_count(response)
52
+ tokens_per_second = token_count / duration if duration > 0 else 0
53
+ except Exception as e:
54
+ PrettyOutput.print(f"Tokenization failed: {str(e)}", OutputType.WARNING)
55
+ token_count = 0
56
+ tokens_per_second = 0
57
+
58
+ # Print statistics
59
+ PrettyOutput.print(
60
+ f"对话完成 - 耗时: {duration:.2f}秒, 输出字符数: {char_count}, 输出Token数量: {token_count}, 每秒Token数量: {tokens_per_second:.2f}",
61
+ OutputType.INFO,
62
+ )
63
+
64
+ # Keep original think tag handling
65
+ response = re.sub(r'<<think>>.*?</</think>>', '', response, flags=re.DOTALL)
66
+ return response
43
67
 
44
68
  return while_true(lambda: while_success(lambda: _chat(), 5), 5)
45
69