jarvis-ai-assistant 0.3.30__py3-none-any.whl → 0.7.6__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.
Files changed (181) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +458 -152
  3. jarvis/jarvis_agent/agent_manager.py +17 -13
  4. jarvis/jarvis_agent/builtin_input_handler.py +2 -6
  5. jarvis/jarvis_agent/config_editor.py +2 -7
  6. jarvis/jarvis_agent/event_bus.py +82 -12
  7. jarvis/jarvis_agent/file_context_handler.py +329 -0
  8. jarvis/jarvis_agent/file_methodology_manager.py +3 -4
  9. jarvis/jarvis_agent/jarvis.py +628 -55
  10. jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
  11. jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
  12. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
  13. jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
  14. jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
  15. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
  16. jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
  17. jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
  18. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
  19. jarvis/jarvis_agent/language_support_info.py +486 -0
  20. jarvis/jarvis_agent/main.py +34 -10
  21. jarvis/jarvis_agent/memory_manager.py +7 -16
  22. jarvis/jarvis_agent/methodology_share_manager.py +10 -16
  23. jarvis/jarvis_agent/prompt_manager.py +1 -1
  24. jarvis/jarvis_agent/prompts.py +193 -171
  25. jarvis/jarvis_agent/protocols.py +8 -12
  26. jarvis/jarvis_agent/run_loop.py +105 -9
  27. jarvis/jarvis_agent/session_manager.py +2 -3
  28. jarvis/jarvis_agent/share_manager.py +20 -22
  29. jarvis/jarvis_agent/shell_input_handler.py +1 -2
  30. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  31. jarvis/jarvis_agent/task_analyzer.py +31 -6
  32. jarvis/jarvis_agent/task_manager.py +11 -27
  33. jarvis/jarvis_agent/tool_executor.py +2 -3
  34. jarvis/jarvis_agent/tool_share_manager.py +12 -24
  35. jarvis/jarvis_agent/utils.py +5 -1
  36. jarvis/jarvis_agent/web_bridge.py +189 -0
  37. jarvis/jarvis_agent/web_output_sink.py +53 -0
  38. jarvis/jarvis_agent/web_server.py +786 -0
  39. jarvis/jarvis_c2rust/__init__.py +26 -0
  40. jarvis/jarvis_c2rust/cli.py +575 -0
  41. jarvis/jarvis_c2rust/collector.py +250 -0
  42. jarvis/jarvis_c2rust/constants.py +26 -0
  43. jarvis/jarvis_c2rust/library_replacer.py +1254 -0
  44. jarvis/jarvis_c2rust/llm_module_agent.py +1272 -0
  45. jarvis/jarvis_c2rust/loaders.py +207 -0
  46. jarvis/jarvis_c2rust/models.py +28 -0
  47. jarvis/jarvis_c2rust/optimizer.py +2157 -0
  48. jarvis/jarvis_c2rust/scanner.py +1681 -0
  49. jarvis/jarvis_c2rust/transpiler.py +2983 -0
  50. jarvis/jarvis_c2rust/utils.py +385 -0
  51. jarvis/jarvis_code_agent/build_validation_config.py +132 -0
  52. jarvis/jarvis_code_agent/code_agent.py +1371 -220
  53. jarvis/jarvis_code_agent/code_analyzer/__init__.py +65 -0
  54. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +106 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +74 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +72 -0
  60. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +70 -0
  61. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +53 -0
  62. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +47 -0
  63. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +61 -0
  64. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +110 -0
  65. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +154 -0
  66. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +110 -0
  67. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +153 -0
  68. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  69. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +648 -0
  70. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  71. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  72. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  73. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  74. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  75. jarvis/jarvis_code_agent/code_analyzer/language_support.py +110 -0
  76. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +49 -0
  77. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +299 -0
  78. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +215 -0
  79. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
  80. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
  81. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +269 -0
  82. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +281 -0
  83. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
  84. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +605 -0
  85. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
  86. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +252 -0
  87. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +58 -0
  88. jarvis/jarvis_code_agent/lint.py +501 -8
  89. jarvis/jarvis_code_agent/utils.py +141 -0
  90. jarvis/jarvis_code_analysis/code_review.py +493 -584
  91. jarvis/jarvis_data/config_schema.json +128 -12
  92. jarvis/jarvis_git_squash/main.py +4 -5
  93. jarvis/jarvis_git_utils/git_commiter.py +82 -75
  94. jarvis/jarvis_mcp/sse_mcp_client.py +22 -29
  95. jarvis/jarvis_mcp/stdio_mcp_client.py +12 -13
  96. jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
  97. jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
  98. jarvis/jarvis_methodology/main.py +32 -48
  99. jarvis/jarvis_multi_agent/__init__.py +287 -55
  100. jarvis/jarvis_multi_agent/main.py +36 -4
  101. jarvis/jarvis_platform/base.py +524 -202
  102. jarvis/jarvis_platform/human.py +7 -8
  103. jarvis/jarvis_platform/kimi.py +30 -36
  104. jarvis/jarvis_platform/openai.py +88 -25
  105. jarvis/jarvis_platform/registry.py +26 -10
  106. jarvis/jarvis_platform/tongyi.py +24 -25
  107. jarvis/jarvis_platform/yuanbao.py +32 -43
  108. jarvis/jarvis_platform_manager/main.py +66 -77
  109. jarvis/jarvis_platform_manager/service.py +8 -13
  110. jarvis/jarvis_rag/cli.py +53 -55
  111. jarvis/jarvis_rag/embedding_manager.py +13 -18
  112. jarvis/jarvis_rag/llm_interface.py +8 -9
  113. jarvis/jarvis_rag/query_rewriter.py +10 -21
  114. jarvis/jarvis_rag/rag_pipeline.py +24 -27
  115. jarvis/jarvis_rag/reranker.py +4 -5
  116. jarvis/jarvis_rag/retriever.py +28 -30
  117. jarvis/jarvis_sec/__init__.py +305 -0
  118. jarvis/jarvis_sec/agents.py +143 -0
  119. jarvis/jarvis_sec/analysis.py +276 -0
  120. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  121. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  122. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  123. jarvis/jarvis_sec/cli.py +139 -0
  124. jarvis/jarvis_sec/clustering.py +1439 -0
  125. jarvis/jarvis_sec/file_manager.py +427 -0
  126. jarvis/jarvis_sec/parsers.py +73 -0
  127. jarvis/jarvis_sec/prompts.py +268 -0
  128. jarvis/jarvis_sec/report.py +336 -0
  129. jarvis/jarvis_sec/review.py +453 -0
  130. jarvis/jarvis_sec/status.py +264 -0
  131. jarvis/jarvis_sec/types.py +20 -0
  132. jarvis/jarvis_sec/utils.py +499 -0
  133. jarvis/jarvis_sec/verification.py +848 -0
  134. jarvis/jarvis_sec/workflow.py +226 -0
  135. jarvis/jarvis_smart_shell/main.py +38 -87
  136. jarvis/jarvis_stats/cli.py +2 -2
  137. jarvis/jarvis_stats/stats.py +8 -8
  138. jarvis/jarvis_stats/storage.py +15 -21
  139. jarvis/jarvis_stats/visualizer.py +1 -1
  140. jarvis/jarvis_tools/clear_memory.py +3 -20
  141. jarvis/jarvis_tools/cli/main.py +21 -23
  142. jarvis/jarvis_tools/edit_file.py +1019 -132
  143. jarvis/jarvis_tools/execute_script.py +83 -25
  144. jarvis/jarvis_tools/file_analyzer.py +6 -9
  145. jarvis/jarvis_tools/generate_new_tool.py +14 -21
  146. jarvis/jarvis_tools/lsp_client.py +1552 -0
  147. jarvis/jarvis_tools/methodology.py +2 -3
  148. jarvis/jarvis_tools/read_code.py +1736 -35
  149. jarvis/jarvis_tools/read_symbols.py +140 -0
  150. jarvis/jarvis_tools/read_webpage.py +12 -13
  151. jarvis/jarvis_tools/registry.py +427 -200
  152. jarvis/jarvis_tools/retrieve_memory.py +20 -19
  153. jarvis/jarvis_tools/rewrite_file.py +72 -158
  154. jarvis/jarvis_tools/save_memory.py +3 -15
  155. jarvis/jarvis_tools/search_web.py +18 -18
  156. jarvis/jarvis_tools/sub_agent.py +36 -43
  157. jarvis/jarvis_tools/sub_code_agent.py +25 -26
  158. jarvis/jarvis_tools/virtual_tty.py +55 -33
  159. jarvis/jarvis_utils/clipboard.py +7 -10
  160. jarvis/jarvis_utils/config.py +232 -45
  161. jarvis/jarvis_utils/embedding.py +8 -5
  162. jarvis/jarvis_utils/fzf.py +8 -8
  163. jarvis/jarvis_utils/git_utils.py +225 -36
  164. jarvis/jarvis_utils/globals.py +3 -3
  165. jarvis/jarvis_utils/http.py +1 -1
  166. jarvis/jarvis_utils/input.py +99 -48
  167. jarvis/jarvis_utils/jsonnet_compat.py +465 -0
  168. jarvis/jarvis_utils/methodology.py +52 -48
  169. jarvis/jarvis_utils/utils.py +819 -491
  170. jarvis_ai_assistant-0.7.6.dist-info/METADATA +600 -0
  171. jarvis_ai_assistant-0.7.6.dist-info/RECORD +218 -0
  172. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/entry_points.txt +4 -0
  173. jarvis/jarvis_agent/config.py +0 -92
  174. jarvis/jarvis_agent/edit_file_handler.py +0 -296
  175. jarvis/jarvis_platform/ai8.py +0 -332
  176. jarvis/jarvis_tools/ask_user.py +0 -54
  177. jarvis_ai_assistant-0.3.30.dist-info/METADATA +0 -381
  178. jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
  179. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/WHEEL +0 -0
  180. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/licenses/LICENSE +0 -0
  181. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import re
3
+ import os
4
+ from datetime import datetime
3
5
  from abc import ABC, abstractmethod
4
6
  from types import TracebackType
5
7
  from typing import Dict, Generator, List, Optional, Tuple, Type
@@ -13,30 +15,36 @@ from rich.status import Status # type: ignore
13
15
  from rich.text import Text # type: ignore
14
16
 
15
17
  from jarvis.jarvis_utils.config import (
16
- get_max_input_token_count,
17
18
  get_pretty_output,
18
19
  is_print_prompt,
19
20
  is_immediate_abort,
21
+ is_save_session_history,
22
+ get_data_dir,
23
+ get_max_input_token_count,
24
+ get_conversation_turn_threshold,
20
25
  )
21
- from jarvis.jarvis_utils.embedding import split_text_into_chunks
22
26
  from jarvis.jarvis_utils.globals import set_in_chat, get_interrupt, console
23
- from jarvis.jarvis_utils.output import OutputType, PrettyOutput
27
+ import jarvis.jarvis_utils.globals as G
28
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput # 保留用于语法高亮
24
29
  from jarvis.jarvis_utils.tag import ct, ot
25
- from jarvis.jarvis_utils.utils import get_context_token_count, while_success, while_true
30
+ from jarvis.jarvis_utils.utils import while_success, while_true
31
+ from jarvis.jarvis_utils.embedding import get_context_token_count
26
32
 
27
33
 
28
34
  class BasePlatform(ABC):
29
- """Base class for large language models"""
35
+ """大语言模型基类"""
30
36
 
31
37
  def __init__(self):
32
- """Initialize model"""
38
+ """初始化模型"""
33
39
  self.suppress_output = True # 添加输出控制标志
34
40
  self.web = False # 添加web属性,默认false
35
41
  self._saved = False
36
42
  self.model_group: Optional[str] = None
43
+ self._session_history_file: Optional[str] = None
44
+ self._conversation_turn = 0 # 对话轮次计数器
37
45
 
38
46
  def __enter__(self) -> Self:
39
- """Enter context manager"""
47
+ """进入上下文管理器"""
40
48
  return self
41
49
 
42
50
  def __exit__(
@@ -45,22 +53,24 @@ class BasePlatform(ABC):
45
53
  exc_val: Optional[BaseException],
46
54
  exc_tb: Optional[TracebackType],
47
55
  ) -> None:
48
- """Exit context manager"""
56
+ """退出上下文管理器"""
49
57
  if not self._saved:
50
58
  self.delete_chat()
51
59
 
52
60
  @abstractmethod
53
61
  def set_model_name(self, model_name: str):
54
- """Set model name"""
62
+ """设置模型名称"""
55
63
  raise NotImplementedError("set_model_name is not implemented")
56
64
 
57
65
  def reset(self):
58
- """Reset model"""
66
+ """重置模型"""
59
67
  self.delete_chat()
68
+ self._session_history_file = None
69
+ self._conversation_turn = 0 # 重置对话轮次计数器
60
70
 
61
71
  @abstractmethod
62
72
  def chat(self, message: str) -> Generator[str, None, None]:
63
- """Execute conversation"""
73
+ """执行对话"""
64
74
  raise NotImplementedError("chat is not implemented")
65
75
 
66
76
  @abstractmethod
@@ -69,173 +79,303 @@ class BasePlatform(ABC):
69
79
 
70
80
  @abstractmethod
71
81
  def support_upload_files(self) -> bool:
72
- """Check if platform supports upload files"""
82
+ """检查平台是否支持文件上传"""
73
83
  return False
74
84
 
75
- def _chat(self, message: str):
76
- import time
77
-
78
- start_time = time.time()
79
-
80
- input_token_count = get_context_token_count(message)
81
-
82
- if input_token_count > get_max_input_token_count(self.model_group):
83
- max_chunk_size = (
84
- get_max_input_token_count(self.model_group) - 1024
85
- ) # 留出一些余量
86
- min_chunk_size = get_max_input_token_count(self.model_group) - 2048
87
- inputs = split_text_into_chunks(message, max_chunk_size, min_chunk_size)
88
- PrettyOutput.print(
89
- f"长上下文,分批提交,共{len(inputs)}部分...", OutputType.INFO
90
- )
91
- prefix_prompt = """
92
- 我将分多次提供大量内容,在我明确告诉你内容已经全部提供完毕之前,每次仅需要输出"已收到",明白请输出"开始接收输入"。
93
- """
94
- while_true(lambda: while_success(lambda: self._chat(prefix_prompt), 5), 5)
95
- submit_count = 0
96
- length = 0
97
- response = ""
98
- for input in inputs:
99
- submit_count += 1
100
- length += len(input)
101
-
102
- response += "\n"
103
- for trunk in while_true(
104
- lambda: while_success(
105
- lambda: self._chat(
106
- f"<part_content>{input}</part_content>\n\n请返回<已收到>,不需要返回其他任何内容"
107
- ),
108
- 5,
109
- ),
110
- 5,
111
- ):
112
- response += trunk
113
-
114
- PrettyOutput.print("提交完成", OutputType.SUCCESS)
115
- response += "\n" + while_true(
116
- lambda: while_success(
117
- lambda: self._chat("内容已经全部提供完毕,请根据内容继续"), 5
118
- ),
119
- 5,
120
- )
85
+ def _format_progress_bar(self, percent: float, width: int = 20) -> str:
86
+ """格式化进度条字符串
87
+
88
+ 参数:
89
+ percent: 百分比 (0-100)
90
+ width: 进度条宽度(字符数)
91
+
92
+ 返回:
93
+ str: 格式化的进度条字符串
94
+ """
95
+ # 限制百分比范围
96
+ percent = max(0, min(100, percent))
97
+
98
+ # 计算填充的字符数
99
+ filled = int(width * percent / 100)
100
+ empty = width - filled
101
+
102
+ # 根据百分比选择颜色
103
+ if percent >= 90:
104
+ color = "red"
105
+ elif percent >= 80:
106
+ color = "yellow"
121
107
  else:
122
- response = ""
123
-
124
- if not self.suppress_output:
125
- if get_pretty_output():
126
- chat_iterator = self.chat(message)
127
- first_chunk = None
128
-
129
- with Status(
130
- f"🤔 {self.name()} 正在思考中...", spinner="dots", console=console
131
- ):
132
- try:
133
- while True:
134
- first_chunk = next(chat_iterator)
135
- if first_chunk:
136
- break
137
- except StopIteration:
138
- return ""
139
-
140
- text_content = Text(overflow="fold")
141
- panel = Panel(
142
- text_content,
143
- title=f"[bold cyan]{self.name()}[/bold cyan]",
144
- subtitle="[yellow]正在回答... (按 Ctrl+C 中断)[/yellow]",
145
- border_style="bright_blue",
146
- box=box.ROUNDED,
147
- expand=True, # 允许面板自动调整大小
108
+ color = "green"
109
+
110
+ # 构建进度条:使用 █ 表示已填充,░ 表示未填充
111
+ bar = "█" * filled + "░" * empty
112
+
113
+ return f"[{color}]{bar}[/{color}]"
114
+
115
+ def _get_token_usage_info(self, current_response: str = "") -> Tuple[float, str, str]:
116
+ """获取 token 使用信息
117
+
118
+ 参数:
119
+ current_response: 当前响应内容(用于计算流式输出时的 token)
120
+
121
+ 返回:
122
+ Tuple[float, str, str]: (usage_percent, percent_color, progress_bar)
123
+ """
124
+ try:
125
+ history_tokens = self.get_used_token_count()
126
+ current_response_tokens = get_context_token_count(current_response)
127
+ total_tokens = history_tokens + current_response_tokens
128
+ max_tokens = get_max_input_token_count(self.model_group)
129
+
130
+ if max_tokens > 0:
131
+ usage_percent = (total_tokens / max_tokens) * 100
132
+ if usage_percent >= 90:
133
+ percent_color = "red"
134
+ elif usage_percent >= 80:
135
+ percent_color = "yellow"
136
+ else:
137
+ percent_color = "green"
138
+ progress_bar = self._format_progress_bar(usage_percent, width=15)
139
+ return usage_percent, percent_color, progress_bar
140
+ return 0.0, "green", ""
141
+ except Exception:
142
+ return 0.0, "green", ""
143
+
144
+ def _update_panel_subtitle_with_token(
145
+ self, panel: Panel, response: str, is_completed: bool = False, duration: float = 0.0
146
+ ) -> None:
147
+ """更新面板的 subtitle,包含 token 使用信息
148
+
149
+ 参数:
150
+ panel: 要更新的面板
151
+ response: 当前响应内容
152
+ is_completed: 是否已完成
153
+ duration: 耗时(秒)
154
+ """
155
+ from datetime import datetime
156
+
157
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
158
+ try:
159
+ usage_percent, percent_color, progress_bar = self._get_token_usage_info(response)
160
+ max_tokens = get_max_input_token_count(self.model_group)
161
+ total_tokens = self.get_used_token_count() + get_context_token_count(response)
162
+
163
+ threshold = get_conversation_turn_threshold()
164
+ if is_completed:
165
+ if max_tokens > 0 and progress_bar:
166
+ panel.subtitle = (
167
+ f"[bold green]✓ {current_time} | ({self._conversation_turn}/{threshold}) | 对话完成耗时: {duration:.2f}秒 | "
168
+ f"Token: {progress_bar} "
169
+ f"[{percent_color}]{usage_percent:.1f}% ({total_tokens}/{max_tokens})[/{percent_color}][/bold green]"
148
170
  )
149
-
150
- buffer = []
151
- buffer_count = 0
152
- with Live(panel, refresh_per_second=4, transient=False) as live:
153
- # Process first chunk
154
- response += first_chunk
155
- buffer.append(first_chunk)
156
- buffer_count += 1
157
-
158
- # Process rest of the chunks
159
- for s in chat_iterator:
160
- if not s:
161
- continue
162
- response += s # Accumulate the full response string
163
- buffer.append(s)
164
- buffer_count += 1
165
-
166
- # 积累一定量或达到最后再更新,减少闪烁
167
- if buffer_count >= 5 or s == "":
168
- # Append buffered content to the Text object
169
- text_content.append(
170
- "".join(buffer), style="bright_white"
171
- )
172
- buffer.clear()
173
- buffer_count = 0
174
-
175
- # --- Scrolling Logic ---
176
- # Calculate available height in the panel
177
- max_text_height = (
178
- console.height - 5
179
- ) # Leave space for borders/titles
180
- if max_text_height <= 0:
181
- max_text_height = 1
182
-
183
- # Get the actual number of lines the text will wrap to
184
- lines = text_content.wrap(
185
- console,
186
- console.width - 4 if console.width > 4 else 1,
187
- )
188
-
189
- # If content overflows, truncate to show only the last few lines
190
- if len(lines) > max_text_height:
191
- # Rebuild the text from the wrapped lines to ensure visual consistency
192
- # This correctly handles both wrapped long lines and explicit newlines
193
- text_content.plain = "\n".join(
194
- [line.plain for line in lines[-max_text_height:]]
195
- )
196
-
197
- panel.subtitle = (
198
- "[yellow]正在回答... (按 Ctrl+C 中断)[/yellow]"
199
- )
200
- live.update(panel)
201
-
202
- if is_immediate_abort() and get_interrupt():
203
- return response # Return the partial response immediately
204
-
205
- # Ensure any remaining content in the buffer is displayed
206
- if buffer:
207
- text_content.append(
208
- "".join(buffer), style="bright_white"
209
- )
210
-
211
- # At the end, display the entire response
212
- text_content.plain = response
213
-
214
- end_time = time.time()
215
- duration = end_time - start_time
216
- panel.subtitle = f"[bold green]✓ 对话完成耗时: {duration:.2f}秒[/bold green]"
217
- live.update(panel)
218
171
  else:
219
- # Print a clear prefix line before streaming model output (non-pretty mode)
220
- console.print(
221
- f"🤖 模型输出 - {self.name()} (按 Ctrl+C 中断)",
222
- soft_wrap=False,
172
+ panel.subtitle = f"[bold green]✓ {current_time} | ({self._conversation_turn}/{threshold}) | 对话完成耗时: {duration:.2f}秒[/bold green]"
173
+ else:
174
+ if max_tokens > 0 and progress_bar:
175
+ panel.subtitle = (
176
+ f"[yellow]{current_time} | ({self._conversation_turn}/{threshold}) | 正在回答... (按 Ctrl+C 中断) | "
177
+ f"Token: {progress_bar} "
178
+ f"[{percent_color}]{usage_percent:.1f}% ({total_tokens}/{max_tokens})[/{percent_color}][/yellow]"
223
179
  )
224
- for s in self.chat(message):
225
- console.print(s, end="")
226
- response += s
227
- if is_immediate_abort() and get_interrupt():
228
- return response
229
- console.print()
230
- end_time = time.time()
231
- duration = end_time - start_time
232
- console.print(f"✓ 对话完成耗时: {duration:.2f}秒")
180
+ else:
181
+ panel.subtitle = f"[yellow]{current_time} | ({self._conversation_turn}/{threshold}) | 正在回答... (按 Ctrl+C 中断)[/yellow]"
182
+ except Exception:
183
+ threshold = get_conversation_turn_threshold()
184
+ if is_completed:
185
+ panel.subtitle = f"[bold green]✓ {current_time} | ({self._conversation_turn}/{threshold}) | 对话完成耗时: {duration:.2f}秒[/bold green]"
233
186
  else:
234
- for s in self.chat(message):
235
- response += s
236
- if is_immediate_abort() and get_interrupt():
237
- return response
238
- # Keep original think tag handling
187
+ panel.subtitle = f"[yellow]{current_time} | ({self._conversation_turn}/{threshold}) | 正在回答... (按 Ctrl+C 中断)[/yellow]"
188
+
189
+ def _chat_with_pretty_output(self, message: str, start_time: float) -> str:
190
+ """使用 pretty output 模式进行聊天
191
+
192
+ 参数:
193
+ message: 用户消息
194
+ start_time: 开始时间
195
+
196
+ 返回:
197
+ str: 模型响应
198
+ """
199
+ import time
200
+
201
+ chat_iterator = self.chat(message)
202
+ first_chunk = None
203
+
204
+ with Status(
205
+ f"🤔 {(G.current_agent_name + ' · ') if G.current_agent_name else ''}{self.name()} 正在思考中...",
206
+ spinner="dots",
207
+ console=console,
208
+ ):
209
+ try:
210
+ while True:
211
+ first_chunk = next(chat_iterator)
212
+ if first_chunk:
213
+ break
214
+ except StopIteration:
215
+ self._append_session_history(message, "")
216
+ return ""
217
+
218
+ text_content = Text(overflow="fold")
219
+ panel = Panel(
220
+ text_content,
221
+ title=f"[bold cyan]{(G.current_agent_name + ' · ') if G.current_agent_name else ''}{self.name()}[/bold cyan]",
222
+ subtitle="[yellow]正在回答... (按 Ctrl+C 中断)[/yellow]",
223
+ border_style="bright_blue",
224
+ box=box.ROUNDED,
225
+ expand=True,
226
+ )
227
+
228
+ response = ""
229
+ last_subtitle_update_time = time.time()
230
+ subtitle_update_interval = 3 # subtitle 更新间隔(秒),减少更新频率避免重复渲染标题
231
+ update_count = 0 # 更新计数器,用于控制 subtitle 更新频率
232
+ with Live(panel, refresh_per_second=4, transient=False) as live:
233
+ def _update_panel_content(content: str, update_subtitle: bool = False):
234
+ nonlocal response, last_subtitle_update_time, update_count
235
+ text_content.append(content, style="bright_white")
236
+ update_count += 1
237
+
238
+ # Scrolling Logic - 只在内容超过一定行数时才应用滚动
239
+ max_text_height = console.height - 5
240
+ if max_text_height <= 0:
241
+ max_text_height = 1
242
+
243
+ lines = text_content.wrap(
244
+ console,
245
+ console.width - 4 if console.width > 4 else 1,
246
+ )
247
+
248
+ # 只在内容超过最大高度时才截取,减少不必要的操作
249
+ if len(lines) > max_text_height:
250
+ text_content.plain = "\n".join(
251
+ [line.plain for line in lines[-max_text_height:]]
252
+ )
253
+
254
+ # 只在需要时更新 subtitle(减少更新频率,避免重复渲染标题)
255
+ # 策略:每 10 次内容更新或每 3 秒更新一次 subtitle
256
+ current_time = time.time()
257
+ should_update_subtitle = (
258
+ update_subtitle
259
+ or update_count % 10 == 0 # 每 10 次更新一次
260
+ or (current_time - last_subtitle_update_time) >= subtitle_update_interval
261
+ )
262
+
263
+ if should_update_subtitle:
264
+ self._update_panel_subtitle_with_token(panel, response, is_completed=False)
265
+ last_subtitle_update_time = current_time
266
+
267
+ # 更新 panel(只更新内容,subtitle 更新频率已降低)
268
+ live.update(panel)
269
+
270
+ # Process first chunk
271
+ response += first_chunk
272
+ if first_chunk:
273
+ _update_panel_content(first_chunk, update_subtitle=True) # 第一次更新时更新 subtitle
274
+
275
+ # 缓存机制:降低更新频率,减少界面闪烁
276
+ buffer = ""
277
+ last_update_time = time.time()
278
+ update_interval = 1
279
+ min_buffer_size = 20
280
+
281
+ def _flush_buffer():
282
+ nonlocal buffer, last_update_time
283
+ if buffer:
284
+ _update_panel_content(buffer)
285
+ buffer = ""
286
+ last_update_time = time.time()
287
+
288
+ # Process rest of the chunks
289
+ for s in chat_iterator:
290
+ if not s:
291
+ continue
292
+ response += s
293
+ buffer += s
294
+
295
+ current_time = time.time()
296
+ should_update = (
297
+ len(buffer) >= min_buffer_size
298
+ or (current_time - last_update_time) >= update_interval
299
+ )
300
+
301
+ if should_update:
302
+ _flush_buffer()
303
+
304
+ if is_immediate_abort() and get_interrupt():
305
+ _flush_buffer()
306
+ self._append_session_history(message, response)
307
+ return response
308
+
309
+ _flush_buffer()
310
+ # 在结束前,将面板内容替换为完整响应,确保最后一次渲染的 panel 显示全部内容
311
+ if response:
312
+ text_content.plain = response
313
+ # 最后更新 subtitle 和 panel
314
+ end_time = time.time()
315
+ duration = end_time - start_time
316
+ self._update_panel_subtitle_with_token(panel, response, is_completed=True, duration=duration)
317
+ # 最后更新 panel,Live 上下文退出时会自动打印(transient=False)
318
+ live.update(panel)
319
+ # 注意:不要在这里调用 console.print(),因为 Live 退出时会自动打印 panel
320
+ # Live 退出后仅添加空行分隔,不再重复打印 panel,避免内容重复
321
+ console.print()
322
+ return response
323
+
324
+ def _chat_with_simple_output(self, message: str, start_time: float) -> str:
325
+ """使用简单输出模式进行聊天
326
+
327
+ 参数:
328
+ message: 用户消息
329
+ start_time: 开始时间
330
+
331
+ 返回:
332
+ str: 模型响应
333
+ """
334
+ import time
335
+
336
+ console.print(
337
+ f"🤖 模型输出 - {(G.current_agent_name + ' · ') if G.current_agent_name else ''}{self.name()} (按 Ctrl+C 中断)",
338
+ soft_wrap=False,
339
+ )
340
+ response = ""
341
+ for s in self.chat(message):
342
+ console.print(s, end="")
343
+ response += s
344
+ if is_immediate_abort() and get_interrupt():
345
+ self._append_session_history(message, response)
346
+ return response
347
+ console.print()
348
+ end_time = time.time()
349
+ duration = end_time - start_time
350
+ console.print(f"✓ 对话完成耗时: {duration:.2f}秒")
351
+ return response
352
+
353
+ def _chat_with_suppressed_output(self, message: str) -> str:
354
+ """使用静默模式进行聊天
355
+
356
+ 参数:
357
+ message: 用户消息
358
+
359
+ 返回:
360
+ str: 模型响应
361
+ """
362
+ response = ""
363
+ for s in self.chat(message):
364
+ response += s
365
+ if is_immediate_abort() and get_interrupt():
366
+ self._append_session_history(message, response)
367
+ return response
368
+ return response
369
+
370
+ def _process_response(self, response: str) -> str:
371
+ """处理响应,移除 think 标签
372
+
373
+ 参数:
374
+ response: 原始响应
375
+
376
+ 返回:
377
+ str: 处理后的响应
378
+ """
239
379
  response = re.sub(
240
380
  ot("think") + r".*?" + ct("think"), "", response, flags=re.DOTALL
241
381
  )
@@ -244,15 +384,52 @@ class BasePlatform(ABC):
244
384
  )
245
385
  return response
246
386
 
387
+ def _chat(self, message: str):
388
+ import time
389
+
390
+ start_time = time.time()
391
+
392
+ # 当输入为空白字符串时,打印警告并直接返回空字符串
393
+ if message.strip() == "":
394
+ print("⚠️ 输入为空白字符串,已忽略本次请求")
395
+ return ""
396
+
397
+ # 检查并截断消息以避免超出剩余token限制
398
+ message = self._truncate_message_if_needed(message)
399
+
400
+ # 根据输出模式选择不同的处理方式
401
+ if not self.suppress_output:
402
+ if get_pretty_output():
403
+ response = self._chat_with_pretty_output(message, start_time)
404
+ else:
405
+ response = self._chat_with_simple_output(message, start_time)
406
+ else:
407
+ response = self._chat_with_suppressed_output(message)
408
+
409
+ # 处理响应并保存会话历史
410
+ response = self._process_response(response)
411
+ self._append_session_history(message, response)
412
+ # 增加对话轮次计数
413
+ self._conversation_turn += 1
414
+ return response
415
+
247
416
  def chat_until_success(self, message: str) -> str:
248
- """Chat with model until successful response"""
417
+ """与模型对话直到成功响应。"""
249
418
  try:
250
419
  set_in_chat(True)
251
420
  if not self.suppress_output and is_print_prompt():
252
- PrettyOutput.print(f"{message}", OutputType.USER)
253
- result: str = while_true(
254
- lambda: while_success(lambda: self._chat(message), 5), 5
421
+ PrettyOutput.print(f"{message}", OutputType.USER) # 保留用于语法高亮
422
+
423
+ result: str = ""
424
+ result = while_true(
425
+ lambda: while_success(lambda: self._chat(message))
255
426
  )
427
+
428
+ # Check if result is empty or False (retry exhausted)
429
+ # Convert False to empty string for type safety
430
+ if result is False or result == "":
431
+ raise ValueError("返回结果为空")
432
+
256
433
  from jarvis.jarvis_utils.globals import set_last_message
257
434
 
258
435
  set_last_message(result)
@@ -262,91 +439,236 @@ class BasePlatform(ABC):
262
439
 
263
440
  @abstractmethod
264
441
  def name(self) -> str:
265
- """Model name"""
442
+ """模型名称"""
266
443
  raise NotImplementedError("name is not implemented")
267
444
 
268
445
  @classmethod
269
446
  @abstractmethod
270
447
  def platform_name(cls) -> str:
271
- """Platform name"""
448
+ """平台名称"""
272
449
  raise NotImplementedError("platform_name is not implemented")
273
450
 
274
451
  @abstractmethod
275
452
  def delete_chat(self) -> bool:
276
- """Delete chat"""
453
+ """删除对话"""
277
454
  raise NotImplementedError("delete_chat is not implemented")
278
455
 
279
456
  @abstractmethod
280
457
  def save(self, file_path: str) -> bool:
281
- """Save chat session to a file.
458
+ """保存对话会话到文件。
282
459
 
283
- Note:
284
- Implementations of this method should set `self._saved = True` upon successful saving
285
- to prevent the session from being deleted on object destruction.
460
+ 注意:
461
+ 此方法的实现应在成功保存后将`self._saved`设置为True
462
+ 以防止在对象销毁时删除会话。
286
463
 
287
- Args:
288
- file_path: The path to save the session file.
464
+ 参数:
465
+ file_path: 保存会话文件的路径。
289
466
 
290
- Returns:
291
- True if saving is successful, False otherwise.
467
+ 返回:
468
+ 如果保存成功返回True,否则返回False
292
469
  """
293
470
  raise NotImplementedError("save is not implemented")
294
471
 
295
472
  @abstractmethod
296
473
  def restore(self, file_path: str) -> bool:
297
- """Restore chat session from a file.
474
+ """从文件恢复对话会话。
298
475
 
299
- Args:
300
- file_path: The path to restore the session file from.
476
+ 参数:
477
+ file_path: 要恢复会话文件的路径。
301
478
 
302
- Returns:
303
- True if restoring is successful, False otherwise.
479
+ 返回:
480
+ 如果恢复成功返回True,否则返回False
304
481
  """
305
482
  raise NotImplementedError("restore is not implemented")
306
483
 
307
484
  @abstractmethod
308
485
  def set_system_prompt(self, message: str):
309
- """Set system message"""
486
+ """设置系统消息"""
310
487
  raise NotImplementedError("set_system_prompt is not implemented")
311
488
 
312
489
  @abstractmethod
313
490
  def get_model_list(self) -> List[Tuple[str, str]]:
314
- """Get model list"""
491
+ """获取模型列表"""
315
492
  raise NotImplementedError("get_model_list is not implemented")
316
493
 
317
494
  @classmethod
318
495
  @abstractmethod
319
496
  def get_required_env_keys(cls) -> List[str]:
320
- """Get required env keys"""
497
+ """获取必需的环境变量键"""
321
498
  raise NotImplementedError("get_required_env_keys is not implemented")
322
499
 
323
500
  @classmethod
324
501
  def get_env_defaults(cls) -> Dict[str, str]:
325
- """Get env default values"""
502
+ """获取环境变量默认值"""
326
503
  return {}
327
504
 
328
505
  @classmethod
329
506
  def get_env_config_guide(cls) -> Dict[str, str]:
330
- """Get environment variable configuration guide
507
+ """获取环境变量配置指南
331
508
 
332
- Returns:
333
- Dict[str, str]: A dictionary mapping env key names to their configuration instructions
509
+ 返回:
510
+ Dict[str, str]: 将环境变量键名映射到其配置说明的字典
334
511
  """
335
512
  return {}
336
513
 
337
514
  def set_suppress_output(self, suppress: bool):
338
- """Set whether to suppress output"""
515
+ """设置是否抑制输出"""
339
516
  self.suppress_output = suppress
340
517
 
341
518
  def set_model_group(self, model_group: Optional[str]):
342
- """Set model group"""
519
+ """设置模型组"""
343
520
  self.model_group = model_group
344
521
 
345
522
  def set_web(self, web: bool):
346
- """Set web flag"""
523
+ """设置网页标志"""
347
524
  self.web = web
348
525
 
526
+ def _append_session_history(self, user_input: str, model_output: str) -> None:
527
+ """
528
+ Append the user input and model output to a session history file if enabled.
529
+ The file name is generated on first save and reused until reset.
530
+ """
531
+ try:
532
+ if not is_save_session_history():
533
+ return
534
+
535
+ if self._session_history_file is None:
536
+ # Ensure session history directory exists under data directory
537
+ data_dir = get_data_dir()
538
+ session_dir = os.path.join(data_dir, "session_history")
539
+ os.makedirs(session_dir, exist_ok=True)
540
+
541
+ # Build a safe filename including platform, model and timestamp
542
+ try:
543
+ platform_name = type(self).platform_name()
544
+ except Exception:
545
+ platform_name = "unknown_platform"
546
+
547
+ try:
548
+ model_name = self.name()
549
+ except Exception:
550
+ model_name = "unknown_model"
551
+
552
+ safe_platform = re.sub(r"[^\w\-\.]+", "_", str(platform_name))
553
+ safe_model = re.sub(r"[^\w\-\.]+", "_", str(model_name))
554
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
555
+
556
+ self._session_history_file = os.path.join(
557
+ session_dir, f"session_history_{safe_platform}_{safe_model}_{ts}.log"
558
+ )
559
+
560
+ # Append record
561
+ with open(self._session_history_file, "a", encoding="utf-8", errors="ignore") as f:
562
+ ts_line = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
563
+ f.write(f"===== {ts_line} =====\n")
564
+ f.write("USER:\n")
565
+ f.write(f"{user_input}\n")
566
+ f.write("\nASSISTANT:\n")
567
+ f.write(f"{model_output}\n\n")
568
+ except Exception:
569
+ # Do not break chat flow if writing history fails
570
+ pass
571
+
572
+ def get_conversation_history(self) -> List[Dict[str, str]]:
573
+ """获取当前对话历史
574
+
575
+ 返回:
576
+ List[Dict[str, str]]: 对话历史列表,每个元素包含 role 和 content
577
+
578
+ 注意:
579
+ 默认实现检查是否有 messages 属性,子类可以重写此方法以提供自定义实现
580
+ """
581
+ if hasattr(self, "messages"):
582
+ return getattr(self, "messages", [])
583
+ return []
584
+
585
+ def get_used_token_count(self) -> int:
586
+ """计算当前对话历史使用的token数量
587
+
588
+ 返回:
589
+ int: 当前对话历史使用的token数量
590
+ """
591
+ history = self.get_conversation_history()
592
+ if not history:
593
+ return 0
594
+
595
+ total_tokens = 0
596
+ for message in history:
597
+ content = message.get("content", "")
598
+ if content:
599
+ total_tokens += get_context_token_count(content)
600
+
601
+ return total_tokens
602
+
603
+ def get_remaining_token_count(self) -> int:
604
+ """获取剩余可用的token数量
605
+
606
+ 返回:
607
+ int: 剩余可用的token数量(输入窗口限制 - 当前使用的token数量)
608
+ """
609
+ max_tokens = get_max_input_token_count(self.model_group)
610
+ used_tokens = self.get_used_token_count()
611
+ remaining = max_tokens - used_tokens
612
+ return max(0, remaining) # 确保返回值不为负数
613
+
614
+ def _truncate_message_if_needed(self, message: str) -> str:
615
+ """如果消息超出剩余token限制,则截断消息
616
+
617
+ 参数:
618
+ message: 原始消息
619
+
620
+ 返回:
621
+ str: 截断后的消息(如果不需要截断则返回原消息)
622
+ """
623
+ try:
624
+ # 获取剩余token数量
625
+ remaining_tokens = self.get_remaining_token_count()
626
+
627
+ # 如果剩余token为0或负数,返回空消息
628
+ if remaining_tokens <= 0:
629
+ print("⚠️ 警告:剩余token为0,无法发送消息")
630
+ return ""
631
+
632
+ # 计算消息的token数量
633
+ message_tokens = get_context_token_count(message)
634
+
635
+ # 如果消息token数小于等于剩余token数,不需要截断
636
+ if message_tokens <= remaining_tokens:
637
+ return message
638
+
639
+ # 需要截断:保留剩余token的80%用于消息,20%作为安全余量
640
+ target_tokens = int(remaining_tokens * 0.8)
641
+ if target_tokens <= 0:
642
+ print("⚠️ 警告:剩余token不足,无法发送消息")
643
+ return ""
644
+
645
+ # 估算字符数(1 token ≈ 4字符)
646
+ target_chars = target_tokens * 4
647
+
648
+ # 如果消息长度小于目标字符数,不需要截断(token估算可能有误差)
649
+ if len(message) <= target_chars:
650
+ return message
651
+
652
+ # 截断消息:保留前面的内容,添加截断提示
653
+ truncated_message = message[:target_chars]
654
+ # 尝试在最后一个完整句子处截断
655
+ last_period = truncated_message.rfind('.')
656
+ last_newline = truncated_message.rfind('\n')
657
+ last_break = max(last_period, last_newline)
658
+
659
+ if last_break > target_chars * 0.5: # 如果找到的断点不太靠前
660
+ truncated_message = truncated_message[:last_break + 1]
661
+
662
+ truncated_message += "\n\n... (消息过长,已截断以避免超出上下文限制)"
663
+ print(f"⚠️ 警告:消息过长({message_tokens} tokens),已截断至约 {target_tokens} tokens")
664
+
665
+ return truncated_message
666
+ except Exception as e:
667
+ # 如果截断过程中出错,返回原消息(避免阻塞对话)
668
+ print(f"⚠️ 警告:检查消息长度时出错: {e},使用原消息")
669
+ return message
670
+
349
671
  @abstractmethod
350
672
  def support_web(self) -> bool:
351
- """Check if platform supports web functionality"""
673
+ """检查平台是否支持网页功能"""
352
674
  return False