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
@@ -3,6 +3,7 @@ from typing import Any, Tuple
3
3
 
4
4
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
5
5
  from jarvis.jarvis_utils.input import user_confirm
6
+ from jarvis.jarvis_agent.utils import join_prompts
6
7
 
7
8
 
8
9
  def shell_input_handler(user_input: str, agent: Any) -> Tuple[str, bool]:
@@ -11,9 +12,24 @@ def shell_input_handler(user_input: str, agent: Any) -> Tuple[str, bool]:
11
12
  if len(cmdline) == 0:
12
13
  return user_input, False
13
14
  else:
14
- script = "\n".join([c[1:] for c in cmdline])
15
+ marker = "# JARVIS-NOCONFIRM"
16
+
17
+ def _clean(line: str) -> str:
18
+ s = line[1:] # remove leading '!'
19
+ # strip no-confirm marker if present
20
+ idx = s.find(marker)
21
+ if idx != -1:
22
+ s = s[:idx]
23
+ return s.rstrip()
24
+
25
+ # Build script while stripping the no-confirm marker from each line
26
+ script = "\n".join([_clean(c) for c in cmdline])
15
27
  PrettyOutput.print(script, OutputType.CODE, lang="bash")
16
- if user_confirm(f"是否要执行以上shell脚本?", default=True):
28
+
29
+ # If any line contains the no-confirm marker, skip the pre-execution confirmation
30
+ no_confirm = any(marker in c for c in cmdline)
31
+
32
+ if no_confirm or user_confirm("是否要执行以上shell脚本?", default=True):
17
33
  from jarvis.jarvis_tools.registry import ToolRegistry
18
34
 
19
35
  output = ToolRegistry().handle_tool_calls(
@@ -26,7 +42,11 @@ def shell_input_handler(user_input: str, agent: Any) -> Tuple[str, bool]:
26
42
  )
27
43
  if user_confirm("是否将执行结果反馈给Agent?", default=True):
28
44
  return (
29
- f"{user_input}\n\n用户执行以下脚本:\n{script}\n\n执行结果:\n{output}",
45
+ join_prompts([
46
+ user_input,
47
+ f"用户执行以下脚本:\n{script}",
48
+ f"执行结果:\n{output}",
49
+ ]),
30
50
  False,
31
51
  )
32
52
  return "", True
@@ -0,0 +1,295 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Web STDIO 重定向模块:
4
+ - 在 Web 模式下,将 Python 层的标准输出/错误(sys.stdout/sys.stderr)重定向到 WebSocket,通过 WebBridge 广播。
5
+ - 适用于工具或第三方库直接使用 print()/stdout/stderr 的输出,从而不经过 PrettyOutput Sink 的场景。
6
+
7
+ 注意:
8
+ - 这是进程级重定向,可能带来重复输出(PrettyOutput 已通过 Sink 广播一次,console.print 也会走到 stdout)。若需要避免重复,可在前端针对 'stdio' 类型进行独立显示或折叠。
9
+ - 对于子进程输出(subprocess),通常由调用方决定是否捕获和打印;若直接透传到父进程的 stdout/stderr,也会被此重定向捕获。
10
+
11
+ 前端消息结构(通过 WebBridge.broadcast):
12
+ { "type": "stdio", "stream": "stdout" | "stderr", "text": "..." }
13
+
14
+ 使用:
15
+ from jarvis.jarvis_agent.stdio_redirect import enable_web_stdio_redirect, disable_web_stdio_redirect
16
+ enable_web_stdio_redirect()
17
+ # ... 运行期间输出将通过 WS 广播 ...
18
+ disable_web_stdio_redirect()
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+ import threading
24
+
25
+ from jarvis.jarvis_agent.web_bridge import WebBridge
26
+
27
+
28
+ _original_stdout = sys.stdout
29
+ _original_stderr = sys.stderr
30
+ _redirect_enabled = False
31
+ _lock = threading.Lock()
32
+
33
+
34
+ class _WebStreamWrapper:
35
+ """文件类兼容包装器,将 write() 的内容通过 WebBridge 广播。"""
36
+
37
+ def __init__(self, stream_name: str) -> None:
38
+ self._stream_name = stream_name
39
+ try:
40
+ self._encoding = getattr(_original_stdout, "encoding", "utf-8")
41
+ except Exception:
42
+ self._encoding = "utf-8"
43
+
44
+ def write(self, s: object) -> int:
45
+ try:
46
+ text = s if isinstance(s, str) else str(s)
47
+ except Exception:
48
+ text = repr(s)
49
+ try:
50
+ WebBridge.instance().broadcast({
51
+ "type": "stdio",
52
+ "stream": self._stream_name,
53
+ "text": text,
54
+ })
55
+ except Exception:
56
+ # 广播异常不影响主流程
57
+ pass
58
+ # 返回写入长度以兼容部分调用方
59
+ try:
60
+ return len(text)
61
+ except Exception:
62
+ return 0
63
+
64
+ def flush(self) -> None:
65
+ # 无需实际刷新;保持接口兼容
66
+ pass
67
+
68
+ def isatty(self) -> bool:
69
+ return False
70
+
71
+ @property
72
+ def encoding(self) -> str:
73
+ return self._encoding
74
+
75
+ def writelines(self, lines) -> None:
76
+ for ln in lines:
77
+ self.write(ln)
78
+
79
+ def __getattr__(self, name: str):
80
+ # 兼容性:必要时委派到原始 stdout/stderr 的属性(尽量避免)
81
+ try:
82
+ return getattr(_original_stdout if self._stream_name == "stdout" else _original_stderr, name)
83
+ except Exception:
84
+ raise AttributeError(name)
85
+
86
+
87
+ def enable_web_stdio_redirect() -> None:
88
+ """启用全局 STDOUT/STDERR 到 WebSocket 的重定向。"""
89
+ global _redirect_enabled
90
+ with _lock:
91
+ if _redirect_enabled:
92
+ return
93
+ try:
94
+ sys.stdout = _WebStreamWrapper("stdout") # type: ignore[assignment]
95
+ sys.stderr = _WebStreamWrapper("stderr") # type: ignore[assignment]
96
+ _redirect_enabled = True
97
+ except Exception:
98
+ # 回退:保持原始输出
99
+ sys.stdout = _original_stdout
100
+ sys.stderr = _original_stderr
101
+ _redirect_enabled = False
102
+
103
+
104
+ def disable_web_stdio_redirect() -> None:
105
+ """禁用全局 STDOUT/STDERR 重定向,恢复原始输出。"""
106
+ global _redirect_enabled
107
+ with _lock:
108
+ try:
109
+ sys.stdout = _original_stdout
110
+ sys.stderr = _original_stderr
111
+ except Exception:
112
+ pass
113
+ _redirect_enabled = False
114
+
115
+
116
+ # ---------------------------
117
+ # Web STDIN 重定向(浏览器 -> 后端)
118
+ # ---------------------------
119
+ # 目的:
120
+ # - 将前端 xterm 的按键数据通过 WS 送回服务端,并作为 sys.stdin 的数据源
121
+ # - 使得 Python 层的 input()/sys.stdin.readline() 等可以从浏览器获得输入
122
+ # - 仅适用于部分交互式场景(非真正 PTY 行为),可满足基础行缓冲输入
123
+ from queue import Queue # noqa: E402
124
+
125
+
126
+ _original_stdin = sys.stdin
127
+ _stdin_enabled = False
128
+ _stdin_wrapper = None # type: ignore[assignment]
129
+
130
+
131
+ class _WebInputWrapper:
132
+ """文件类兼容包装器:作为 sys.stdin 的替身,从队列中读取浏览器送来的数据。"""
133
+
134
+ def __init__(self) -> None:
135
+ self._queue: "Queue[str]" = Queue()
136
+ self._buffer: str = ""
137
+ self._lock = threading.Lock()
138
+ try:
139
+ self._encoding = getattr(_original_stdin, "encoding", "utf-8") # type: ignore[name-defined]
140
+ except Exception:
141
+ self._encoding = "utf-8"
142
+
143
+ # 外部注入:由 WebSocket 端点调用
144
+ def feed(self, data: str) -> None:
145
+ try:
146
+ s = data if isinstance(data, str) else str(data)
147
+ except Exception:
148
+ s = repr(data)
149
+ # 将回车转换为换行,方便基于 readline 的读取
150
+ s = s.replace("\r", "\n")
151
+ self._queue.put_nowait(s)
152
+
153
+ # 基础读取:尽可能兼容常用调用
154
+ def read(self, size: int = -1) -> str:
155
+ # size < 0 表示尽可能多地读取(直到当前缓冲区内容)
156
+ if size == 0:
157
+ return ""
158
+
159
+ while True:
160
+ with self._lock:
161
+ if size > 0 and len(self._buffer) >= size:
162
+ out = self._buffer[:size]
163
+ self._buffer = self._buffer[size:]
164
+ return out
165
+ if size < 0 and self._buffer:
166
+ out = self._buffer
167
+ self._buffer = ""
168
+ return out
169
+ # 需要更多数据,阻塞等待
170
+ try:
171
+ chunk = self._queue.get(timeout=None)
172
+ except Exception:
173
+ chunk = ""
174
+ if not isinstance(chunk, str):
175
+ try:
176
+ chunk = str(chunk)
177
+ except Exception:
178
+ chunk = ""
179
+ with self._lock:
180
+ self._buffer += chunk
181
+
182
+ def readline(self, size: int = -1) -> str:
183
+ # 读取到换行符为止(包含换行),可选 size 限制
184
+ while True:
185
+ with self._lock:
186
+ idx = self._buffer.find("\n")
187
+ if idx != -1:
188
+ # 找到换行
189
+ end_index = idx + 1
190
+ if size > 0:
191
+ end_index = min(end_index, size)
192
+ out = self._buffer[:end_index]
193
+ self._buffer = self._buffer[end_index:]
194
+ return out
195
+ # 未找到换行,但如果指定了 size 且缓冲已有足够数据,则返回
196
+ if size > 0 and len(self._buffer) >= size:
197
+ out = self._buffer[:size]
198
+ self._buffer = self._buffer[size:]
199
+ return out
200
+ # 更多数据
201
+ try:
202
+ chunk = self._queue.get(timeout=None)
203
+ except Exception:
204
+ chunk = ""
205
+ if not isinstance(chunk, str):
206
+ try:
207
+ chunk = str(chunk)
208
+ except Exception:
209
+ chunk = ""
210
+ with self._lock:
211
+ self._buffer += chunk
212
+
213
+ def readlines(self, hint: int = -1):
214
+ lines = []
215
+ total = 0
216
+ while True:
217
+ ln = self.readline()
218
+ if not ln:
219
+ break
220
+ lines.append(ln)
221
+ total += len(ln)
222
+ if hint > 0 and total >= hint:
223
+ break
224
+ return lines
225
+
226
+ def writable(self) -> bool:
227
+ return False
228
+
229
+ def readable(self) -> bool:
230
+ return True
231
+
232
+ def seekable(self) -> bool:
233
+ return False
234
+
235
+ def flush(self) -> None:
236
+ pass
237
+
238
+ def isatty(self) -> bool:
239
+ # 伪装为 TTY,可改善部分库的行为(注意并非真正 PTY)
240
+ return True
241
+
242
+ @property
243
+ def encoding(self) -> str:
244
+ return self._encoding
245
+
246
+ def __getattr__(self, name: str):
247
+ # 尽量代理到原始 stdin 的属性以增强兼容性
248
+ try:
249
+ return getattr(_original_stdin, name)
250
+ except Exception:
251
+ raise AttributeError(name)
252
+
253
+
254
+ def enable_web_stdin_redirect() -> None:
255
+ """启用 Web STDIN 重定向:将 sys.stdin 替换为浏览器数据源。"""
256
+ global _stdin_enabled, _stdin_wrapper, _original_stdin
257
+ with _lock:
258
+ if _stdin_enabled:
259
+ return
260
+ try:
261
+ # 记录原始 stdin(若尚未记录)
262
+ if "_original_stdin" not in globals() or _original_stdin is None:
263
+ _original_stdin = sys.stdin # type: ignore[assignment]
264
+ _stdin_wrapper = _WebInputWrapper()
265
+ sys.stdin = _stdin_wrapper # type: ignore[assignment]
266
+ _stdin_enabled = True
267
+ except Exception:
268
+ # 回退:保持原始输入
269
+ try:
270
+ sys.stdin = _original_stdin # type: ignore[assignment]
271
+ except Exception:
272
+ pass
273
+ _stdin_enabled = False
274
+
275
+
276
+ def disable_web_stdin_redirect() -> None:
277
+ """禁用 Web STDIN 重定向,恢复原始输入。"""
278
+ global _stdin_enabled, _stdin_wrapper
279
+ with _lock:
280
+ try:
281
+ sys.stdin = _original_stdin # type: ignore[assignment]
282
+ except Exception:
283
+ pass
284
+ _stdin_wrapper = None
285
+ _stdin_enabled = False
286
+
287
+
288
+ def feed_web_stdin(data: str) -> None:
289
+ """向 Web STDIN 注入数据(由 WebSocket /stdio 端点调用)。"""
290
+ try:
291
+ if _stdin_enabled and _stdin_wrapper is not None:
292
+ _stdin_wrapper.feed(data) # type: ignore[attr-defined]
293
+ except Exception:
294
+ # 注入失败不影响主流程
295
+ pass
@@ -0,0 +1,212 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 任务分析器模块
4
+ 负责处理任务分析和方法论生成功能
5
+ """
6
+
7
+ from jarvis.jarvis_utils.globals import get_interrupt, set_interrupt
8
+
9
+ from jarvis.jarvis_agent.prompts import TASK_ANALYSIS_PROMPT
10
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
11
+ from jarvis.jarvis_agent.utils import join_prompts
12
+ from jarvis.jarvis_agent.events import BEFORE_TOOL_CALL, AFTER_TOOL_CALL, BEFORE_SUMMARY, TASK_COMPLETED
13
+
14
+
15
+ class TaskAnalyzer:
16
+ """任务分析器,负责任务分析和满意度反馈处理"""
17
+
18
+ def __init__(self, agent):
19
+ """
20
+ 初始化任务分析器
21
+
22
+ 参数:
23
+ agent: Agent实例
24
+ """
25
+ self.agent = agent
26
+ self._analysis_done = False
27
+ # 旁路集成事件订阅,失败不影响主流程
28
+ try:
29
+ self._subscribe_events()
30
+ except Exception:
31
+ pass
32
+
33
+ def analysis_task(self, satisfaction_feedback: str = ""):
34
+ """分析任务并生成方法论"""
35
+
36
+ try:
37
+ # 准备分析提示
38
+ self.agent.session.prompt = self._prepare_analysis_prompt(
39
+ satisfaction_feedback
40
+ )
41
+
42
+ if not self.agent.model:
43
+ raise RuntimeError("Model not initialized")
44
+
45
+ # 循环处理工具调用,直到没有工具调用为止
46
+ self._process_analysis_loop()
47
+
48
+ except Exception:
49
+ PrettyOutput.print("分析失败", OutputType.ERROR)
50
+ finally:
51
+ # 标记已完成一次分析,避免事件回调重复执行
52
+ self._analysis_done = True
53
+ try:
54
+ self.agent.set_user_data("__task_analysis_done__", True)
55
+ except Exception:
56
+ pass
57
+
58
+ def _prepare_analysis_prompt(self, satisfaction_feedback: str) -> str:
59
+ """准备分析提示"""
60
+ return join_prompts([TASK_ANALYSIS_PROMPT, satisfaction_feedback])
61
+
62
+ def _process_analysis_loop(self):
63
+ """处理分析循环"""
64
+ while True:
65
+ response = self.agent.model.chat_until_success(self.agent.session.prompt) # type: ignore
66
+ self.agent.session.prompt = ""
67
+
68
+ # 处理用户中断
69
+ if get_interrupt():
70
+ if not self._handle_analysis_interrupt(response):
71
+ break
72
+
73
+ # 执行工具调用(补充事件:before_tool_call/after_tool_call)
74
+ try:
75
+ self.agent.event_bus.emit(
76
+ BEFORE_TOOL_CALL,
77
+ agent=self.agent,
78
+ current_response=response,
79
+ )
80
+ except Exception:
81
+ pass
82
+ need_return, tool_prompt = self.agent._call_tools(response)
83
+ self.agent.session.prompt = tool_prompt
84
+ try:
85
+ self.agent.event_bus.emit(
86
+ AFTER_TOOL_CALL,
87
+ agent=self.agent,
88
+ current_response=response,
89
+ need_return=need_return,
90
+ tool_prompt=tool_prompt,
91
+ )
92
+ except Exception:
93
+ pass
94
+
95
+ # 如果没有工具调用或者没有新的提示,退出循环
96
+ if not self.agent.session.prompt:
97
+ break
98
+
99
+ def _handle_analysis_interrupt(self, response: str) -> bool:
100
+ """处理分析过程中的用户中断
101
+
102
+ 返回:
103
+ bool: True 继续分析,False 退出分析
104
+ """
105
+ set_interrupt(False)
106
+ user_input = self.agent._multiline_input(
107
+ "分析任务期间被中断,请输入用户干预信息:", False
108
+ )
109
+
110
+ if not user_input:
111
+ # 用户输入为空,退出分析
112
+ return False
113
+
114
+ if self._has_tool_calls(response):
115
+ self.agent.session.prompt = self._handle_interrupt_with_tool_calls(
116
+ user_input
117
+ )
118
+ else:
119
+ self.agent.session.prompt = f"被用户中断,用户补充信息为:{user_input}"
120
+
121
+ return True
122
+
123
+ def _has_tool_calls(self, response: str) -> bool:
124
+ """检查响应中是否有工具调用"""
125
+ return any(
126
+ handler.can_handle(response) for handler in self.agent.output_handler
127
+ )
128
+
129
+ def _handle_interrupt_with_tool_calls(self, user_input: str) -> str:
130
+ """处理有工具调用时的中断"""
131
+ if self.agent.confirm_callback("检测到有工具调用,是否继续处理工具调用?", True):
132
+ return join_prompts([
133
+ f"被用户中断,用户补充信息为:{user_input}",
134
+ "用户同意继续工具调用。"
135
+ ])
136
+ else:
137
+ return join_prompts([
138
+ f"被用户中断,用户补充信息为:{user_input}",
139
+ "检测到有工具调用,但被用户拒绝执行。请根据用户的补充信息重新考虑下一步操作。"
140
+ ])
141
+
142
+ def collect_satisfaction_feedback(self, auto_completed: bool) -> str:
143
+ """收集满意度反馈"""
144
+ satisfaction_feedback = ""
145
+
146
+ if not auto_completed and self.agent.use_analysis:
147
+ if self.agent.confirm_callback("您对本次任务的完成是否满意?", True):
148
+ satisfaction_feedback = "用户对本次任务的完成表示满意。"
149
+ else:
150
+ feedback = self.agent._multiline_input(
151
+ "请提供您的反馈意见(可留空直接回车):", False
152
+ )
153
+ if feedback:
154
+ satisfaction_feedback = (
155
+ f"用户对本次任务的完成不满意,反馈意见如下:\n{feedback}"
156
+ )
157
+ else:
158
+ satisfaction_feedback = (
159
+ "用户对本次任务的完成不满意,未提供具体反馈意见。"
160
+ )
161
+ elif auto_completed and self.agent.use_analysis:
162
+ # 自动完成模式下,仍然执行分析,但不收集用户反馈
163
+ satisfaction_feedback = "任务已自动完成,无需用户反馈。"
164
+
165
+ return satisfaction_feedback
166
+
167
+ # -----------------------
168
+ # 事件订阅与处理(旁路)
169
+ # -----------------------
170
+ def _subscribe_events(self) -> None:
171
+ bus = self.agent.get_event_bus() # type: ignore[attr-defined]
172
+ # 在生成总结前触发(保持与原顺序一致)
173
+ bus.subscribe(BEFORE_SUMMARY, self._on_before_summary)
174
+ # 当无需总结时,作为兜底触发分析
175
+ bus.subscribe(TASK_COMPLETED, self._on_task_completed)
176
+
177
+ def _on_before_summary(self, **payload) -> None:
178
+ if self._analysis_done:
179
+ return
180
+ # 避免与直接调用重复
181
+ try:
182
+ if bool(self.agent.get_user_data("__task_analysis_done__")):
183
+ self._analysis_done = True
184
+ return
185
+ except Exception:
186
+ pass
187
+ auto_completed = bool(payload.get("auto_completed", False))
188
+ try:
189
+ feedback = self.collect_satisfaction_feedback(auto_completed)
190
+ if getattr(self.agent, "use_analysis", False):
191
+ self.analysis_task(feedback)
192
+ except Exception:
193
+ # 忽略事件处理异常,保证主流程
194
+ self._analysis_done = True
195
+
196
+ def _on_task_completed(self, **payload) -> None:
197
+ # 当未在 before_summary 阶段执行过时,作为兜底
198
+ if self._analysis_done:
199
+ return
200
+ try:
201
+ if bool(self.agent.get_user_data("__task_analysis_done__")):
202
+ self._analysis_done = True
203
+ return
204
+ except Exception:
205
+ pass
206
+ auto_completed = bool(payload.get("auto_completed", False))
207
+ try:
208
+ feedback = self.collect_satisfaction_feedback(auto_completed)
209
+ if getattr(self.agent, "use_analysis", False):
210
+ self.analysis_task(feedback)
211
+ except Exception:
212
+ self._analysis_done = True