jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.0__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 (162) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +1143 -245
  3. jarvis/jarvis_agent/agent_manager.py +97 -0
  4. jarvis/jarvis_agent/builtin_input_handler.py +12 -10
  5. jarvis/jarvis_agent/config_editor.py +57 -0
  6. jarvis/jarvis_agent/edit_file_handler.py +392 -99
  7. jarvis/jarvis_agent/event_bus.py +48 -0
  8. jarvis/jarvis_agent/events.py +157 -0
  9. jarvis/jarvis_agent/file_context_handler.py +79 -0
  10. jarvis/jarvis_agent/file_methodology_manager.py +117 -0
  11. jarvis/jarvis_agent/jarvis.py +1117 -147
  12. jarvis/jarvis_agent/main.py +78 -34
  13. jarvis/jarvis_agent/memory_manager.py +195 -0
  14. jarvis/jarvis_agent/methodology_share_manager.py +174 -0
  15. jarvis/jarvis_agent/prompt_manager.py +82 -0
  16. jarvis/jarvis_agent/prompts.py +46 -9
  17. jarvis/jarvis_agent/protocols.py +4 -1
  18. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  19. jarvis/jarvis_agent/run_loop.py +146 -0
  20. jarvis/jarvis_agent/session_manager.py +9 -9
  21. jarvis/jarvis_agent/share_manager.py +228 -0
  22. jarvis/jarvis_agent/shell_input_handler.py +23 -3
  23. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  24. jarvis/jarvis_agent/task_analyzer.py +212 -0
  25. jarvis/jarvis_agent/task_manager.py +154 -0
  26. jarvis/jarvis_agent/task_planner.py +496 -0
  27. jarvis/jarvis_agent/tool_executor.py +8 -4
  28. jarvis/jarvis_agent/tool_share_manager.py +139 -0
  29. jarvis/jarvis_agent/user_interaction.py +42 -0
  30. jarvis/jarvis_agent/utils.py +54 -0
  31. jarvis/jarvis_agent/web_bridge.py +189 -0
  32. jarvis/jarvis_agent/web_output_sink.py +53 -0
  33. jarvis/jarvis_agent/web_server.py +751 -0
  34. jarvis/jarvis_c2rust/__init__.py +26 -0
  35. jarvis/jarvis_c2rust/cli.py +613 -0
  36. jarvis/jarvis_c2rust/collector.py +258 -0
  37. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  38. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  39. jarvis/jarvis_c2rust/optimizer.py +960 -0
  40. jarvis/jarvis_c2rust/scanner.py +1681 -0
  41. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  42. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  43. jarvis/jarvis_code_agent/code_agent.py +1605 -178
  44. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  45. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  46. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  47. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  48. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  61. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  62. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  63. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  64. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  65. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  66. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  68. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  69. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  70. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  71. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  72. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  73. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  74. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  75. jarvis/jarvis_code_agent/lint.py +275 -13
  76. jarvis/jarvis_code_agent/utils.py +142 -0
  77. jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
  78. jarvis/jarvis_code_analysis/code_review.py +583 -548
  79. jarvis/jarvis_data/config_schema.json +339 -28
  80. jarvis/jarvis_git_squash/main.py +22 -13
  81. jarvis/jarvis_git_utils/git_commiter.py +171 -55
  82. jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
  83. jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
  84. jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
  85. jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
  86. jarvis/jarvis_methodology/main.py +48 -63
  87. jarvis/jarvis_multi_agent/__init__.py +302 -43
  88. jarvis/jarvis_multi_agent/main.py +70 -24
  89. jarvis/jarvis_platform/ai8.py +40 -23
  90. jarvis/jarvis_platform/base.py +210 -49
  91. jarvis/jarvis_platform/human.py +11 -1
  92. jarvis/jarvis_platform/kimi.py +82 -76
  93. jarvis/jarvis_platform/openai.py +73 -1
  94. jarvis/jarvis_platform/registry.py +8 -15
  95. jarvis/jarvis_platform/tongyi.py +115 -101
  96. jarvis/jarvis_platform/yuanbao.py +89 -63
  97. jarvis/jarvis_platform_manager/main.py +194 -132
  98. jarvis/jarvis_platform_manager/service.py +122 -86
  99. jarvis/jarvis_rag/cli.py +156 -53
  100. jarvis/jarvis_rag/embedding_manager.py +155 -12
  101. jarvis/jarvis_rag/llm_interface.py +10 -13
  102. jarvis/jarvis_rag/query_rewriter.py +63 -12
  103. jarvis/jarvis_rag/rag_pipeline.py +222 -40
  104. jarvis/jarvis_rag/reranker.py +26 -3
  105. jarvis/jarvis_rag/retriever.py +270 -14
  106. jarvis/jarvis_sec/__init__.py +3605 -0
  107. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  108. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  109. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  110. jarvis/jarvis_sec/cli.py +116 -0
  111. jarvis/jarvis_sec/report.py +257 -0
  112. jarvis/jarvis_sec/status.py +264 -0
  113. jarvis/jarvis_sec/types.py +20 -0
  114. jarvis/jarvis_sec/workflow.py +219 -0
  115. jarvis/jarvis_smart_shell/main.py +405 -137
  116. jarvis/jarvis_stats/__init__.py +13 -0
  117. jarvis/jarvis_stats/cli.py +387 -0
  118. jarvis/jarvis_stats/stats.py +711 -0
  119. jarvis/jarvis_stats/storage.py +612 -0
  120. jarvis/jarvis_stats/visualizer.py +282 -0
  121. jarvis/jarvis_tools/ask_user.py +1 -0
  122. jarvis/jarvis_tools/base.py +18 -2
  123. jarvis/jarvis_tools/clear_memory.py +239 -0
  124. jarvis/jarvis_tools/cli/main.py +220 -144
  125. jarvis/jarvis_tools/execute_script.py +52 -12
  126. jarvis/jarvis_tools/file_analyzer.py +17 -12
  127. jarvis/jarvis_tools/generate_new_tool.py +46 -24
  128. jarvis/jarvis_tools/read_code.py +277 -18
  129. jarvis/jarvis_tools/read_symbols.py +141 -0
  130. jarvis/jarvis_tools/read_webpage.py +86 -13
  131. jarvis/jarvis_tools/registry.py +294 -90
  132. jarvis/jarvis_tools/retrieve_memory.py +227 -0
  133. jarvis/jarvis_tools/save_memory.py +194 -0
  134. jarvis/jarvis_tools/search_web.py +62 -28
  135. jarvis/jarvis_tools/sub_agent.py +205 -0
  136. jarvis/jarvis_tools/sub_code_agent.py +217 -0
  137. jarvis/jarvis_tools/virtual_tty.py +330 -62
  138. jarvis/jarvis_utils/builtin_replace_map.py +4 -5
  139. jarvis/jarvis_utils/clipboard.py +90 -0
  140. jarvis/jarvis_utils/config.py +607 -50
  141. jarvis/jarvis_utils/embedding.py +3 -0
  142. jarvis/jarvis_utils/fzf.py +57 -0
  143. jarvis/jarvis_utils/git_utils.py +251 -29
  144. jarvis/jarvis_utils/globals.py +174 -17
  145. jarvis/jarvis_utils/http.py +58 -79
  146. jarvis/jarvis_utils/input.py +899 -153
  147. jarvis/jarvis_utils/methodology.py +210 -83
  148. jarvis/jarvis_utils/output.py +220 -137
  149. jarvis/jarvis_utils/utils.py +1906 -135
  150. jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
  151. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  152. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
  153. jarvis/jarvis_git_details/main.py +0 -265
  154. jarvis/jarvis_platform/oyi.py +0 -357
  155. jarvis/jarvis_tools/edit_file.py +0 -255
  156. jarvis/jarvis_tools/rewrite_file.py +0 -195
  157. jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
  158. jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
  159. /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
  160. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  161. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  162. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -1,27 +1,61 @@
1
1
  # -*- coding: utf-8 -*-
2
- import yaml
2
+ from typing import Optional
3
+
4
+ import typer
5
+ import yaml # type: ignore[import-untyped]
6
+ import os
3
7
 
4
8
  from jarvis.jarvis_multi_agent import MultiAgent
5
9
  from jarvis.jarvis_utils.input import get_multiline_input
6
10
  from jarvis.jarvis_utils.utils import init_env
11
+ from jarvis.jarvis_utils.config import set_config
12
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
7
13
 
14
+ app = typer.Typer(help="多智能体系统启动器")
8
15
 
9
- def main():
10
- """从YAML配置文件初始化并运行多智能体系统
11
16
 
12
- Returns:
13
- 最终处理结果
14
- """
17
+ @app.command()
18
+ def cli(
19
+ config: str = typer.Option(..., "--config", "-c", help="YAML配置文件路径"),
20
+ user_input: Optional[str] = typer.Option(
21
+ None, "--input", "-i", help="用户输入(可选)"
22
+ ),
23
+ model_group: Optional[str] = typer.Option(
24
+ None, "-g", "--llm-group", help="使用的模型组,覆盖配置文件中的设置"
25
+ ),
26
+ non_interactive: bool = typer.Option(
27
+ False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
28
+ ),
29
+ ):
30
+ """从YAML配置文件初始化并运行多智能体系统"""
31
+ # CLI 标志:非交互模式(不依赖配置文件)
32
+ if non_interactive:
33
+ try:
34
+ os.environ["JARVIS_NON_INTERACTIVE"] = "true"
35
+ except Exception:
36
+ pass
37
+ # 注意:全局配置同步在 init_env 之后执行,避免被覆盖
38
+ # 非交互模式要求从命令行传入任务
39
+ if non_interactive and not (user_input and str(user_input).strip()):
40
+ PrettyOutput.print(
41
+ "非交互模式已启用:必须使用 --input 传入任务内容,因多行输入不可用。",
42
+ OutputType.ERROR,
43
+ )
44
+ raise typer.Exit(code=2)
15
45
  init_env("欢迎使用 Jarvis-MultiAgent,您的多智能体系统已准备就绪!")
16
- import argparse
17
-
18
- parser = argparse.ArgumentParser(description="多智能体系统启动器")
19
- parser.add_argument("--config", "-c", required=True, help="YAML配置文件路径")
20
- parser.add_argument("--input", "-i", help="用户输入(可选)")
21
- args = parser.parse_args()
46
+
47
+ # 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
48
+ try:
49
+ if non_interactive:
50
+ set_config("JARVIS_NON_INTERACTIVE", True)
51
+ if model_group:
52
+ set_config("JARVIS_LLM_GROUP", str(model_group))
53
+ except Exception:
54
+ # 静默忽略同步异常,不影响主流程
55
+ pass
22
56
 
23
57
  try:
24
- with open(args.config, "r", errors="ignore") as f:
58
+ with open(config, "r", errors="ignore") as f:
25
59
  config_data = yaml.safe_load(f)
26
60
 
27
61
  # 获取agents配置
@@ -32,21 +66,33 @@ def main():
32
66
  raise ValueError("必须指定main_agent作为主智能体")
33
67
 
34
68
  # 创建并运行多智能体系统
35
- multi_agent = MultiAgent(agents_config, main_agent_name)
36
- user_input = (
37
- args.input
38
- if args.input is not None
69
+ multi_agent = MultiAgent(
70
+ agents_config,
71
+ main_agent_name,
72
+ common_system_prompt=str(config_data.get("common_system_prompt", "") or "")
73
+ )
74
+ final_input = (
75
+ user_input
76
+ if user_input is not None
39
77
  else get_multiline_input("请输入内容(输入空行结束):")
40
78
  )
41
- if user_input == "":
79
+ if not final_input:
42
80
  return
43
- return multi_agent.run(user_input)
81
+ multi_agent.run(final_input)
82
+
83
+ except KeyboardInterrupt:
84
+ return
85
+ except typer.Exit:
86
+ return
87
+ except (ValueError, RuntimeError, yaml.YAMLError) as e:
88
+ PrettyOutput.print(f"错误: {str(e)}", OutputType.ERROR)
89
+ raise typer.Exit(code=1)
90
+
44
91
 
45
- except yaml.YAMLError as e:
46
- raise ValueError(f"YAML配置文件解析错误: {str(e)}")
47
- except Exception as e:
48
- raise RuntimeError(f"多智能体系统初始化失败: {str(e)}")
92
+ def main() -> None:
93
+ """Application entry point."""
94
+ app()
49
95
 
50
96
 
51
97
  if __name__ == "__main__":
52
- result = main()
98
+ main()
@@ -32,22 +32,24 @@ class AI8Model(BasePlatform):
32
32
 
33
33
  self.headers = {
34
34
  "Authorization": self.token,
35
+ "sec-ch-ua-platform": '"Windows"',
36
+ "sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
37
+ "sec-ch-ua-mobile": "?0",
35
38
  "Content-Type": "application/json",
36
39
  "Accept": "application/json, text/plain, */*",
37
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
38
- "X-APP-VERSION": "2.3.0",
40
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
41
+ "X-APP-VERSION": "2.4.2",
39
42
  "Origin": self.BASE_URL,
40
43
  "Referer": f"{self.BASE_URL}/chat?_userMenuKey=chat",
41
44
  "Sec-Fetch-Site": "same-origin",
42
45
  "Sec-Fetch-Mode": "cors",
43
46
  "Sec-Fetch-Dest": "empty",
47
+ "Accept-Language": "zh-CN,zh;q=0.9",
48
+ "Accept-Encoding": "gzip, deflate, br, zstd",
49
+ "Connection": "keep-alive",
44
50
  }
45
51
 
46
52
  self.model_name = os.getenv("JARVIS_MODEL") or "deepseek-chat"
47
- if self.model_name not in self.get_available_models():
48
- PrettyOutput.print(
49
- f"警告: 选择的模型 {self.model_name} 不在可用列表中", OutputType.WARNING
50
- )
51
53
 
52
54
  def set_model_name(self, model_name: str):
53
55
  """Set model name"""
@@ -60,7 +62,14 @@ class AI8Model(BasePlatform):
60
62
  # 1. 创建会话
61
63
  response = while_success(
62
64
  lambda: http.post(
63
- f"{self.BASE_URL}/api/chat/session", headers=self.headers, json={}
65
+ f"{self.BASE_URL}/api/chat/session",
66
+ headers=self.headers,
67
+ json={
68
+ "mcp": [],
69
+ "model": self.model_name,
70
+ "plugins": [],
71
+ "rags": [],
72
+ },
64
73
  ),
65
74
  sleep_time=5,
66
75
  )
@@ -83,6 +92,7 @@ class AI8Model(BasePlatform):
83
92
  "plugins": [],
84
93
  "localPlugins": None,
85
94
  "useAppId": 0,
95
+ "temperature": 0,
86
96
  }
87
97
 
88
98
  response = while_success(
@@ -127,33 +137,30 @@ class AI8Model(BasePlatform):
127
137
  "files": [],
128
138
  }
129
139
 
140
+ # 为流式请求构造专用的请求头,避免 'Accept' 和 'accept' 键冲突
141
+ stream_headers = self.headers.copy()
142
+ stream_headers["Accept"] = "text/event-stream" # 添加流式专用的accept头
143
+
130
144
  # 使用stream_post进行流式请求
131
145
  response_stream = while_success(
132
146
  lambda: http.stream_post(
133
147
  f"{self.BASE_URL}/api/chat/completions",
134
- headers=self.headers,
148
+ headers=stream_headers,
135
149
  json=payload,
136
150
  ),
137
151
  sleep_time=5,
138
152
  )
139
153
 
140
154
  # 处理流式响应
141
- for chunk in response_stream:
142
- if chunk:
155
+ for line in response_stream:
156
+ if line and line.startswith("data: "):
143
157
  try:
144
- line = chunk.decode("utf-8")
145
- if line.startswith("data: "):
146
- try:
147
- data = json.loads(line[6:])
148
- if data.get("type") == "string":
149
- chunk_data = data.get("data", "")
150
- if chunk_data:
151
- yield chunk_data
152
-
153
- except json.JSONDecodeError:
154
- continue
155
-
156
- except UnicodeDecodeError:
158
+ data = json.loads(line[6:])
159
+ if data.get("type") == "string":
160
+ chunk_data = data.get("data", "")
161
+ if chunk_data:
162
+ yield chunk_data
163
+ except json.JSONDecodeError:
157
164
  continue
158
165
 
159
166
  return None
@@ -313,3 +320,13 @@ class AI8Model(BasePlatform):
313
320
 
314
321
  def upload_files(self, file_list: List[str]) -> bool:
315
322
  return False
323
+
324
+ @classmethod
325
+ def get_required_env_keys(cls) -> List[str]:
326
+ """
327
+ 获取AI8平台所需的环境变量键列表
328
+
329
+ 返回:
330
+ List[str]: 环境变量键的列表
331
+ """
332
+ return ["AI8_API_KEY"]
@@ -1,20 +1,30 @@
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
- from typing import Generator, List, Tuple
6
+ from types import TracebackType
7
+ from typing import Dict, Generator, List, Optional, Tuple, Type
8
+
9
+ from typing_extensions import Self
5
10
 
6
11
  from rich import box # type: ignore
7
12
  from rich.live import Live # type: ignore
8
13
  from rich.panel import Panel # type: ignore
14
+ from rich.status import Status # type: ignore
9
15
  from rich.text import Text # type: ignore
10
16
 
11
17
  from jarvis.jarvis_utils.config import (
12
18
  get_max_input_token_count,
13
19
  get_pretty_output,
14
20
  is_print_prompt,
21
+ is_immediate_abort,
22
+ is_save_session_history,
23
+ get_data_dir,
15
24
  )
16
25
  from jarvis.jarvis_utils.embedding import split_text_into_chunks
17
- from jarvis.jarvis_utils.globals import set_in_chat
26
+ from jarvis.jarvis_utils.globals import set_in_chat, get_interrupt, console
27
+ import jarvis.jarvis_utils.globals as G
18
28
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
19
29
  from jarvis.jarvis_utils.tag import ct, ot
20
30
  from jarvis.jarvis_utils.utils import get_context_token_count, while_success, while_true
@@ -28,9 +38,20 @@ class BasePlatform(ABC):
28
38
  self.suppress_output = True # 添加输出控制标志
29
39
  self.web = False # 添加web属性,默认false
30
40
  self._saved = False
31
-
32
- def __del__(self):
33
- """Destroy model"""
41
+ self.model_group: Optional[str] = None
42
+ self._session_history_file: Optional[str] = None
43
+
44
+ def __enter__(self) -> Self:
45
+ """Enter context manager"""
46
+ return self
47
+
48
+ def __exit__(
49
+ self,
50
+ exc_type: Optional[Type[BaseException]],
51
+ exc_val: Optional[BaseException],
52
+ exc_tb: Optional[TracebackType],
53
+ ) -> None:
54
+ """Exit context manager"""
34
55
  if not self._saved:
35
56
  self.delete_chat()
36
57
 
@@ -42,6 +63,7 @@ class BasePlatform(ABC):
42
63
  def reset(self):
43
64
  """Reset model"""
44
65
  self.delete_chat()
66
+ self._session_history_file = None
45
67
 
46
68
  @abstractmethod
47
69
  def chat(self, message: str) -> Generator[str, None, None]:
@@ -62,31 +84,37 @@ class BasePlatform(ABC):
62
84
 
63
85
  start_time = time.time()
64
86
 
87
+ # 当输入为空白字符串时,打印警告并直接返回空字符串
88
+ if message.strip() == "":
89
+ PrettyOutput.print("输入为空白字符串,已忽略本次请求", OutputType.WARNING)
90
+ return ""
91
+
65
92
  input_token_count = get_context_token_count(message)
66
93
 
67
- if input_token_count > get_max_input_token_count():
68
- max_chunk_size = get_max_input_token_count() - 1024 # 留出一些余量
69
- min_chunk_size = get_max_input_token_count() - 2048
94
+ if input_token_count > get_max_input_token_count(self.model_group):
95
+ max_chunk_size = (
96
+ get_max_input_token_count(self.model_group) - 1024
97
+ ) # 留出一些余量
98
+ min_chunk_size = get_max_input_token_count(self.model_group) - 2048
70
99
  inputs = split_text_into_chunks(message, max_chunk_size, min_chunk_size)
71
- print("📤 正在提交长上下文...")
72
- prefix_prompt = f"""
100
+ PrettyOutput.print(
101
+ f"长上下文,分批提交,共{len(inputs)}部分...", OutputType.INFO
102
+ )
103
+ prefix_prompt = """
73
104
  我将分多次提供大量内容,在我明确告诉你内容已经全部提供完毕之前,每次仅需要输出"已收到",明白请输出"开始接收输入"。
74
105
  """
75
- while_true(lambda: while_success(lambda: self.chat(prefix_prompt), 5), 5)
106
+ while_true(lambda: while_success(lambda: self._chat(prefix_prompt), 5), 5)
76
107
  submit_count = 0
77
108
  length = 0
78
109
  response = ""
79
110
  for input in inputs:
80
111
  submit_count += 1
81
112
  length += len(input)
82
- print(
83
- f"📤 正在提交第{submit_count}部分(共{len(inputs)}部分({length}/{len(message)}))"
84
- )
85
113
 
86
114
  response += "\n"
87
115
  for trunk in while_true(
88
116
  lambda: while_success(
89
- lambda: self.chat(
117
+ lambda: self._chat(
90
118
  f"<part_content>{input}</part_content>\n\n请返回<已收到>,不需要返回其他任何内容"
91
119
  ),
92
120
  5,
@@ -95,10 +123,7 @@ class BasePlatform(ABC):
95
123
  ):
96
124
  response += trunk
97
125
 
98
- print(
99
- f"📤 提交第{submit_count}部分完成,当前进度:{length}/{len(message)}"
100
- )
101
- print("✅ 提交完成")
126
+ PrettyOutput.print("提交完成", OutputType.SUCCESS)
102
127
  response += "\n" + while_true(
103
128
  lambda: while_success(
104
129
  lambda: self._chat("内容已经全部提供完毕,请根据内容继续"), 5
@@ -108,48 +133,112 @@ class BasePlatform(ABC):
108
133
  else:
109
134
  response = ""
110
135
 
111
- text_content = Text()
112
- panel = Panel(
113
- text_content,
114
- title=f"[bold cyan]{self.name()}[/bold cyan]",
115
- subtitle="[dim]思考中...[/dim]",
116
- border_style="bright_blue",
117
- box=box.ROUNDED,
118
- )
119
-
120
136
  if not self.suppress_output:
121
137
  if get_pretty_output():
122
- with Live(panel, refresh_per_second=10, transient=False) as live:
123
- for s in self.chat(message):
124
- response += s
125
- text_content.append(s, style="bright_white")
126
- panel.subtitle = "[yellow]正在回答...[/yellow]"
127
- live.update(panel)
128
- end_time = time.time()
129
- duration = end_time - start_time
130
- char_count = len(response)
131
- # Calculate token count and tokens per second
138
+ chat_iterator = self.chat(message)
139
+ first_chunk = None
140
+
141
+ with Status(
142
+ f"🤔 {(G.current_agent_name + ' · ') if G.current_agent_name else ''}{self.name()} 正在思考中...",
143
+ spinner="dots",
144
+ console=console,
145
+ ):
132
146
  try:
133
- token_count = get_context_token_count(response)
134
- tokens_per_second = (
135
- token_count / duration if duration > 0 else 0
147
+ while True:
148
+ first_chunk = next(chat_iterator)
149
+ if first_chunk:
150
+ break
151
+ except StopIteration:
152
+ self._append_session_history(message, "")
153
+ return ""
154
+
155
+ text_content = Text(overflow="fold")
156
+ panel = Panel(
157
+ text_content,
158
+ title=f"[bold cyan]{(G.current_agent_name + ' · ') if G.current_agent_name else ''}{self.name()}[/bold cyan]",
159
+ subtitle="[yellow]正在回答... (按 Ctrl+C 中断)[/yellow]",
160
+ border_style="bright_blue",
161
+ box=box.ROUNDED,
162
+ expand=True, # 允许面板自动调整大小
163
+ )
164
+
165
+ with Live(panel, refresh_per_second=4, transient=False) as live:
166
+
167
+ def _update_panel_content(content: str):
168
+ text_content.append(content, style="bright_white")
169
+ # --- Scrolling Logic ---
170
+ # Calculate available height in the panel
171
+ max_text_height = (
172
+ console.height - 5
173
+ ) # Leave space for borders/titles
174
+ if max_text_height <= 0:
175
+ max_text_height = 1
176
+
177
+ # Get the actual number of lines the text will wrap to
178
+ lines = text_content.wrap(
179
+ console,
180
+ console.width - 4 if console.width > 4 else 1,
136
181
  )
137
- except Exception as e:
138
- PrettyOutput.print(
139
- f"Tokenization failed: {str(e)}", OutputType.WARNING
182
+
183
+ # If content overflows, truncate to show only the last few lines
184
+ if len(lines) > max_text_height:
185
+ # Rebuild the text from the wrapped lines to ensure visual consistency
186
+ # This correctly handles both wrapped long lines and explicit newlines
187
+ text_content.plain = "\n".join(
188
+ [line.plain for line in lines[-max_text_height:]]
189
+ )
190
+
191
+ panel.subtitle = (
192
+ "[yellow]正在回答... (按 Ctrl+C 中断)[/yellow]"
140
193
  )
141
- token_count = 0
142
- tokens_per_second = 0
143
- panel.subtitle = f"[bold green]✓ 对话完成耗时: {duration:.2f}秒, 输入字符数: {len(message)}, 输入Token数量: {input_token_count}, 输出字符数: {char_count}, 输出Token数量: {token_count}, 每秒Token数量: {tokens_per_second:.2f}[/bold green]"
194
+ live.update(panel)
195
+
196
+ # Process first chunk
197
+ response += first_chunk
198
+ if first_chunk:
199
+ _update_panel_content(first_chunk)
200
+
201
+ # Process rest of the chunks
202
+ for s in chat_iterator:
203
+ if not s:
204
+ continue
205
+ response += s # Accumulate the full response string
206
+ _update_panel_content(s)
207
+
208
+ if is_immediate_abort() and get_interrupt():
209
+ self._append_session_history(message, response)
210
+ return response # Return the partial response immediately
211
+
212
+ # At the end, display the entire response
213
+ text_content.plain = response
214
+
215
+ end_time = time.time()
216
+ duration = end_time - start_time
217
+ panel.subtitle = f"[bold green]✓ 对话完成耗时: {duration:.2f}秒[/bold green]"
144
218
  live.update(panel)
219
+ console.print()
145
220
  else:
221
+ # Print a clear prefix line before streaming model output (non-pretty mode)
222
+ console.print(
223
+ f"🤖 模型输出 - {(G.current_agent_name + ' · ') if G.current_agent_name else ''}{self.name()} (按 Ctrl+C 中断)",
224
+ soft_wrap=False,
225
+ )
146
226
  for s in self.chat(message):
147
- print(s, end="", flush=True)
227
+ console.print(s, end="")
148
228
  response += s
149
- print()
229
+ if is_immediate_abort() and get_interrupt():
230
+ self._append_session_history(message, response)
231
+ return response
232
+ console.print()
233
+ end_time = time.time()
234
+ duration = end_time - start_time
235
+ console.print(f"✓ 对话完成耗时: {duration:.2f}秒")
150
236
  else:
151
237
  for s in self.chat(message):
152
238
  response += s
239
+ if is_immediate_abort() and get_interrupt():
240
+ self._append_session_history(message, response)
241
+ return response
153
242
  # Keep original think tag handling
154
243
  response = re.sub(
155
244
  ot("think") + r".*?" + ct("think"), "", response, flags=re.DOTALL
@@ -157,6 +246,8 @@ class BasePlatform(ABC):
157
246
  response = re.sub(
158
247
  ot("thinking") + r".*?" + ct("thinking"), "", response, flags=re.DOTALL
159
248
  )
249
+ # Save session history (input and full response)
250
+ self._append_session_history(message, response)
160
251
  return response
161
252
 
162
253
  def chat_until_success(self, message: str) -> str:
@@ -229,14 +320,84 @@ class BasePlatform(ABC):
229
320
  """Get model list"""
230
321
  raise NotImplementedError("get_model_list is not implemented")
231
322
 
323
+ @classmethod
324
+ @abstractmethod
325
+ def get_required_env_keys(cls) -> List[str]:
326
+ """Get required env keys"""
327
+ raise NotImplementedError("get_required_env_keys is not implemented")
328
+
329
+ @classmethod
330
+ def get_env_defaults(cls) -> Dict[str, str]:
331
+ """Get env default values"""
332
+ return {}
333
+
334
+ @classmethod
335
+ def get_env_config_guide(cls) -> Dict[str, str]:
336
+ """Get environment variable configuration guide
337
+
338
+ Returns:
339
+ Dict[str, str]: A dictionary mapping env key names to their configuration instructions
340
+ """
341
+ return {}
342
+
232
343
  def set_suppress_output(self, suppress: bool):
233
344
  """Set whether to suppress output"""
234
345
  self.suppress_output = suppress
235
346
 
347
+ def set_model_group(self, model_group: Optional[str]):
348
+ """Set model group"""
349
+ self.model_group = model_group
350
+
236
351
  def set_web(self, web: bool):
237
352
  """Set web flag"""
238
353
  self.web = web
239
354
 
355
+ def _append_session_history(self, user_input: str, model_output: str) -> None:
356
+ """
357
+ Append the user input and model output to a session history file if enabled.
358
+ The file name is generated on first save and reused until reset.
359
+ """
360
+ try:
361
+ if not is_save_session_history():
362
+ return
363
+
364
+ if self._session_history_file is None:
365
+ # Ensure session history directory exists under data directory
366
+ data_dir = get_data_dir()
367
+ session_dir = os.path.join(data_dir, "session_history")
368
+ os.makedirs(session_dir, exist_ok=True)
369
+
370
+ # Build a safe filename including platform, model and timestamp
371
+ try:
372
+ platform_name = type(self).platform_name()
373
+ except Exception:
374
+ platform_name = "unknown_platform"
375
+
376
+ try:
377
+ model_name = self.name()
378
+ except Exception:
379
+ model_name = "unknown_model"
380
+
381
+ safe_platform = re.sub(r"[^\w\-\.]+", "_", str(platform_name))
382
+ safe_model = re.sub(r"[^\w\-\.]+", "_", str(model_name))
383
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
384
+
385
+ self._session_history_file = os.path.join(
386
+ session_dir, f"session_history_{safe_platform}_{safe_model}_{ts}.log"
387
+ )
388
+
389
+ # Append record
390
+ with open(self._session_history_file, "a", encoding="utf-8", errors="ignore") as f:
391
+ ts_line = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
392
+ f.write(f"===== {ts_line} =====\n")
393
+ f.write("USER:\n")
394
+ f.write(f"{user_input}\n")
395
+ f.write("\nASSISTANT:\n")
396
+ f.write(f"{model_output}\n\n")
397
+ except Exception:
398
+ # Do not break chat flow if writing history fails
399
+ pass
400
+
240
401
  @abstractmethod
241
402
  def support_web(self) -> bool:
242
403
  """Check if platform supports web functionality"""
@@ -10,9 +10,9 @@ import string
10
10
  from typing import Generator, List, Tuple
11
11
 
12
12
  from jarvis.jarvis_platform.base import BasePlatform
13
+ from jarvis.jarvis_utils.clipboard import copy_to_clipboard
13
14
  from jarvis.jarvis_utils.input import get_multiline_input
14
15
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
15
- from jarvis.jarvis_utils.utils import copy_to_clipboard
16
16
 
17
17
 
18
18
  class HumanPlatform(BasePlatform):
@@ -131,3 +131,13 @@ class HumanPlatform(BasePlatform):
131
131
  def support_upload_files(self) -> bool:
132
132
  """是否支持文件上传功能"""
133
133
  return False
134
+
135
+ @classmethod
136
+ def get_required_env_keys(cls) -> List[str]:
137
+ """
138
+ 获取Human平台所需的环境变量键列表
139
+
140
+ 返回:
141
+ List[str]: 环境变量键的列表
142
+ """
143
+ return []