jarvis-ai-assistant 0.1.117__py3-none-any.whl → 0.1.119__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,259 @@ 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:
24
+ # 📝 Code Modification Format
25
+ Use specific blocks for different operations:
25
26
 
26
- ```
27
- <PATCH>
28
- path/to/file start,end
27
+ # 🔄 REPLACE: Modify existing code
28
+ <REPLACE>
29
+ File: path/to/file
30
+ Lines: [start,end] or [start,end)
31
+ -----
29
32
  new_content
30
- </PATCH>
31
- ```
33
+ </REPLACE>
32
34
 
33
- # 📋 Format Rules
34
- 1. File Path
35
- - Use relative path from project root
35
+ # INSERT: Add new code
36
+ <INSERT>
37
+ File: path/to/file
38
+ Line: position
39
+ -----
40
+ new_content
41
+ </INSERT>
42
+
43
+ # 🗑️ DELETE: Remove existing code
44
+ <DELETE>
45
+ File: path/to/file
46
+ Lines: [start,end] or [start,end)
47
+ </DELETE>
48
+
49
+ # 🆕 NEW_FILE: Create new file
50
+ <NEW_FILE>
51
+ File: path/to/file
52
+ -----
53
+ new_content
54
+ </NEW_FILE>
55
+
56
+ # ➡️ MOVE_FILE: Relocate a file
57
+ <MOVE_FILE>
58
+ File: path/to/source/file
59
+ NewPath: path/to/destination/file
60
+ </MOVE_FILE>
61
+
62
+ # ❌ REMOVE_FILE: Delete entire file
63
+ <REMOVE_FILE>
64
+ File: path/to/file
65
+ </REMOVE_FILE>
66
+
67
+ # 📋 Formatting Rules
68
+ 1. File Paths
69
+ - Use relative paths from project root
36
70
  - Must be exact and case-sensitive
37
71
  - Example: src/module/file.py
38
-
72
+
39
73
  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():
74
+ - Format: [start,end] (inclusive) or [start,end) (right-exclusive)
75
+ - 1-based line numbers
76
+ - Single number for INSERT
77
+ - Omit for NEW_FILE/REMOVE_FILE
78
+
79
+ 3. Content
80
+ - Use "-----" separator
81
+ - Maintain original indentation
82
+ - Follow existing code style
83
+
84
+ # 📌 Usage Examples
85
+ ## REPLACE Example (Closed Interval)
86
+ <REPLACE>
87
+ File: src/utils.py
88
+ Lines: [9,13]
89
+ -----
90
+ def updated_function():
91
+ # Replaces lines 9-13 inclusive
92
+ return "new_implementation"
93
+ </REPLACE>
94
+
95
+ ## REPLACE Example (Left-Closed Right-Open)
96
+ <REPLACE>
97
+ File: src/calculator.py
98
+ Lines: [5,8)
99
+ -----
100
+ def new_calculation():
101
+ # Replaces lines 5-7 (excludes line 8)
102
+ return 42
103
+ </REPLACE>
104
+
105
+ ## INSERT Example
106
+ <INSERT>
107
+ File: src/main.py
108
+ Line: 19
109
+ -----
110
+ # Inserted before line 19
111
+ new_feature()
112
+ </INSERT>
113
+
114
+ ## NEW_FILE Example
115
+ <NEW_FILE>
116
+ File: src/new_module.py
117
+ -----
118
+ # New file creation
119
+ def feature_entry():
73
120
  pass
74
- </PATCH>
75
- ```
121
+ </NEW_FILE>
122
+
123
+ ## DELETE Example
124
+ <DELETE>
125
+ File: src/utils.py
126
+ Lines: [9,13]
127
+ </DELETE>
76
128
 
77
- # Important Rules
78
- 1. ONE modification per patch block
79
- 2. Include necessary context
129
+ ## MOVE_FILE Example
130
+ <MOVE_FILE>
131
+ File: src/old_dir/file.py
132
+ NewPath: src/new_dir/file.py
133
+ </MOVE_FILE>
134
+
135
+ ## REMOVE_FILE Example
136
+ <REMOVE_FILE>
137
+ File: src/obsolete.py
138
+ </REMOVE_FILE>
139
+
140
+ # 🚨 Critical Requirements
141
+ 1. One change per block
142
+ 2. Use correct operation type
80
143
  3. Match existing code style
81
- 4. Preserve indentation
82
- 5. Use exact file paths
144
+ 4. Preserve indentation levels
145
+ 5. Exact file paths required
146
+ 6. Handle edge cases properly
147
+ 7. Include error handling
148
+ 8. Maintain code consistency
83
149
  """
84
150
 
85
151
 
86
152
  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
- """
153
+ """Parse patches from string with optimized format"""
95
154
  result = {}
96
- patches = re.findall(r"<PATCH>(.*?)</PATCH>", patch_str, re.DOTALL)
155
+ patches = re.findall(r"<(REPLACE|INSERT|DELETE|NEW_FILE|REMOVE_FILE|MOVE_FILE)>(.*?)</\1>", patch_str, re.DOTALL)
97
156
 
98
- for patch in patches:
157
+ for patch_type, patch in patches:
99
158
  lines = patch.strip().split('\n')
100
159
  if not lines:
101
160
  continue
102
161
 
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:
162
+ # Parse file path
163
+ file_match = re.match(r"File:\s*([^\s]+)", lines[0])
164
+ if not file_match:
109
165
  continue
110
-
111
- filepath = match.group(1).strip()
112
- start_line = int(match.group(2))
113
- end_line = int(match.group(3))
166
+ filepath = file_match.group(1).strip()
167
+
168
+ # Initialize line numbers
169
+ start_line = end_line = 0
170
+
171
+ # Parse line numbers based on operation type
172
+ if patch_type in ['REPLACE', 'DELETE']:
173
+ # 增强正则表达式兼容性
174
+ line_match = re.match(
175
+ r"^Lines:\s*\[\s*(\d+)\s*,\s*(\d+)\s*([\]\)])\s*$", # 匹配行尾
176
+ lines[1].strip(), # 去除前后空格
177
+ re.IGNORECASE
178
+ )
179
+ if line_match:
180
+ start_line = int(line_match.group(1))
181
+ end_value = int(line_match.group(2))
182
+ bracket_type = line_match.group(3).strip()
183
+
184
+ # 根据括号类型处理区间
185
+ if bracket_type == ')': # [m,n)
186
+ end_line = end_value - 1
187
+ else: # [m,n]
188
+ end_line = end_value
189
+
190
+ # 确保 end_line >= start_line
191
+ end_line = max(end_line, start_line)
192
+ else:
193
+ PrettyOutput.print(f"无法解析行号格式: {lines[1]}", OutputType.WARNING)
194
+ continue
195
+ elif patch_type == 'INSERT':
196
+ line_match = re.match(r"Line:\s*(\d+)", lines[1])
197
+ if line_match:
198
+ start_line = int(line_match.group(1)) # 1-based
199
+ end_line = start_line
200
+ elif patch_type == 'MOVE_FILE':
201
+ new_path_match = re.match(r"NewPath:\s*([^\s]+)", lines[1])
202
+ if new_path_match:
203
+ new_path = new_path_match.group(1).strip()
204
+ else:
205
+ continue
114
206
 
115
- # Get content lines (skip the first line with file info)
116
- content = '\n'.join(lines[1:])
207
+ # Get content (after separator)
208
+ separator_index = next((i for i, line in enumerate(lines) if line.strip() == "-----"), -1)
209
+ content = '\n'.join(lines[separator_index + 1:]) if separator_index != -1 else ''
117
210
 
118
211
  if filepath not in result:
119
212
  result[filepath] = []
120
213
 
121
- # Store in result dictionary
122
- result[filepath].append({
123
- 'start_line': start_line,
124
- 'end_line': end_line,
125
- 'content': content
126
- })
214
+ # Handle MOVE_FILE specially
215
+ if patch_type == 'MOVE_FILE':
216
+ result[filepath].append({
217
+ 'type': patch_type,
218
+ 'new_path': new_path,
219
+ 'content': content
220
+ })
221
+ else:
222
+ result[filepath].append({
223
+ 'type': patch_type,
224
+ 'start_line': start_line,
225
+ 'end_line': end_line,
226
+ 'content': content
227
+ })
127
228
 
128
229
  # Sort patches by start line in reverse order to apply from bottom to top
129
230
  for filepath in result:
130
- result[filepath].sort(key=lambda x: x['start_line'], reverse=True)
231
+ result[filepath].sort(key=lambda x: x.get('start_line', 0), reverse=True)
131
232
 
132
233
  return result
133
234
 
134
235
 
135
- def apply_patch(output_str: str)->str:
236
+ def apply_patch(output_str: str) -> str:
136
237
  """Apply patches to files"""
137
238
  patches = _parse_patch(output_str)
239
+ ret = "" # Initialize return value
138
240
 
139
241
  for filepath, patch_info in patches.items():
140
242
  try:
141
243
  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
244
+ patch_type = patch['type']
174
245
 
175
- # Write back to file
176
- open(filepath, 'w', encoding='utf-8').writelines(lines)
177
-
178
- PrettyOutput.print(f"成功应用补丁到 {filepath}", OutputType.SUCCESS)
246
+ if patch_type == 'MOVE_FILE':
247
+ handle_move_file(filepath, patch)
248
+ elif patch_type == 'NEW_FILE':
249
+ handle_new_file(filepath, patch)
250
+ elif patch_type == 'REMOVE_FILE':
251
+ handle_remove_file(filepath)
252
+ else:
253
+ handle_code_operation(filepath, patch)
179
254
 
180
255
  except Exception as e:
181
- PrettyOutput.print(f"应用补丁到 {filepath} 失败: {str(e)}", OutputType.ERROR)
256
+ PrettyOutput.print(f"应用 {patch_type} 操作到 {filepath} 失败: {str(e)}", OutputType.ERROR)
182
257
  continue
183
- ret = ""
258
+
184
259
  if has_uncommitted_changes():
185
260
  if handle_commit_workflow():
186
- ret += "Successfully applied the patch"
261
+ ret += "Successfully applied the patch\n"
262
+ # Get modified line ranges
263
+ modified_ranges = get_modified_line_ranges()
264
+ modified_code = ReadCodeTool().execute({"files": [{"path": filepath, "start_line": start, "end_line": end} for filepath, (start, end) in modified_ranges.items()]})
265
+ if modified_code["success"]:
266
+ ret += "New code:\n"
267
+ ret += modified_code["stdout"]
187
268
  else:
188
269
  ret += "User rejected the patch"
189
270
  user_input = get_multiline_input("你可以继续输入: ")
190
271
  if user_input:
191
- ret += user_input
192
- return ret
272
+ ret += "\n" + user_input
273
+ else:
274
+ return ""
275
+
276
+ return ret # Ensure a string is always returned
193
277
 
194
278
  def handle_commit_workflow()->bool:
195
279
  """Handle the git commit workflow and return the commit details.
@@ -209,4 +293,114 @@ def handle_commit_workflow()->bool:
209
293
 
210
294
  git_commiter = GitCommitTool()
211
295
  commit_result = git_commiter.execute({})
212
- return commit_result["success"]
296
+ return commit_result["success"]
297
+
298
+ def get_modified_line_ranges() -> Dict[str, Tuple[int, int]]:
299
+ """Get modified line ranges from git diff for all changed files.
300
+
301
+ Returns:
302
+ Dictionary mapping file paths to tuple with (start_line, end_line) ranges
303
+ for modified sections. Line numbers are 0-based.
304
+ """
305
+ # Get git diff for all files
306
+ diff_output = os.popen("git show").read()
307
+
308
+ # Parse the diff to get modified files and their line ranges
309
+ result = {}
310
+ current_file = None
311
+
312
+ for line in diff_output.splitlines():
313
+ # Match lines like "+++ b/path/to/file"
314
+ file_match = re.match(r"^\+\+\+ b/(.*)", line)
315
+ if file_match:
316
+ current_file = file_match.group(1)
317
+ continue
318
+
319
+ # Match lines like "@@ -100,5 +100,7 @@" where the + part shows new lines
320
+ range_match = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@", line)
321
+ if range_match and current_file:
322
+ start_line = int(range_match.group(1)) - 1 # Convert to 0-based
323
+ line_count = int(range_match.group(2)) if range_match.group(2) else 1
324
+ end_line = start_line + line_count
325
+ result[current_file] = (start_line, end_line)
326
+
327
+ return result
328
+
329
+ # New handler functions below ▼▼▼
330
+
331
+ def handle_move_file(filepath: str, patch: Dict[str, Any]):
332
+ """Handle file moving operation"""
333
+ new_path = patch['new_path']
334
+ os.makedirs(os.path.dirname(new_path), exist_ok=True)
335
+ if os.path.exists(filepath):
336
+ os.rename(filepath, new_path)
337
+ PrettyOutput.print(f"成功移动文件 {filepath} -> {new_path}", OutputType.SUCCESS)
338
+ else:
339
+ PrettyOutput.print(f"源文件不存在: {filepath}", OutputType.WARNING)
340
+
341
+ def handle_new_file(filepath: str, patch: Dict[str, Any]):
342
+ """Handle new file creation"""
343
+ new_content = patch.get('content', '').splitlines(keepends=True)
344
+ if new_content and new_content[-1] and new_content[-1][-1] != '\n':
345
+ new_content[-1] += '\n'
346
+
347
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
348
+ with open(filepath, 'w', encoding='utf-8') as f:
349
+ f.writelines(new_content)
350
+ PrettyOutput.print(f"成功创建新文件 {filepath}", OutputType.SUCCESS)
351
+
352
+ def handle_remove_file(filepath: str):
353
+ """Handle file removal"""
354
+ if os.path.exists(filepath):
355
+ os.remove(filepath)
356
+ PrettyOutput.print(f"成功删除文件 {filepath}", OutputType.SUCCESS)
357
+ else:
358
+ PrettyOutput.print(f"文件不存在: {filepath}", OutputType.WARNING)
359
+
360
+ def handle_code_operation(filepath: str, patch: Dict[str, Any]):
361
+ """Handle code modification operations (REPLACE/INSERT/DELETE)"""
362
+ patch_type = patch['type']
363
+ start_line = patch.get('start_line', 0)
364
+ end_line = patch.get('end_line', 0)
365
+ new_content = patch.get('content', '').splitlines(keepends=True)
366
+
367
+ PrettyOutput.print(f"patch_type: {patch_type}\nstart_line: {start_line}\nend_line: {end_line}\nnew_content:\n{new_content}", OutputType.INFO)
368
+
369
+ if new_content and new_content[-1] and new_content[-1][-1] != '\n':
370
+ new_content[-1] += '\n'
371
+
372
+ if not os.path.exists(filepath):
373
+ PrettyOutput.print(f"文件不存在: {filepath}", OutputType.WARNING)
374
+ return
375
+
376
+ with open(filepath, 'r+', encoding='utf-8') as f:
377
+ lines = f.readlines()
378
+ validate_and_apply_changes(patch_type, lines, start_line, end_line, new_content)
379
+ f.seek(0)
380
+ f.writelines(lines)
381
+ f.truncate()
382
+
383
+ PrettyOutput.print(f"成功对 {filepath} 执行 {patch_type} 操作", OutputType.SUCCESS)
384
+
385
+ def validate_and_apply_changes(
386
+ patch_type: str,
387
+ lines: List[str],
388
+ start_line: int,
389
+ end_line: int,
390
+ new_content: List[str]
391
+ ):
392
+ """Validate and apply code changes to in-memory file content"""
393
+ if patch_type in ['REPLACE', 'DELETE']:
394
+ if start_line < 1 or end_line > len(lines) or start_line > end_line:
395
+ raise ValueError(f"Invalid line range [{start_line}, {end_line}] (total lines: {len(lines)})")
396
+
397
+ if patch_type == 'REPLACE':
398
+ lines[start_line-1:end_line] = new_content
399
+ else: # DELETE
400
+ lines[start_line-1:end_line] = []
401
+
402
+ elif patch_type == 'INSERT':
403
+ if start_line < 1 or start_line > len(lines) + 1:
404
+ raise ValueError(f"Invalid insertion position [{start_line}] (valid range: 1-{len(lines)+1})")
405
+
406
+ lines[start_line-1:start_line-1] = new_content
@@ -809,7 +809,6 @@ You are a code analysis expert who provides comprehensive and accurate answers a
809
809
  - Use professional terminology
810
810
 
811
811
  ## Response Format
812
- ```yaml
813
812
  - question: [Restate the question]
814
813
  answer: |
815
814
  [Detailed technical answer with:
@@ -821,7 +820,6 @@ You are a code analysis expert who provides comprehensive and accurate answers a
821
820
  - question: [Follow-up question if needed]
822
821
  answer: |
823
822
  [Additional technical details]
824
- ```
825
823
 
826
824
  # 🔍 Analysis Context
827
825
  Question: {query}
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,27 @@ 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
+ # Print statistics
49
+ PrettyOutput.print(f"对话完成 - 耗时: {duration:.2f}秒, 输出字符数: {char_count}", OutputType.INFO)
50
+
51
+ # Keep original think tag handling
52
+ response = re.sub(r'<<think>>.*?</</think>>', '', response, flags=re.DOTALL)
53
+ return response
43
54
 
44
55
  return while_true(lambda: while_success(lambda: _chat(), 5), 5)
45
56