jarvis-ai-assistant 0.3.30__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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +289 -87
- jarvis/jarvis_agent/agent_manager.py +17 -8
- jarvis/jarvis_agent/edit_file_handler.py +374 -86
- jarvis/jarvis_agent/event_bus.py +1 -1
- jarvis/jarvis_agent/file_context_handler.py +79 -0
- jarvis/jarvis_agent/jarvis.py +601 -43
- jarvis/jarvis_agent/main.py +32 -2
- jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
- jarvis/jarvis_agent/run_loop.py +38 -5
- jarvis/jarvis_agent/share_manager.py +8 -1
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +5 -2
- jarvis/jarvis_agent/task_planner.py +496 -0
- 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 +751 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +613 -0
- jarvis/jarvis_c2rust/collector.py +258 -0
- jarvis/jarvis_c2rust/library_replacer.py +1122 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
- jarvis/jarvis_c2rust/optimizer.py +960 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2325 -0
- jarvis/jarvis_code_agent/build_validation_config.py +133 -0
- jarvis/jarvis_code_agent/code_agent.py +1171 -94
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -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 +102 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -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 +89 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
- jarvis/jarvis_code_agent/lint.py +270 -8
- jarvis/jarvis_code_agent/utils.py +142 -0
- jarvis/jarvis_code_analysis/code_review.py +483 -569
- jarvis/jarvis_data/config_schema.json +97 -8
- jarvis/jarvis_git_utils/git_commiter.py +38 -26
- jarvis/jarvis_mcp/sse_mcp_client.py +2 -2
- jarvis/jarvis_mcp/stdio_mcp_client.py +1 -1
- jarvis/jarvis_memory_organizer/memory_organizer.py +1 -1
- jarvis/jarvis_multi_agent/__init__.py +239 -25
- jarvis/jarvis_multi_agent/main.py +37 -1
- jarvis/jarvis_platform/base.py +103 -51
- jarvis/jarvis_platform/openai.py +26 -1
- jarvis/jarvis_platform/yuanbao.py +1 -1
- jarvis/jarvis_platform_manager/service.py +2 -2
- jarvis/jarvis_rag/cli.py +4 -4
- jarvis/jarvis_sec/__init__.py +3605 -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 +116 -0
- jarvis/jarvis_sec/report.py +257 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/workflow.py +219 -0
- jarvis/jarvis_stats/cli.py +1 -1
- jarvis/jarvis_stats/stats.py +1 -1
- jarvis/jarvis_stats/visualizer.py +1 -1
- jarvis/jarvis_tools/cli/main.py +1 -0
- jarvis/jarvis_tools/execute_script.py +46 -9
- jarvis/jarvis_tools/generate_new_tool.py +3 -1
- jarvis/jarvis_tools/read_code.py +275 -12
- jarvis/jarvis_tools/read_symbols.py +141 -0
- jarvis/jarvis_tools/read_webpage.py +5 -3
- jarvis/jarvis_tools/registry.py +73 -35
- jarvis/jarvis_tools/search_web.py +15 -11
- jarvis/jarvis_tools/sub_agent.py +24 -42
- jarvis/jarvis_tools/sub_code_agent.py +14 -13
- jarvis/jarvis_tools/virtual_tty.py +1 -1
- jarvis/jarvis_utils/config.py +187 -35
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/git_utils.py +181 -6
- jarvis/jarvis_utils/globals.py +3 -3
- jarvis/jarvis_utils/http.py +1 -1
- jarvis/jarvis_utils/input.py +78 -2
- jarvis/jarvis_utils/methodology.py +25 -19
- jarvis/jarvis_utils/utils.py +644 -359
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/METADATA +85 -1
- jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +4 -0
- jarvis/jarvis_agent/config.py +0 -92
- jarvis/jarvis_tools/edit_file.py +0 -179
- jarvis/jarvis_tools/rewrite_file.py +0 -191
- jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
jarvis/jarvis_agent/main.py
CHANGED
|
@@ -9,6 +9,7 @@ from jarvis.jarvis_agent import Agent
|
|
|
9
9
|
from jarvis.jarvis_utils.input import get_multiline_input
|
|
10
10
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
11
11
|
from jarvis.jarvis_utils.utils import init_env
|
|
12
|
+
from jarvis.jarvis_utils.config import set_config
|
|
12
13
|
|
|
13
14
|
app = typer.Typer(help="Jarvis AI 助手")
|
|
14
15
|
|
|
@@ -46,16 +47,45 @@ def cli(
|
|
|
46
47
|
None, "-c", "--agent-definition", help="代理定义文件路径"
|
|
47
48
|
),
|
|
48
49
|
task: Optional[str] = typer.Option(None, "-T", "--task", help="初始任务内容"),
|
|
49
|
-
|
|
50
|
+
|
|
50
51
|
model_group: Optional[str] = typer.Option(
|
|
51
52
|
None, "-g", "--llm-group", help="使用的模型组,覆盖配置文件中的设置"
|
|
52
53
|
),
|
|
54
|
+
non_interactive: bool = typer.Option(
|
|
55
|
+
False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
|
|
56
|
+
),
|
|
53
57
|
):
|
|
54
58
|
"""Main entry point for Jarvis agent"""
|
|
55
|
-
#
|
|
59
|
+
# CLI 标志:非交互模式(不依赖配置文件)
|
|
60
|
+
if non_interactive:
|
|
61
|
+
try:
|
|
62
|
+
os.environ["JARVIS_NON_INTERACTIVE"] = "true"
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
try:
|
|
66
|
+
set_config("JARVIS_NON_INTERACTIVE", True)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
# 非交互模式要求从命令行传入任务
|
|
70
|
+
if non_interactive and not (task and str(task).strip()):
|
|
71
|
+
PrettyOutput.print(
|
|
72
|
+
"非交互模式已启用:必须使用 --task 传入任务内容,因多行输入不可用。",
|
|
73
|
+
OutputType.ERROR,
|
|
74
|
+
)
|
|
75
|
+
raise typer.Exit(code=2)
|
|
76
|
+
# Initialize环境
|
|
56
77
|
init_env(
|
|
57
78
|
"欢迎使用 Jarvis AI 助手,您的智能助理已准备就绪!", config_file=config_file
|
|
58
79
|
)
|
|
80
|
+
# 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
|
|
81
|
+
try:
|
|
82
|
+
if model_group:
|
|
83
|
+
set_config("JARVIS_LLM_GROUP", str(model_group))
|
|
84
|
+
if non_interactive:
|
|
85
|
+
set_config("JARVIS_NON_INTERACTIVE", True)
|
|
86
|
+
except Exception:
|
|
87
|
+
# 静默忽略同步异常,不影响主流程
|
|
88
|
+
pass
|
|
59
89
|
|
|
60
90
|
# Load configuration
|
|
61
91
|
config = load_config(agent_definition) if agent_definition else {}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, List, Tuple
|
|
5
|
+
|
|
6
|
+
from jarvis.jarvis_agent.output_handler import OutputHandler
|
|
7
|
+
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
8
|
+
from jarvis.jarvis_utils.tag import ct, ot
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RewriteFileHandler(OutputHandler):
|
|
12
|
+
"""
|
|
13
|
+
处理整文件重写指令的输出处理器。
|
|
14
|
+
|
|
15
|
+
指令格式:
|
|
16
|
+
<REWRITE file=文件路径>
|
|
17
|
+
新的文件完整内容
|
|
18
|
+
</REWRITE>
|
|
19
|
+
|
|
20
|
+
等价支持以下写法:
|
|
21
|
+
<REWRITE file=文件路径>
|
|
22
|
+
新的文件完整内容
|
|
23
|
+
</REWRITE>
|
|
24
|
+
|
|
25
|
+
说明:
|
|
26
|
+
- 该处理器用于完全重写文件内容,适用于新增文件或大范围改写
|
|
27
|
+
- 内部直接执行写入,提供失败回滚能力
|
|
28
|
+
- 支持同一响应中包含多个 REWRITE/REWRITE 块
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
# 允许 file 参数为单引号、双引号或无引号
|
|
33
|
+
self.rewrite_pattern_file = re.compile(
|
|
34
|
+
ot("REWRITE file=(?:'([^']+)'|\"([^\"]+)\"|([^>]+))")
|
|
35
|
+
+ r"\s*"
|
|
36
|
+
+ r"(.*?)"
|
|
37
|
+
+ r"\s*"
|
|
38
|
+
+ r"^"
|
|
39
|
+
+ ct("REWRITE"),
|
|
40
|
+
re.DOTALL | re.MULTILINE,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def name(self) -> str:
|
|
44
|
+
"""获取处理器名称,用于操作列表展示"""
|
|
45
|
+
return "REWRITE"
|
|
46
|
+
|
|
47
|
+
def prompt(self) -> str:
|
|
48
|
+
"""返回用户提示,描述使用方法与格式"""
|
|
49
|
+
return f"""文件重写指令格式:
|
|
50
|
+
{ot("REWRITE file=文件路径")}
|
|
51
|
+
新的文件完整内容
|
|
52
|
+
{ct("REWRITE")}
|
|
53
|
+
|
|
54
|
+
注意:
|
|
55
|
+
- {ot("REWRITE")}、{ct("REWRITE")} 必须出现在行首,否则不生效(会被忽略)
|
|
56
|
+
- 整文件重写会完全替换文件内容,如需局部修改请使用 PATCH 操作
|
|
57
|
+
- 该操作由处理器直接执行,具备失败回滚能力"""
|
|
58
|
+
|
|
59
|
+
def can_handle(self, response: str) -> bool:
|
|
60
|
+
"""判断响应中是否包含 REWRITE/REWRITE 指令"""
|
|
61
|
+
return bool(self.rewrite_pattern_file.search(response))
|
|
62
|
+
|
|
63
|
+
def handle(self, response: str, agent: Any) -> Tuple[bool, str]:
|
|
64
|
+
"""解析并执行整文件重写指令"""
|
|
65
|
+
rewrites = self._parse_rewrites(response)
|
|
66
|
+
if not rewrites:
|
|
67
|
+
return False, "未找到有效的文件重写指令"
|
|
68
|
+
|
|
69
|
+
# 记录 REWRITE 操作调用统计
|
|
70
|
+
try:
|
|
71
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
|
72
|
+
|
|
73
|
+
StatsManager.increment("rewrite_file", group="tool")
|
|
74
|
+
except Exception:
|
|
75
|
+
# 统计失败不影响主流程
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
results: List[str] = []
|
|
79
|
+
|
|
80
|
+
for file_path, content in rewrites:
|
|
81
|
+
abs_path = os.path.abspath(file_path)
|
|
82
|
+
original_content = None
|
|
83
|
+
processed = False
|
|
84
|
+
try:
|
|
85
|
+
file_exists = os.path.exists(abs_path)
|
|
86
|
+
if file_exists:
|
|
87
|
+
with open(abs_path, "r", encoding="utf-8") as rf:
|
|
88
|
+
original_content = rf.read()
|
|
89
|
+
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
|
90
|
+
with open(abs_path, "w", encoding="utf-8") as wf:
|
|
91
|
+
wf.write(content)
|
|
92
|
+
processed = True
|
|
93
|
+
results.append(f"✅ 文件 {abs_path} 重写成功")
|
|
94
|
+
# 记录成功处理的文件(使用绝对路径)
|
|
95
|
+
if agent:
|
|
96
|
+
files = agent.get_user_data("files")
|
|
97
|
+
if files:
|
|
98
|
+
if abs_path not in files:
|
|
99
|
+
files.append(abs_path)
|
|
100
|
+
else:
|
|
101
|
+
files = [abs_path]
|
|
102
|
+
agent.set_user_data("files", files)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
# 回滚已修改内容
|
|
105
|
+
try:
|
|
106
|
+
if processed:
|
|
107
|
+
if original_content is None:
|
|
108
|
+
if os.path.exists(abs_path):
|
|
109
|
+
os.remove(abs_path)
|
|
110
|
+
else:
|
|
111
|
+
with open(abs_path, "w", encoding="utf-8") as wf:
|
|
112
|
+
wf.write(original_content)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
PrettyOutput.print(f"文件重写失败: {str(e)}", OutputType.ERROR)
|
|
116
|
+
results.append(f"❌ 文件 {abs_path} 重写失败: {str(e)}")
|
|
117
|
+
|
|
118
|
+
summary = "\n".join(results)
|
|
119
|
+
# 按现有 EditFileHandler 约定,始终返回 (False, summary) 以继续主循环
|
|
120
|
+
return False, summary
|
|
121
|
+
|
|
122
|
+
def _parse_rewrites(self, response: str) -> List[Tuple[str, str]]:
|
|
123
|
+
"""
|
|
124
|
+
解析响应中的 REWRITE/REWRITE 指令块。
|
|
125
|
+
返回列表 [(file_path, content), ...],按在响应中的出现顺序排序
|
|
126
|
+
"""
|
|
127
|
+
items: List[Tuple[str, str]] = []
|
|
128
|
+
matches: List[Tuple[int, Any]] = []
|
|
129
|
+
for m in self.rewrite_pattern_file.finditer(response):
|
|
130
|
+
matches.append((m.start(), m))
|
|
131
|
+
|
|
132
|
+
# 按出现顺序排序
|
|
133
|
+
matches.sort(key=lambda x: x[0])
|
|
134
|
+
|
|
135
|
+
for _, m in matches:
|
|
136
|
+
file_path = m.group(1) or m.group(2) or m.group(3) or ""
|
|
137
|
+
file_path = file_path.strip()
|
|
138
|
+
content = m.group(4)
|
|
139
|
+
if file_path:
|
|
140
|
+
items.append((file_path, content))
|
|
141
|
+
return items
|
jarvis/jarvis_agent/run_loop.py
CHANGED
|
@@ -7,12 +7,14 @@ AgentRunLoop: 承载 Agent 的主运行循环逻辑。
|
|
|
7
7
|
- 暂不变更外部调用入口,后续在 Agent._main_loop 中委派到该类
|
|
8
8
|
- 保持与现有异常处理、工具调用、用户交互完全一致
|
|
9
9
|
"""
|
|
10
|
+
import os
|
|
10
11
|
from enum import Enum
|
|
11
12
|
from typing import Any, TYPE_CHECKING
|
|
12
13
|
|
|
13
14
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
14
15
|
from jarvis.jarvis_agent.events import BEFORE_TOOL_CALL, AFTER_TOOL_CALL
|
|
15
16
|
from jarvis.jarvis_agent.utils import join_prompts, is_auto_complete, normalize_next_action
|
|
17
|
+
from jarvis.jarvis_utils.config import get_auto_summary_rounds
|
|
16
18
|
|
|
17
19
|
if TYPE_CHECKING:
|
|
18
20
|
# 仅用于类型标注,避免运行时循环依赖
|
|
@@ -22,6 +24,14 @@ if TYPE_CHECKING:
|
|
|
22
24
|
class AgentRunLoop:
|
|
23
25
|
def __init__(self, agent: "Agent") -> None:
|
|
24
26
|
self.agent = agent
|
|
27
|
+
self.conversation_rounds = 0
|
|
28
|
+
self.tool_reminder_rounds = int(os.environ.get("JARVIS_TOOL_REMINDER_ROUNDS", 20))
|
|
29
|
+
# 基于轮次的自动总结阈值:优先使用 Agent 入参,否则回落到全局配置(默认20轮)
|
|
30
|
+
self.auto_summary_rounds = (
|
|
31
|
+
self.agent.auto_summary_rounds
|
|
32
|
+
if getattr(self.agent, "auto_summary_rounds", None) is not None
|
|
33
|
+
else get_auto_summary_rounds()
|
|
34
|
+
)
|
|
25
35
|
|
|
26
36
|
def run(self) -> Any:
|
|
27
37
|
"""主运行循环(委派到传入的 agent 实例的方法与属性)"""
|
|
@@ -29,6 +39,23 @@ class AgentRunLoop:
|
|
|
29
39
|
|
|
30
40
|
while True:
|
|
31
41
|
try:
|
|
42
|
+
self.conversation_rounds += 1
|
|
43
|
+
if self.conversation_rounds % self.tool_reminder_rounds == 0:
|
|
44
|
+
self.agent.session.addon_prompt = join_prompts(
|
|
45
|
+
[self.agent.session.addon_prompt, self.agent.get_tool_usage_prompt()]
|
|
46
|
+
)
|
|
47
|
+
# 基于轮次的自动总结判断:达到阈值后执行一次总结与历史清理
|
|
48
|
+
if self.conversation_rounds >= self.auto_summary_rounds:
|
|
49
|
+
summary_text = self.agent._summarize_and_clear_history()
|
|
50
|
+
if summary_text:
|
|
51
|
+
# 将摘要作为下一轮的附加提示加入,从而维持上下文连续性
|
|
52
|
+
self.agent.session.addon_prompt = join_prompts(
|
|
53
|
+
[self.agent.session.addon_prompt, summary_text]
|
|
54
|
+
)
|
|
55
|
+
# 重置轮次计数与对话长度计数器,开始新一轮周期
|
|
56
|
+
self.conversation_rounds = 0
|
|
57
|
+
self.agent.session.conversation_length = 0
|
|
58
|
+
|
|
32
59
|
ag = self.agent
|
|
33
60
|
|
|
34
61
|
# 更新输入处理器标志
|
|
@@ -72,12 +99,13 @@ class AgentRunLoop:
|
|
|
72
99
|
pass
|
|
73
100
|
need_return, tool_prompt = ag._call_tools(current_response)
|
|
74
101
|
|
|
75
|
-
#
|
|
76
|
-
ag.session.prompt = join_prompts([ag.session.prompt, tool_prompt])
|
|
77
|
-
|
|
102
|
+
# 如果工具要求立即返回结果(例如 SEND_MESSAGE 需要将字典返回给上层),直接返回该结果
|
|
78
103
|
if need_return:
|
|
79
|
-
return
|
|
104
|
+
return tool_prompt
|
|
80
105
|
|
|
106
|
+
# 将上一个提示和工具提示安全地拼接起来(仅当工具结果为字符串时)
|
|
107
|
+
safe_tool_prompt = tool_prompt if isinstance(tool_prompt, str) else ""
|
|
108
|
+
ag.session.prompt = join_prompts([ag.session.prompt, safe_tool_prompt])
|
|
81
109
|
|
|
82
110
|
# 广播工具调用后的事件(不影响主流程)
|
|
83
111
|
try:
|
|
@@ -97,7 +125,12 @@ class AgentRunLoop:
|
|
|
97
125
|
|
|
98
126
|
# 检查自动完成
|
|
99
127
|
if ag.auto_complete and is_auto_complete(current_response):
|
|
100
|
-
|
|
128
|
+
# 先运行_complete_task,触发记忆整理/事件等副作用,再决定返回值
|
|
129
|
+
result = ag._complete_task(auto_completed=True)
|
|
130
|
+
# 若不需要summary,则将最后一条LLM输出作为返回值
|
|
131
|
+
if not getattr(ag, "need_summary", True):
|
|
132
|
+
return current_response
|
|
133
|
+
return result
|
|
101
134
|
|
|
102
135
|
# 获取下一步用户输入
|
|
103
136
|
next_action = ag._get_next_user_action()
|
|
@@ -49,7 +49,14 @@ class ShareManager(ABC):
|
|
|
49
49
|
def __init__(self, central_repo_url: str, repo_name: str):
|
|
50
50
|
self.central_repo_url = central_repo_url
|
|
51
51
|
self.repo_name = repo_name
|
|
52
|
-
|
|
52
|
+
# 支持将中心仓库配置为本地目录(含git子路径)
|
|
53
|
+
expanded = os.path.expanduser(os.path.expandvars(central_repo_url))
|
|
54
|
+
if os.path.isdir(expanded):
|
|
55
|
+
# 直接使用本地目录作为中心仓库路径(支持git仓库子目录)
|
|
56
|
+
self.repo_path = expanded
|
|
57
|
+
else:
|
|
58
|
+
# 仍按原逻辑使用数据目录中的克隆路径
|
|
59
|
+
self.repo_path = os.path.join(get_data_dir(), repo_name)
|
|
53
60
|
|
|
54
61
|
def update_central_repo(self) -> None:
|
|
55
62
|
"""克隆或更新中心仓库"""
|
|
@@ -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
|
|
@@ -128,7 +128,7 @@ class TaskAnalyzer:
|
|
|
128
128
|
|
|
129
129
|
def _handle_interrupt_with_tool_calls(self, user_input: str) -> str:
|
|
130
130
|
"""处理有工具调用时的中断"""
|
|
131
|
-
if self.agent.
|
|
131
|
+
if self.agent.confirm_callback("检测到有工具调用,是否继续处理工具调用?", True):
|
|
132
132
|
return join_prompts([
|
|
133
133
|
f"被用户中断,用户补充信息为:{user_input}",
|
|
134
134
|
"用户同意继续工具调用。"
|
|
@@ -144,7 +144,7 @@ class TaskAnalyzer:
|
|
|
144
144
|
satisfaction_feedback = ""
|
|
145
145
|
|
|
146
146
|
if not auto_completed and self.agent.use_analysis:
|
|
147
|
-
if self.agent.
|
|
147
|
+
if self.agent.confirm_callback("您对本次任务的完成是否满意?", True):
|
|
148
148
|
satisfaction_feedback = "用户对本次任务的完成表示满意。"
|
|
149
149
|
else:
|
|
150
150
|
feedback = self.agent._multiline_input(
|
|
@@ -158,6 +158,9 @@ class TaskAnalyzer:
|
|
|
158
158
|
satisfaction_feedback = (
|
|
159
159
|
"用户对本次任务的完成不满意,未提供具体反馈意见。"
|
|
160
160
|
)
|
|
161
|
+
elif auto_completed and self.agent.use_analysis:
|
|
162
|
+
# 自动完成模式下,仍然执行分析,但不收集用户反馈
|
|
163
|
+
satisfaction_feedback = "任务已自动完成,无需用户反馈。"
|
|
161
164
|
|
|
162
165
|
return satisfaction_feedback
|
|
163
166
|
|