jarvis-ai-assistant 0.4.0__py3-none-any.whl → 0.4.2__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 +95 -74
- jarvis/jarvis_agent/agent_manager.py +6 -4
- jarvis/jarvis_agent/edit_file_handler.py +2 -2
- jarvis/jarvis_agent/jarvis.py +40 -9
- jarvis/jarvis_agent/rewrite_file_handler.py +143 -0
- jarvis/jarvis_agent/run_loop.py +23 -4
- jarvis/jarvis_agent/utils.py +5 -1
- jarvis/jarvis_agent/web_server.py +332 -234
- jarvis/jarvis_code_agent/code_agent.py +10 -7
- jarvis/jarvis_code_analysis/code_review.py +0 -1
- jarvis/jarvis_data/config_schema.json +10 -0
- jarvis/jarvis_multi_agent/__init__.py +227 -34
- jarvis/jarvis_multi_agent/main.py +10 -1
- jarvis/jarvis_platform/base.py +15 -6
- jarvis/jarvis_tools/registry.py +6 -0
- jarvis/jarvis_tools/sub_agent.py +19 -34
- jarvis/jarvis_tools/sub_code_agent.py +3 -1
- jarvis/jarvis_utils/config.py +26 -11
- jarvis/jarvis_utils/input.py +77 -6
- {jarvis_ai_assistant-0.4.0.dist-info → jarvis_ai_assistant-0.4.2.dist-info}/METADATA +1 -1
- {jarvis_ai_assistant-0.4.0.dist-info → jarvis_ai_assistant-0.4.2.dist-info}/RECORD +26 -28
- jarvis/jarvis_agent/config.py +0 -99
- jarvis/jarvis_tools/edit_file.py +0 -208
- jarvis/jarvis_tools/rewrite_file.py +0 -191
- {jarvis_ai_assistant-0.4.0.dist-info → jarvis_ai_assistant-0.4.2.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.4.0.dist-info → jarvis_ai_assistant-0.4.2.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.4.0.dist-info → jarvis_ai_assistant-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.4.0.dist-info → jarvis_ai_assistant-0.4.2.dist-info}/top_level.txt +0 -0
jarvis/__init__.py
CHANGED
jarvis/jarvis_agent/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
# 第三方库导入
|
|
13
14
|
from rich.align import Align
|
|
14
15
|
from rich.console import Console
|
|
@@ -31,9 +32,10 @@ from jarvis.jarvis_agent.prompts import (
|
|
|
31
32
|
TASK_ANALYSIS_PROMPT,
|
|
32
33
|
)
|
|
33
34
|
from jarvis.jarvis_tools.registry import ToolRegistry
|
|
35
|
+
from jarvis.jarvis_agent.edit_file_handler import EditFileHandler
|
|
36
|
+
from jarvis.jarvis_agent.rewrite_file_handler import RewriteFileHandler
|
|
34
37
|
from jarvis.jarvis_agent.prompt_manager import PromptManager
|
|
35
38
|
from jarvis.jarvis_agent.event_bus import EventBus
|
|
36
|
-
from jarvis.jarvis_agent.config import AgentConfig
|
|
37
39
|
from jarvis.jarvis_agent.run_loop import AgentRunLoop
|
|
38
40
|
from jarvis.jarvis_agent.events import (
|
|
39
41
|
BEFORE_SUMMARY,
|
|
@@ -65,7 +67,6 @@ from jarvis.jarvis_platform.registry import PlatformRegistry
|
|
|
65
67
|
# jarvis_utils 相关
|
|
66
68
|
from jarvis.jarvis_utils.config import (
|
|
67
69
|
get_data_dir,
|
|
68
|
-
get_max_token_count,
|
|
69
70
|
get_normal_model_name,
|
|
70
71
|
get_normal_platform_name,
|
|
71
72
|
is_execute_tool_confirm,
|
|
@@ -74,6 +75,8 @@ from jarvis.jarvis_utils.config import (
|
|
|
74
75
|
is_use_methodology,
|
|
75
76
|
get_tool_filter_threshold,
|
|
76
77
|
get_after_tool_call_cb_dirs,
|
|
78
|
+
|
|
79
|
+
|
|
77
80
|
)
|
|
78
81
|
from jarvis.jarvis_utils.embedding import get_context_token_count
|
|
79
82
|
from jarvis.jarvis_utils.globals import (
|
|
@@ -279,12 +282,16 @@ class Agent:
|
|
|
279
282
|
use_tools: Optional[List[str]] = None,
|
|
280
283
|
execute_tool_confirm: Optional[bool] = None,
|
|
281
284
|
need_summary: bool = True,
|
|
285
|
+
auto_summary_rounds: Optional[int] = None,
|
|
282
286
|
multiline_inputer: Optional[Callable[[str], str]] = None,
|
|
283
287
|
use_methodology: Optional[bool] = None,
|
|
284
288
|
use_analysis: Optional[bool] = None,
|
|
285
289
|
force_save_memory: Optional[bool] = None,
|
|
286
290
|
files: Optional[List[str]] = None,
|
|
287
291
|
confirm_callback: Optional[Callable[[str, bool], bool]] = None,
|
|
292
|
+
non_interactive: Optional[bool] = None,
|
|
293
|
+
in_multi_agent: Optional[bool] = None,
|
|
294
|
+
**kwargs,
|
|
288
295
|
):
|
|
289
296
|
"""初始化Jarvis Agent实例
|
|
290
297
|
|
|
@@ -295,7 +302,6 @@ class Agent:
|
|
|
295
302
|
|
|
296
303
|
summary_prompt: 任务总结提示模板
|
|
297
304
|
auto_complete: 是否自动完成任务
|
|
298
|
-
output_handler: 输出处理器列表
|
|
299
305
|
execute_tool_confirm: 执行工具前是否需要确认
|
|
300
306
|
need_summary: 是否需要生成总结
|
|
301
307
|
multiline_inputer: 多行输入处理器
|
|
@@ -303,21 +309,38 @@ class Agent:
|
|
|
303
309
|
use_analysis: 是否使用任务分析
|
|
304
310
|
force_save_memory: 是否强制保存记忆
|
|
305
311
|
confirm_callback: 用户确认回调函数,签名为 (tip: str, default: bool) -> bool;默认使用CLI的user_confirm
|
|
312
|
+
non_interactive: 是否以非交互模式运行(优先级最高,覆盖环境变量与配置)
|
|
306
313
|
"""
|
|
307
|
-
#
|
|
308
|
-
|
|
314
|
+
# 基础属性初始化(仅根据入参设置原始值;实际生效的默认回退在 _init_config 中统一解析)
|
|
315
|
+
# 标识与描述
|
|
309
316
|
self.name = make_agent_name(name)
|
|
310
317
|
self.description = description
|
|
311
318
|
self.system_prompt = system_prompt
|
|
312
|
-
|
|
313
|
-
self.auto_complete = auto_complete
|
|
319
|
+
# 行为控制开关(原始入参值)
|
|
320
|
+
self.auto_complete = bool(auto_complete)
|
|
321
|
+
self.need_summary = bool(need_summary)
|
|
322
|
+
# 自动摘要轮次:None 表示使用配置文件中的默认值,由 AgentRunLoop 决定最终取值
|
|
323
|
+
self.auto_summary_rounds = auto_summary_rounds
|
|
324
|
+
self.use_methodology = use_methodology
|
|
325
|
+
self.use_analysis = use_analysis
|
|
326
|
+
self.execute_tool_confirm = execute_tool_confirm
|
|
327
|
+
self.summary_prompt = summary_prompt
|
|
328
|
+
self.force_save_memory = force_save_memory
|
|
329
|
+
# 资源与环境
|
|
330
|
+
self.model_group = model_group
|
|
331
|
+
self.files = files or []
|
|
332
|
+
self.use_tools = use_tools
|
|
333
|
+
self.non_interactive = non_interactive
|
|
334
|
+
# 多智能体运行标志:用于控制非交互模式下的自动完成行为
|
|
335
|
+
self.in_multi_agent = bool(in_multi_agent)
|
|
336
|
+
# 运行时状态
|
|
314
337
|
self.first = True
|
|
315
338
|
self.run_input_handlers_next_turn = False
|
|
316
339
|
self.user_data: Dict[str, Any] = {}
|
|
317
340
|
|
|
318
341
|
|
|
319
342
|
# 用户确认回调:默认使用 CLI 的 user_confirm,可由外部注入以支持 TUI/GUI
|
|
320
|
-
self.
|
|
343
|
+
self.confirm_callback: Callable[[str, bool], bool] = (
|
|
321
344
|
confirm_callback or user_confirm # type: ignore[assignment]
|
|
322
345
|
)
|
|
323
346
|
|
|
@@ -327,24 +350,62 @@ class Agent:
|
|
|
327
350
|
|
|
328
351
|
# 初始化处理器
|
|
329
352
|
self._init_handlers(
|
|
330
|
-
output_handler or [],
|
|
331
353
|
multiline_inputer,
|
|
354
|
+
output_handler,
|
|
332
355
|
use_tools or [],
|
|
333
356
|
)
|
|
334
357
|
# 初始化用户交互封装,保持向后兼容
|
|
335
|
-
self.user_interaction = UserInteractionHandler(self.multiline_inputer, self.
|
|
358
|
+
self.user_interaction = UserInteractionHandler(self.multiline_inputer, self.confirm_callback)
|
|
336
359
|
# 将确认函数指向封装后的 confirm,保持既有调用不变
|
|
337
|
-
self.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
360
|
+
self.confirm_callback = self.user_interaction.confirm # type: ignore[assignment]
|
|
361
|
+
# 非交互模式参数支持:允许通过构造参数显式控制,便于其他Agent调用时设置
|
|
362
|
+
try:
|
|
363
|
+
# 优先使用构造参数,其次回退到环境变量
|
|
364
|
+
self.non_interactive = (
|
|
365
|
+
bool(non_interactive)
|
|
366
|
+
if non_interactive is not None
|
|
367
|
+
else str(os.environ.get("JARVIS_NON_INTERACTIVE", "")).lower() in ("1", "true", "yes")
|
|
368
|
+
)
|
|
369
|
+
# 如果构造参数显式提供,则同步到环境变量与全局配置,供下游组件读取
|
|
370
|
+
if non_interactive is not None:
|
|
371
|
+
os.environ["JARVIS_NON_INTERACTIVE"] = "true" if self.non_interactive else "false"
|
|
372
|
+
|
|
373
|
+
except Exception:
|
|
374
|
+
# 防御式回退
|
|
375
|
+
self.non_interactive = False
|
|
376
|
+
|
|
377
|
+
# 初始化配置(直接解析,不再依赖 _init_config)
|
|
378
|
+
try:
|
|
379
|
+
resolved_use_methodology = bool(use_methodology if use_methodology is not None else is_use_methodology())
|
|
380
|
+
except Exception:
|
|
381
|
+
resolved_use_methodology = bool(use_methodology) if use_methodology is not None else True
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
resolved_use_analysis = bool(use_analysis if use_analysis is not None else is_use_analysis())
|
|
385
|
+
except Exception:
|
|
386
|
+
resolved_use_analysis = bool(use_analysis) if use_analysis is not None else True
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
resolved_execute_tool_confirm = bool(execute_tool_confirm if execute_tool_confirm is not None else is_execute_tool_confirm())
|
|
390
|
+
except Exception:
|
|
391
|
+
resolved_execute_tool_confirm = bool(execute_tool_confirm) if execute_tool_confirm is not None else False
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
resolved_force_save_memory = bool(force_save_memory if force_save_memory is not None else is_force_save_memory())
|
|
395
|
+
except Exception:
|
|
396
|
+
resolved_force_save_memory = bool(force_save_memory) if force_save_memory is not None else False
|
|
397
|
+
|
|
398
|
+
self.use_methodology = resolved_use_methodology
|
|
399
|
+
self.use_analysis = resolved_use_analysis
|
|
400
|
+
self.execute_tool_confirm = resolved_execute_tool_confirm
|
|
401
|
+
self.summary_prompt = (summary_prompt or DEFAULT_SUMMARY_PROMPT)
|
|
402
|
+
self.force_save_memory = resolved_force_save_memory
|
|
403
|
+
# 多智能体模式下,默认不自动完成(即使是非交互),仅在明确传入 auto_complete=True 时开启
|
|
404
|
+
if self.in_multi_agent:
|
|
405
|
+
self.auto_complete = bool(self.auto_complete)
|
|
406
|
+
else:
|
|
407
|
+
# 非交互模式下默认自动完成;否则保持传入的 auto_complete 值
|
|
408
|
+
self.auto_complete = bool(self.auto_complete or (self.non_interactive or False))
|
|
348
409
|
|
|
349
410
|
# 初始化事件总线需先于管理器,以便管理器在构造中安全订阅事件
|
|
350
411
|
self.event_bus = EventBus()
|
|
@@ -394,12 +455,12 @@ class Agent:
|
|
|
394
455
|
|
|
395
456
|
def _init_handlers(
|
|
396
457
|
self,
|
|
397
|
-
output_handler: List[OutputHandlerProtocol],
|
|
398
458
|
multiline_inputer: Optional[Callable[[str], str]],
|
|
459
|
+
output_handler: Optional[List[OutputHandlerProtocol]],
|
|
399
460
|
use_tools: List[str],
|
|
400
461
|
):
|
|
401
462
|
"""初始化各种处理器"""
|
|
402
|
-
self.output_handler = output_handler or [ToolRegistry()]
|
|
463
|
+
self.output_handler = output_handler or [ToolRegistry(), EditFileHandler(), RewriteFileHandler()]
|
|
403
464
|
self.set_use_tools(use_tools)
|
|
404
465
|
self.input_handler = [
|
|
405
466
|
builtin_input_handler,
|
|
@@ -408,46 +469,6 @@ class Agent:
|
|
|
408
469
|
]
|
|
409
470
|
self.multiline_inputer = multiline_inputer or get_multiline_input
|
|
410
471
|
|
|
411
|
-
def _init_config(
|
|
412
|
-
self,
|
|
413
|
-
use_methodology: Optional[bool],
|
|
414
|
-
use_analysis: Optional[bool],
|
|
415
|
-
execute_tool_confirm: Optional[bool],
|
|
416
|
-
summary_prompt: Optional[str],
|
|
417
|
-
model_group: Optional[str],
|
|
418
|
-
force_save_memory: Optional[bool],
|
|
419
|
-
):
|
|
420
|
-
"""初始化配置选项"""
|
|
421
|
-
# 使用集中配置解析,保持与原逻辑一致
|
|
422
|
-
cfg = AgentConfig(
|
|
423
|
-
system_prompt=self.system_prompt,
|
|
424
|
-
name=self.name,
|
|
425
|
-
description=self.description,
|
|
426
|
-
model_group=model_group,
|
|
427
|
-
auto_complete=self.auto_complete,
|
|
428
|
-
need_summary=self.need_summary,
|
|
429
|
-
summary_prompt=summary_prompt,
|
|
430
|
-
execute_tool_confirm=execute_tool_confirm,
|
|
431
|
-
use_methodology=use_methodology,
|
|
432
|
-
use_analysis=use_analysis,
|
|
433
|
-
force_save_memory=force_save_memory,
|
|
434
|
-
files=self.files,
|
|
435
|
-
max_token_count=None,
|
|
436
|
-
).resolve_defaults()
|
|
437
|
-
|
|
438
|
-
# 将解析结果回填到 Agent 实例属性,保持向后兼容
|
|
439
|
-
self.use_methodology = bool(cfg.use_methodology)
|
|
440
|
-
self.use_analysis = bool(cfg.use_analysis)
|
|
441
|
-
self.execute_tool_confirm = bool(cfg.execute_tool_confirm)
|
|
442
|
-
self.summary_prompt = cfg.summary_prompt or DEFAULT_SUMMARY_PROMPT
|
|
443
|
-
self.max_token_count = int(cfg.max_token_count or get_max_token_count(model_group))
|
|
444
|
-
self.force_save_memory = bool(cfg.force_save_memory)
|
|
445
|
-
# 非交互模式下自动完成标志需要同步到 Agent 实例,避免循环
|
|
446
|
-
self.auto_complete = bool(cfg.auto_complete)
|
|
447
|
-
|
|
448
|
-
# 聚合配置到 AgentConfig,作为后续单一事实来源(保持兼容,不改变既有属性使用)
|
|
449
|
-
self.config = cfg
|
|
450
|
-
|
|
451
472
|
def _setup_system_prompt(self):
|
|
452
473
|
"""设置系统提示词"""
|
|
453
474
|
try:
|
|
@@ -707,14 +728,9 @@ class Agent:
|
|
|
707
728
|
return message
|
|
708
729
|
|
|
709
730
|
def _manage_conversation_length(self, message: str) -> str:
|
|
710
|
-
"""
|
|
731
|
+
"""管理对话长度计数;摘要触发由轮次在 AgentRunLoop 中统一处理。"""
|
|
711
732
|
self.session.conversation_length += get_context_token_count(message)
|
|
712
733
|
|
|
713
|
-
if self.session.conversation_length > self.max_token_count:
|
|
714
|
-
summary = self._summarize_and_clear_history()
|
|
715
|
-
if summary:
|
|
716
|
-
message = join_prompts([summary, message])
|
|
717
|
-
self.session.conversation_length = get_context_token_count(message)
|
|
718
734
|
|
|
719
735
|
return message
|
|
720
736
|
|
|
@@ -900,13 +916,17 @@ class Agent:
|
|
|
900
916
|
|
|
901
917
|
if self.need_summary:
|
|
902
918
|
|
|
903
|
-
|
|
919
|
+
# 确保总结提示词非空:若为None或仅空白,则回退到默认提示词
|
|
920
|
+
safe_summary_prompt = self.summary_prompt or ""
|
|
921
|
+
if isinstance(safe_summary_prompt, str) and safe_summary_prompt.strip() == "":
|
|
922
|
+
safe_summary_prompt = DEFAULT_SUMMARY_PROMPT
|
|
923
|
+
# 注意:不要写回 session.prompt,避免 BEFORE_SUMMARY 事件回调修改/清空后导致使用空prompt
|
|
904
924
|
# 广播将要生成总结事件
|
|
905
925
|
try:
|
|
906
926
|
self.event_bus.emit(
|
|
907
927
|
BEFORE_SUMMARY,
|
|
908
928
|
agent=self,
|
|
909
|
-
prompt=
|
|
929
|
+
prompt=safe_summary_prompt,
|
|
910
930
|
auto_completed=auto_completed,
|
|
911
931
|
need_summary=self.need_summary,
|
|
912
932
|
)
|
|
@@ -915,7 +935,8 @@ class Agent:
|
|
|
915
935
|
|
|
916
936
|
if not self.model:
|
|
917
937
|
raise RuntimeError("Model not initialized")
|
|
918
|
-
|
|
938
|
+
# 直接使用本地变量,避免受事件回调影响
|
|
939
|
+
ret = self.model.chat_until_success(safe_summary_prompt) # type: ignore
|
|
919
940
|
# 防御: 总结阶段模型可能返回空响应(None或空字符串),统一为空字符串并告警
|
|
920
941
|
if not ret:
|
|
921
942
|
try:
|
|
@@ -1066,7 +1087,7 @@ class Agent:
|
|
|
1066
1087
|
return self._complete_task(auto_completed=False)
|
|
1067
1088
|
|
|
1068
1089
|
if any(handler.can_handle(current_response) for handler in self.output_handler):
|
|
1069
|
-
if self.
|
|
1090
|
+
if self.confirm_callback("检测到有工具调用,是否继续处理工具调用?", True):
|
|
1070
1091
|
self.session.prompt = join_prompts([
|
|
1071
1092
|
f"被用户中断,用户补充信息为:{user_input}",
|
|
1072
1093
|
"用户同意继续工具调用。"
|
|
@@ -1293,7 +1314,7 @@ class Agent:
|
|
|
1293
1314
|
f"并且存在3个以上标签重叠的记忆。\n"
|
|
1294
1315
|
f"是否立即整理记忆库以优化性能和相关性?"
|
|
1295
1316
|
)
|
|
1296
|
-
if self.
|
|
1317
|
+
if self.confirm_callback(prompt, True):
|
|
1297
1318
|
PrettyOutput.print(
|
|
1298
1319
|
f"正在开始整理 '{scope_name}' ({memory_type}) 记忆库...",
|
|
1299
1320
|
OutputType.INFO,
|
|
@@ -16,7 +16,7 @@ from jarvis.jarvis_agent.file_context_handler import file_context_handler
|
|
|
16
16
|
from jarvis.jarvis_agent.shell_input_handler import shell_input_handler
|
|
17
17
|
from jarvis.jarvis_agent.task_manager import TaskManager
|
|
18
18
|
from jarvis.jarvis_tools.registry import ToolRegistry
|
|
19
|
-
from jarvis.jarvis_utils.config import is_non_interactive
|
|
19
|
+
from jarvis.jarvis_utils.config import is_non_interactive, is_skip_predefined_tasks
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class AgentManager:
|
|
@@ -31,6 +31,7 @@ class AgentManager:
|
|
|
31
31
|
use_analysis: Optional[bool] = None,
|
|
32
32
|
multiline_inputer: Optional[Callable[[str], str]] = None,
|
|
33
33
|
confirm_callback: Optional[Callable[[str, bool], bool]] = None,
|
|
34
|
+
non_interactive: Optional[bool] = None,
|
|
34
35
|
):
|
|
35
36
|
self.model_group = model_group
|
|
36
37
|
self.tool_group = tool_group
|
|
@@ -41,6 +42,7 @@ class AgentManager:
|
|
|
41
42
|
# 可选:注入输入与确认回调,用于Web模式等前端替代交互
|
|
42
43
|
self.multiline_inputer = multiline_inputer
|
|
43
44
|
self.confirm_callback = confirm_callback
|
|
45
|
+
self.non_interactive = non_interactive
|
|
44
46
|
|
|
45
47
|
def initialize(self) -> Agent:
|
|
46
48
|
"""初始化Agent"""
|
|
@@ -53,12 +55,12 @@ class AgentManager:
|
|
|
53
55
|
self.agent = Agent(
|
|
54
56
|
system_prompt=origin_agent_system_prompt,
|
|
55
57
|
model_group=self.model_group,
|
|
56
|
-
output_handler=[ToolRegistry()], # type: ignore
|
|
57
58
|
need_summary=False,
|
|
58
59
|
use_methodology=self.use_methodology,
|
|
59
60
|
use_analysis=self.use_analysis,
|
|
60
61
|
multiline_inputer=self.multiline_inputer,
|
|
61
62
|
confirm_callback=self.confirm_callback,
|
|
63
|
+
non_interactive=self.non_interactive,
|
|
62
64
|
)
|
|
63
65
|
|
|
64
66
|
# 尝试恢复会话
|
|
@@ -80,8 +82,8 @@ class AgentManager:
|
|
|
80
82
|
self.agent.run(task_content)
|
|
81
83
|
raise typer.Exit(code=0)
|
|
82
84
|
|
|
83
|
-
#
|
|
84
|
-
if not is_non_interactive() and self.agent.first:
|
|
85
|
+
# 处理预定义任务(非交互模式下跳过;支持配置跳过加载)
|
|
86
|
+
if not is_non_interactive() and not is_skip_predefined_tasks() and self.agent.first:
|
|
85
87
|
task_manager = TaskManager()
|
|
86
88
|
tasks = task_manager.load_tasks()
|
|
87
89
|
if tasks and (selected_task := task_manager.select_task(tasks)):
|
|
@@ -92,10 +92,10 @@ class EditFileHandler(OutputHandler):
|
|
|
92
92
|
if not patches:
|
|
93
93
|
return False, "未找到有效的文件编辑指令"
|
|
94
94
|
|
|
95
|
-
# 记录
|
|
95
|
+
# 记录 PATCH 操作调用统计
|
|
96
96
|
from jarvis.jarvis_stats.stats import StatsManager
|
|
97
97
|
|
|
98
|
-
StatsManager.increment("
|
|
98
|
+
StatsManager.increment("patch", group="tool")
|
|
99
99
|
|
|
100
100
|
results = []
|
|
101
101
|
|
jarvis/jarvis_agent/jarvis.py
CHANGED
|
@@ -595,7 +595,10 @@ def handle_builtin_config_selector(
|
|
|
595
595
|
|
|
596
596
|
elif sel["category"] == "multi_agent":
|
|
597
597
|
# jarvis-multi-agent 需要 -c/--config,用户输入通过 -i/--input 传递
|
|
598
|
+
# 同时传递 -g/--llm-group 以继承 jvs 的模型组选择
|
|
598
599
|
args = [str(sel["cmd"]), "-c", str(sel["file"])]
|
|
600
|
+
if model_group:
|
|
601
|
+
args += ["-g", str(model_group)]
|
|
599
602
|
if task:
|
|
600
603
|
args += ["-i", str(task)]
|
|
601
604
|
|
|
@@ -967,14 +970,14 @@ def run_cli(
|
|
|
967
970
|
return
|
|
968
971
|
|
|
969
972
|
# 在初始化环境前检测Git仓库,并可选择自动切换到代码开发模式(jca)
|
|
970
|
-
if not non_interactive
|
|
973
|
+
if not non_interactive:
|
|
971
974
|
try_switch_to_jca_if_git_repo(
|
|
972
975
|
model_group, tool_group, config_file, restore_session, task
|
|
973
976
|
)
|
|
974
977
|
|
|
975
978
|
# 在进入默认通用代理前,列出内置配置供选择(agent/multi_agent/roles)
|
|
976
979
|
# 非交互模式下跳过内置角色/配置选择
|
|
977
|
-
if not non_interactive
|
|
980
|
+
if not non_interactive:
|
|
978
981
|
handle_builtin_config_selector(model_group, tool_group, config_file, task)
|
|
979
982
|
|
|
980
983
|
# 初始化环境
|
|
@@ -1005,13 +1008,9 @@ def run_cli(
|
|
|
1005
1008
|
# 在 Web 模式下注入基于 WebSocket 的输入/确认回调
|
|
1006
1009
|
extra_kwargs = {}
|
|
1007
1010
|
if web:
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
extra_kwargs["confirm_callback"] = web_user_confirm
|
|
1012
|
-
except Exception as e:
|
|
1013
|
-
PrettyOutput.print(f"Web 模式初始化失败(加载 Web 桥接模块): {e}", OutputType.ERROR)
|
|
1014
|
-
raise typer.Exit(code=1)
|
|
1011
|
+
# 纯 xterm 交互模式:不注入 WebBridge 的输入/确认回调,避免阻塞等待浏览器响应
|
|
1012
|
+
#(交互由 /terminal PTY 会话中的 jvs 进程处理)
|
|
1013
|
+
pass
|
|
1015
1014
|
|
|
1016
1015
|
agent_manager = AgentManager(
|
|
1017
1016
|
model_group=model_group,
|
|
@@ -1019,6 +1018,7 @@ def run_cli(
|
|
|
1019
1018
|
restore_session=restore_session,
|
|
1020
1019
|
use_methodology=False if disable_methodology_analysis else None,
|
|
1021
1020
|
use_analysis=False if disable_methodology_analysis else None,
|
|
1021
|
+
non_interactive=non_interactive,
|
|
1022
1022
|
**extra_kwargs,
|
|
1023
1023
|
)
|
|
1024
1024
|
agent = agent_manager.initialize()
|
|
@@ -1051,6 +1051,37 @@ def run_cli(
|
|
|
1051
1051
|
enable_web_stdin_redirect()
|
|
1052
1052
|
except Exception:
|
|
1053
1053
|
pass
|
|
1054
|
+
# 记录用于交互式终端(PTY)重启的 jvs 启动命令(移除 web 相关参数)
|
|
1055
|
+
try:
|
|
1056
|
+
import sys as _sys, os as _os, json as _json
|
|
1057
|
+
_argv = list(_sys.argv)
|
|
1058
|
+
# 去掉程序名(argv[0]),并过滤 --web 相关参数
|
|
1059
|
+
filtered = []
|
|
1060
|
+
i = 1
|
|
1061
|
+
while i < len(_argv):
|
|
1062
|
+
a = _argv[i]
|
|
1063
|
+
if a == "--web" or a.startswith("--web="):
|
|
1064
|
+
i += 1
|
|
1065
|
+
continue
|
|
1066
|
+
if a == "--web-host":
|
|
1067
|
+
i += 2
|
|
1068
|
+
continue
|
|
1069
|
+
if a.startswith("--web-host="):
|
|
1070
|
+
i += 1
|
|
1071
|
+
continue
|
|
1072
|
+
if a == "--web-port":
|
|
1073
|
+
i += 2
|
|
1074
|
+
continue
|
|
1075
|
+
if a.startswith("--web-port="):
|
|
1076
|
+
i += 1
|
|
1077
|
+
continue
|
|
1078
|
+
filtered.append(a)
|
|
1079
|
+
i += 1
|
|
1080
|
+
# 使用 jvs 命令作为可执行文件,保留其余业务参数
|
|
1081
|
+
cmd = ["jvs"] + filtered
|
|
1082
|
+
_os.environ["JARVIS_WEB_LAUNCH_JSON"] = _json.dumps(cmd, ensure_ascii=False)
|
|
1083
|
+
except Exception:
|
|
1084
|
+
pass
|
|
1054
1085
|
PrettyOutput.print("以 Web 模式启动,请在浏览器中打开提供的地址进行交互。", OutputType.INFO)
|
|
1055
1086
|
# 启动 Web 服务(阻塞调用)
|
|
1056
1087
|
start_web_server(agent_manager, host=web_host, port=web_port)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Dict, 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
|
+
overall_success = True
|
|
80
|
+
|
|
81
|
+
for file_path, content in rewrites:
|
|
82
|
+
abs_path = os.path.abspath(file_path)
|
|
83
|
+
original_content = None
|
|
84
|
+
processed = False
|
|
85
|
+
try:
|
|
86
|
+
file_exists = os.path.exists(abs_path)
|
|
87
|
+
if file_exists:
|
|
88
|
+
with open(abs_path, "r", encoding="utf-8") as rf:
|
|
89
|
+
original_content = rf.read()
|
|
90
|
+
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
|
91
|
+
with open(abs_path, "w", encoding="utf-8") as wf:
|
|
92
|
+
wf.write(content)
|
|
93
|
+
processed = True
|
|
94
|
+
results.append(f"✅ 文件 {abs_path} 重写成功")
|
|
95
|
+
# 记录成功处理的文件(使用绝对路径)
|
|
96
|
+
if agent:
|
|
97
|
+
files = agent.get_user_data("files")
|
|
98
|
+
if files:
|
|
99
|
+
if abs_path not in files:
|
|
100
|
+
files.append(abs_path)
|
|
101
|
+
else:
|
|
102
|
+
files = [abs_path]
|
|
103
|
+
agent.set_user_data("files", files)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
overall_success = False
|
|
106
|
+
# 回滚已修改内容
|
|
107
|
+
try:
|
|
108
|
+
if processed:
|
|
109
|
+
if original_content is None:
|
|
110
|
+
if os.path.exists(abs_path):
|
|
111
|
+
os.remove(abs_path)
|
|
112
|
+
else:
|
|
113
|
+
with open(abs_path, "w", encoding="utf-8") as wf:
|
|
114
|
+
wf.write(original_content)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
PrettyOutput.print(f"文件重写失败: {str(e)}", OutputType.ERROR)
|
|
118
|
+
results.append(f"❌ 文件 {abs_path} 重写失败: {str(e)}")
|
|
119
|
+
|
|
120
|
+
summary = "\n".join(results)
|
|
121
|
+
# 按现有 EditFileHandler 约定,始终返回 (False, summary) 以继续主循环
|
|
122
|
+
return False, summary
|
|
123
|
+
|
|
124
|
+
def _parse_rewrites(self, response: str) -> List[Tuple[str, str]]:
|
|
125
|
+
"""
|
|
126
|
+
解析响应中的 REWRITE/REWRITE 指令块。
|
|
127
|
+
返回列表 [(file_path, content), ...],按在响应中的出现顺序排序
|
|
128
|
+
"""
|
|
129
|
+
items: List[Tuple[str, str]] = []
|
|
130
|
+
matches: List[Tuple[int, Any]] = []
|
|
131
|
+
for m in self.rewrite_pattern_file.finditer(response):
|
|
132
|
+
matches.append((m.start(), m))
|
|
133
|
+
|
|
134
|
+
# 按出现顺序排序
|
|
135
|
+
matches.sort(key=lambda x: x[0])
|
|
136
|
+
|
|
137
|
+
for _, m in matches:
|
|
138
|
+
file_path = m.group(1) or m.group(2) or m.group(3) or ""
|
|
139
|
+
file_path = file_path.strip()
|
|
140
|
+
content = m.group(4)
|
|
141
|
+
if file_path:
|
|
142
|
+
items.append((file_path, content))
|
|
143
|
+
return items
|
jarvis/jarvis_agent/run_loop.py
CHANGED
|
@@ -14,6 +14,7 @@ from typing import Any, TYPE_CHECKING
|
|
|
14
14
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
15
15
|
from jarvis.jarvis_agent.events import BEFORE_TOOL_CALL, AFTER_TOOL_CALL
|
|
16
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
|
|
17
18
|
|
|
18
19
|
if TYPE_CHECKING:
|
|
19
20
|
# 仅用于类型标注,避免运行时循环依赖
|
|
@@ -25,6 +26,12 @@ class AgentRunLoop:
|
|
|
25
26
|
self.agent = agent
|
|
26
27
|
self.conversation_rounds = 0
|
|
27
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
|
+
)
|
|
28
35
|
|
|
29
36
|
def run(self) -> Any:
|
|
30
37
|
"""主运行循环(委派到传入的 agent 实例的方法与属性)"""
|
|
@@ -37,6 +44,17 @@ class AgentRunLoop:
|
|
|
37
44
|
self.agent.session.addon_prompt = join_prompts(
|
|
38
45
|
[self.agent.session.addon_prompt, self.agent.get_tool_usage_prompt()]
|
|
39
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
|
|
40
58
|
|
|
41
59
|
ag = self.agent
|
|
42
60
|
|
|
@@ -81,12 +99,13 @@ class AgentRunLoop:
|
|
|
81
99
|
pass
|
|
82
100
|
need_return, tool_prompt = ag._call_tools(current_response)
|
|
83
101
|
|
|
84
|
-
#
|
|
85
|
-
ag.session.prompt = join_prompts([ag.session.prompt, tool_prompt])
|
|
86
|
-
|
|
102
|
+
# 如果工具要求立即返回结果(例如 SEND_MESSAGE 需要将字典返回给上层),直接返回该结果
|
|
87
103
|
if need_return:
|
|
88
|
-
return
|
|
104
|
+
return tool_prompt
|
|
89
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])
|
|
90
109
|
|
|
91
110
|
# 广播工具调用后的事件(不影响主流程)
|
|
92
111
|
try:
|
jarvis/jarvis_agent/utils.py
CHANGED
|
@@ -16,7 +16,11 @@ def join_prompts(parts: Iterable[str]) -> str:
|
|
|
16
16
|
- 使用两个换行分隔
|
|
17
17
|
- 不进行额外 strip,保持调用方原样语义
|
|
18
18
|
"""
|
|
19
|
-
|
|
19
|
+
try:
|
|
20
|
+
non_empty: List[str] = [p for p in parts if isinstance(p, str) and p]
|
|
21
|
+
except Exception:
|
|
22
|
+
# 防御性处理:若 parts 不可迭代或出现异常,直接返回空字符串
|
|
23
|
+
return ""
|
|
20
24
|
return "\n\n".join(non_empty)
|
|
21
25
|
|
|
22
26
|
def is_auto_complete(response: str) -> bool:
|