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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +458 -152
- jarvis/jarvis_agent/agent_manager.py +17 -13
- jarvis/jarvis_agent/builtin_input_handler.py +2 -6
- jarvis/jarvis_agent/config_editor.py +2 -7
- jarvis/jarvis_agent/event_bus.py +82 -12
- jarvis/jarvis_agent/file_context_handler.py +329 -0
- jarvis/jarvis_agent/file_methodology_manager.py +3 -4
- jarvis/jarvis_agent/jarvis.py +628 -55
- jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
- jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
- jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
- jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
- jarvis/jarvis_agent/language_support_info.py +486 -0
- jarvis/jarvis_agent/main.py +34 -10
- jarvis/jarvis_agent/memory_manager.py +7 -16
- jarvis/jarvis_agent/methodology_share_manager.py +10 -16
- jarvis/jarvis_agent/prompt_manager.py +1 -1
- jarvis/jarvis_agent/prompts.py +193 -171
- jarvis/jarvis_agent/protocols.py +8 -12
- jarvis/jarvis_agent/run_loop.py +105 -9
- jarvis/jarvis_agent/session_manager.py +2 -3
- jarvis/jarvis_agent/share_manager.py +20 -22
- jarvis/jarvis_agent/shell_input_handler.py +1 -2
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +31 -6
- jarvis/jarvis_agent/task_manager.py +11 -27
- jarvis/jarvis_agent/tool_executor.py +2 -3
- jarvis/jarvis_agent/tool_share_manager.py +12 -24
- jarvis/jarvis_agent/utils.py +5 -1
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +786 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +575 -0
- jarvis/jarvis_c2rust/collector.py +250 -0
- jarvis/jarvis_c2rust/constants.py +26 -0
- jarvis/jarvis_c2rust/library_replacer.py +1254 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1272 -0
- jarvis/jarvis_c2rust/loaders.py +207 -0
- jarvis/jarvis_c2rust/models.py +28 -0
- jarvis/jarvis_c2rust/optimizer.py +2157 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2983 -0
- jarvis/jarvis_c2rust/utils.py +385 -0
- jarvis/jarvis_code_agent/build_validation_config.py +132 -0
- jarvis/jarvis_code_agent/code_agent.py +1371 -220
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +65 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +106 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +72 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +70 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +53 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +47 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +61 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +110 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +110 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +153 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +648 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +110 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +49 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +299 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +215 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +269 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +281 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +605 -0
- jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +252 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +58 -0
- jarvis/jarvis_code_agent/lint.py +501 -8
- jarvis/jarvis_code_agent/utils.py +141 -0
- jarvis/jarvis_code_analysis/code_review.py +493 -584
- jarvis/jarvis_data/config_schema.json +128 -12
- jarvis/jarvis_git_squash/main.py +4 -5
- jarvis/jarvis_git_utils/git_commiter.py +82 -75
- jarvis/jarvis_mcp/sse_mcp_client.py +22 -29
- jarvis/jarvis_mcp/stdio_mcp_client.py +12 -13
- jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
- jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
- jarvis/jarvis_methodology/main.py +32 -48
- jarvis/jarvis_multi_agent/__init__.py +287 -55
- jarvis/jarvis_multi_agent/main.py +36 -4
- jarvis/jarvis_platform/base.py +524 -202
- jarvis/jarvis_platform/human.py +7 -8
- jarvis/jarvis_platform/kimi.py +30 -36
- jarvis/jarvis_platform/openai.py +88 -25
- jarvis/jarvis_platform/registry.py +26 -10
- jarvis/jarvis_platform/tongyi.py +24 -25
- jarvis/jarvis_platform/yuanbao.py +32 -43
- jarvis/jarvis_platform_manager/main.py +66 -77
- jarvis/jarvis_platform_manager/service.py +8 -13
- jarvis/jarvis_rag/cli.py +53 -55
- jarvis/jarvis_rag/embedding_manager.py +13 -18
- jarvis/jarvis_rag/llm_interface.py +8 -9
- jarvis/jarvis_rag/query_rewriter.py +10 -21
- jarvis/jarvis_rag/rag_pipeline.py +24 -27
- jarvis/jarvis_rag/reranker.py +4 -5
- jarvis/jarvis_rag/retriever.py +28 -30
- jarvis/jarvis_sec/__init__.py +305 -0
- jarvis/jarvis_sec/agents.py +143 -0
- jarvis/jarvis_sec/analysis.py +276 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +139 -0
- jarvis/jarvis_sec/clustering.py +1439 -0
- jarvis/jarvis_sec/file_manager.py +427 -0
- jarvis/jarvis_sec/parsers.py +73 -0
- jarvis/jarvis_sec/prompts.py +268 -0
- jarvis/jarvis_sec/report.py +336 -0
- jarvis/jarvis_sec/review.py +453 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/utils.py +499 -0
- jarvis/jarvis_sec/verification.py +848 -0
- jarvis/jarvis_sec/workflow.py +226 -0
- jarvis/jarvis_smart_shell/main.py +38 -87
- jarvis/jarvis_stats/cli.py +2 -2
- jarvis/jarvis_stats/stats.py +8 -8
- jarvis/jarvis_stats/storage.py +15 -21
- jarvis/jarvis_stats/visualizer.py +1 -1
- jarvis/jarvis_tools/clear_memory.py +3 -20
- jarvis/jarvis_tools/cli/main.py +21 -23
- jarvis/jarvis_tools/edit_file.py +1019 -132
- jarvis/jarvis_tools/execute_script.py +83 -25
- jarvis/jarvis_tools/file_analyzer.py +6 -9
- jarvis/jarvis_tools/generate_new_tool.py +14 -21
- jarvis/jarvis_tools/lsp_client.py +1552 -0
- jarvis/jarvis_tools/methodology.py +2 -3
- jarvis/jarvis_tools/read_code.py +1736 -35
- jarvis/jarvis_tools/read_symbols.py +140 -0
- jarvis/jarvis_tools/read_webpage.py +12 -13
- jarvis/jarvis_tools/registry.py +427 -200
- jarvis/jarvis_tools/retrieve_memory.py +20 -19
- jarvis/jarvis_tools/rewrite_file.py +72 -158
- jarvis/jarvis_tools/save_memory.py +3 -15
- jarvis/jarvis_tools/search_web.py +18 -18
- jarvis/jarvis_tools/sub_agent.py +36 -43
- jarvis/jarvis_tools/sub_code_agent.py +25 -26
- jarvis/jarvis_tools/virtual_tty.py +55 -33
- jarvis/jarvis_utils/clipboard.py +7 -10
- jarvis/jarvis_utils/config.py +232 -45
- jarvis/jarvis_utils/embedding.py +8 -5
- jarvis/jarvis_utils/fzf.py +8 -8
- jarvis/jarvis_utils/git_utils.py +225 -36
- jarvis/jarvis_utils/globals.py +3 -3
- jarvis/jarvis_utils/http.py +1 -1
- jarvis/jarvis_utils/input.py +99 -48
- jarvis/jarvis_utils/jsonnet_compat.py +465 -0
- jarvis/jarvis_utils/methodology.py +52 -48
- jarvis/jarvis_utils/utils.py +819 -491
- jarvis_ai_assistant-0.7.6.dist-info/METADATA +600 -0
- jarvis_ai_assistant-0.7.6.dist-info/RECORD +218 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/entry_points.txt +4 -0
- jarvis/jarvis_agent/config.py +0 -92
- jarvis/jarvis_agent/edit_file_handler.py +0 -296
- jarvis/jarvis_platform/ai8.py +0 -332
- jarvis/jarvis_tools/ask_user.py +0 -54
- jarvis_ai_assistant-0.3.30.dist-info/METADATA +0 -381
- jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/top_level.txt +0 -0
jarvis/jarvis_platform/base.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
"""
|
|
35
|
+
"""大语言模型基类"""
|
|
30
36
|
|
|
31
37
|
def __init__(self):
|
|
32
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
62
|
+
"""设置模型名称"""
|
|
55
63
|
raise NotImplementedError("set_model_name is not implemented")
|
|
56
64
|
|
|
57
65
|
def reset(self):
|
|
58
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
82
|
+
"""检查平台是否支持文件上传"""
|
|
73
83
|
return False
|
|
74
84
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
448
|
+
"""平台名称"""
|
|
272
449
|
raise NotImplementedError("platform_name is not implemented")
|
|
273
450
|
|
|
274
451
|
@abstractmethod
|
|
275
452
|
def delete_chat(self) -> bool:
|
|
276
|
-
"""
|
|
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
|
-
"""
|
|
458
|
+
"""保存对话会话到文件。
|
|
282
459
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
460
|
+
注意:
|
|
461
|
+
此方法的实现应在成功保存后将`self._saved`设置为True,
|
|
462
|
+
以防止在对象销毁时删除会话。
|
|
286
463
|
|
|
287
|
-
|
|
288
|
-
file_path:
|
|
464
|
+
参数:
|
|
465
|
+
file_path: 保存会话文件的路径。
|
|
289
466
|
|
|
290
|
-
|
|
291
|
-
True
|
|
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
|
-
"""
|
|
474
|
+
"""从文件恢复对话会话。
|
|
298
475
|
|
|
299
|
-
|
|
300
|
-
file_path:
|
|
476
|
+
参数:
|
|
477
|
+
file_path: 要恢复会话文件的路径。
|
|
301
478
|
|
|
302
|
-
|
|
303
|
-
True
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
502
|
+
"""获取环境变量默认值"""
|
|
326
503
|
return {}
|
|
327
504
|
|
|
328
505
|
@classmethod
|
|
329
506
|
def get_env_config_guide(cls) -> Dict[str, str]:
|
|
330
|
-
"""
|
|
507
|
+
"""获取环境变量配置指南
|
|
331
508
|
|
|
332
|
-
|
|
333
|
-
Dict[str, str]:
|
|
509
|
+
返回:
|
|
510
|
+
Dict[str, str]: 将环境变量键名映射到其配置说明的字典
|
|
334
511
|
"""
|
|
335
512
|
return {}
|
|
336
513
|
|
|
337
514
|
def set_suppress_output(self, suppress: bool):
|
|
338
|
-
"""
|
|
515
|
+
"""设置是否抑制输出"""
|
|
339
516
|
self.suppress_output = suppress
|
|
340
517
|
|
|
341
518
|
def set_model_group(self, model_group: Optional[str]):
|
|
342
|
-
"""
|
|
519
|
+
"""设置模型组"""
|
|
343
520
|
self.model_group = model_group
|
|
344
521
|
|
|
345
522
|
def set_web(self, web: bool):
|
|
346
|
-
"""
|
|
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
|
-
"""
|
|
673
|
+
"""检查平台是否支持网页功能"""
|
|
352
674
|
return False
|