jarvis-ai-assistant 0.4.1__py3-none-any.whl → 0.5.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 CHANGED
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """Jarvis AI Assistant"""
3
3
 
4
- __version__ = "0.4.1"
4
+ __version__ = "0.5.0"
@@ -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
@@ -35,7 +36,6 @@ from jarvis.jarvis_agent.edit_file_handler import EditFileHandler
35
36
  from jarvis.jarvis_agent.rewrite_file_handler import RewriteFileHandler
36
37
  from jarvis.jarvis_agent.prompt_manager import PromptManager
37
38
  from jarvis.jarvis_agent.event_bus import EventBus
38
- from jarvis.jarvis_agent.config import AgentConfig
39
39
  from jarvis.jarvis_agent.run_loop import AgentRunLoop
40
40
  from jarvis.jarvis_agent.events import (
41
41
  BEFORE_SUMMARY,
@@ -67,7 +67,6 @@ from jarvis.jarvis_platform.registry import PlatformRegistry
67
67
  # jarvis_utils 相关
68
68
  from jarvis.jarvis_utils.config import (
69
69
  get_data_dir,
70
- get_max_token_count,
71
70
  get_normal_model_name,
72
71
  get_normal_platform_name,
73
72
  is_execute_tool_confirm,
@@ -76,7 +75,8 @@ from jarvis.jarvis_utils.config import (
76
75
  is_use_methodology,
77
76
  get_tool_filter_threshold,
78
77
  get_after_tool_call_cb_dirs,
79
- set_config,
78
+
79
+
80
80
  )
81
81
  from jarvis.jarvis_utils.embedding import get_context_token_count
82
82
  from jarvis.jarvis_utils.globals import (
@@ -249,8 +249,23 @@ class Agent:
249
249
  def clear_history(self):
250
250
  """
251
251
  Clears the current conversation history by delegating to the session manager.
252
+ Emits BEFORE_HISTORY_CLEAR/AFTER_HISTORY_CLEAR and reapplies system prompt to preserve constraints.
252
253
  """
254
+ # 广播清理历史前事件(不影响主流程)
255
+ try:
256
+ self.event_bus.emit(BEFORE_HISTORY_CLEAR, agent=self)
257
+ except Exception:
258
+ pass
259
+
260
+ # 清理会话历史并重置模型状态
253
261
  self.session.clear_history()
262
+
263
+ # 重置后重新设置系统提示词,确保系统约束仍然生效
264
+ try:
265
+ self._setup_system_prompt()
266
+ except Exception:
267
+ pass
268
+
254
269
  # 广播清理历史后的事件
255
270
  try:
256
271
  self.event_bus.emit(AFTER_HISTORY_CLEAR, agent=self)
@@ -282,6 +297,7 @@ class Agent:
282
297
  use_tools: Optional[List[str]] = None,
283
298
  execute_tool_confirm: Optional[bool] = None,
284
299
  need_summary: bool = True,
300
+ auto_summary_rounds: Optional[int] = None,
285
301
  multiline_inputer: Optional[Callable[[str], str]] = None,
286
302
  use_methodology: Optional[bool] = None,
287
303
  use_analysis: Optional[bool] = None,
@@ -289,6 +305,7 @@ class Agent:
289
305
  files: Optional[List[str]] = None,
290
306
  confirm_callback: Optional[Callable[[str, bool], bool]] = None,
291
307
  non_interactive: Optional[bool] = None,
308
+ in_multi_agent: Optional[bool] = None,
292
309
  **kwargs,
293
310
  ):
294
311
  """初始化Jarvis Agent实例
@@ -309,20 +326,36 @@ class Agent:
309
326
  confirm_callback: 用户确认回调函数,签名为 (tip: str, default: bool) -> bool;默认使用CLI的user_confirm
310
327
  non_interactive: 是否以非交互模式运行(优先级最高,覆盖环境变量与配置)
311
328
  """
312
- # 基础属性初始化
313
- self.files = files or []
329
+ # 基础属性初始化(仅根据入参设置原始值;实际生效的默认回退在 _init_config 中统一解析)
330
+ # 标识与描述
314
331
  self.name = make_agent_name(name)
315
332
  self.description = description
316
333
  self.system_prompt = system_prompt
317
- self.need_summary = need_summary
318
- self.auto_complete = auto_complete
334
+ # 行为控制开关(原始入参值)
335
+ self.auto_complete = bool(auto_complete)
336
+ self.need_summary = bool(need_summary)
337
+ # 自动摘要轮次:None 表示使用配置文件中的默认值,由 AgentRunLoop 决定最终取值
338
+ self.auto_summary_rounds = auto_summary_rounds
339
+ self.use_methodology = use_methodology
340
+ self.use_analysis = use_analysis
341
+ self.execute_tool_confirm = execute_tool_confirm
342
+ self.summary_prompt = summary_prompt
343
+ self.force_save_memory = force_save_memory
344
+ # 资源与环境
345
+ self.model_group = model_group
346
+ self.files = files or []
347
+ self.use_tools = use_tools
348
+ self.non_interactive = non_interactive
349
+ # 多智能体运行标志:用于控制非交互模式下的自动完成行为
350
+ self.in_multi_agent = bool(in_multi_agent)
351
+ # 运行时状态
319
352
  self.first = True
320
353
  self.run_input_handlers_next_turn = False
321
354
  self.user_data: Dict[str, Any] = {}
322
355
 
323
356
 
324
357
  # 用户确认回调:默认使用 CLI 的 user_confirm,可由外部注入以支持 TUI/GUI
325
- self.user_confirm: Callable[[str, bool], bool] = (
358
+ self.confirm_callback: Callable[[str, bool], bool] = (
326
359
  confirm_callback or user_confirm # type: ignore[assignment]
327
360
  )
328
361
 
@@ -337,9 +370,9 @@ class Agent:
337
370
  use_tools or [],
338
371
  )
339
372
  # 初始化用户交互封装,保持向后兼容
340
- self.user_interaction = UserInteractionHandler(self.multiline_inputer, self.user_confirm)
373
+ self.user_interaction = UserInteractionHandler(self.multiline_inputer, self.confirm_callback)
341
374
  # 将确认函数指向封装后的 confirm,保持既有调用不变
342
- self.user_confirm = self.user_interaction.confirm # type: ignore[assignment]
375
+ self.confirm_callback = self.user_interaction.confirm # type: ignore[assignment]
343
376
  # 非交互模式参数支持:允许通过构造参数显式控制,便于其他Agent调用时设置
344
377
  try:
345
378
  # 优先使用构造参数,其次回退到环境变量
@@ -351,24 +384,43 @@ class Agent:
351
384
  # 如果构造参数显式提供,则同步到环境变量与全局配置,供下游组件读取
352
385
  if non_interactive is not None:
353
386
  os.environ["JARVIS_NON_INTERACTIVE"] = "true" if self.non_interactive else "false"
354
- try:
355
- set_config("JARVIS_NON_INTERACTIVE", self.non_interactive)
356
- except Exception:
357
- # 配置同步失败不影响主流程
358
- pass
387
+
359
388
  except Exception:
360
389
  # 防御式回退
361
390
  self.non_interactive = False
362
391
 
363
- # 初始化配置
364
- self._init_config(
365
- use_methodology,
366
- use_analysis,
367
- execute_tool_confirm,
368
- summary_prompt,
369
- model_group,
370
- force_save_memory,
371
- )
392
+ # 初始化配置(直接解析,不再依赖 _init_config)
393
+ try:
394
+ resolved_use_methodology = bool(use_methodology if use_methodology is not None else is_use_methodology())
395
+ except Exception:
396
+ resolved_use_methodology = bool(use_methodology) if use_methodology is not None else True
397
+
398
+ try:
399
+ resolved_use_analysis = bool(use_analysis if use_analysis is not None else is_use_analysis())
400
+ except Exception:
401
+ resolved_use_analysis = bool(use_analysis) if use_analysis is not None else True
402
+
403
+ try:
404
+ resolved_execute_tool_confirm = bool(execute_tool_confirm if execute_tool_confirm is not None else is_execute_tool_confirm())
405
+ except Exception:
406
+ resolved_execute_tool_confirm = bool(execute_tool_confirm) if execute_tool_confirm is not None else False
407
+
408
+ try:
409
+ resolved_force_save_memory = bool(force_save_memory if force_save_memory is not None else is_force_save_memory())
410
+ except Exception:
411
+ resolved_force_save_memory = bool(force_save_memory) if force_save_memory is not None else False
412
+
413
+ self.use_methodology = resolved_use_methodology
414
+ self.use_analysis = resolved_use_analysis
415
+ self.execute_tool_confirm = resolved_execute_tool_confirm
416
+ self.summary_prompt = (summary_prompt or DEFAULT_SUMMARY_PROMPT)
417
+ self.force_save_memory = resolved_force_save_memory
418
+ # 多智能体模式下,默认不自动完成(即使是非交互),仅在明确传入 auto_complete=True 时开启
419
+ if self.in_multi_agent:
420
+ self.auto_complete = bool(self.auto_complete)
421
+ else:
422
+ # 非交互模式下默认自动完成;否则保持传入的 auto_complete 值
423
+ self.auto_complete = bool(self.auto_complete or (self.non_interactive or False))
372
424
 
373
425
  # 初始化事件总线需先于管理器,以便管理器在构造中安全订阅事件
374
426
  self.event_bus = EventBus()
@@ -432,46 +484,6 @@ class Agent:
432
484
  ]
433
485
  self.multiline_inputer = multiline_inputer or get_multiline_input
434
486
 
435
- def _init_config(
436
- self,
437
- use_methodology: Optional[bool],
438
- use_analysis: Optional[bool],
439
- execute_tool_confirm: Optional[bool],
440
- summary_prompt: Optional[str],
441
- model_group: Optional[str],
442
- force_save_memory: Optional[bool],
443
- ):
444
- """初始化配置选项"""
445
- # 使用集中配置解析,保持与原逻辑一致
446
- cfg = AgentConfig(
447
- system_prompt=self.system_prompt,
448
- name=self.name,
449
- description=self.description,
450
- model_group=model_group,
451
- auto_complete=self.auto_complete,
452
- need_summary=self.need_summary,
453
- summary_prompt=summary_prompt,
454
- execute_tool_confirm=execute_tool_confirm,
455
- use_methodology=use_methodology,
456
- use_analysis=use_analysis,
457
- force_save_memory=force_save_memory,
458
- files=self.files,
459
- max_token_count=None,
460
- ).resolve_defaults()
461
-
462
- # 将解析结果回填到 Agent 实例属性,保持向后兼容
463
- self.use_methodology = bool(cfg.use_methodology)
464
- self.use_analysis = bool(cfg.use_analysis)
465
- self.execute_tool_confirm = bool(cfg.execute_tool_confirm)
466
- self.summary_prompt = cfg.summary_prompt or DEFAULT_SUMMARY_PROMPT
467
- self.max_token_count = int(cfg.max_token_count or get_max_token_count(model_group))
468
- self.force_save_memory = bool(cfg.force_save_memory)
469
- # 非交互模式下自动完成标志需要同步到 Agent 实例,避免循环
470
- self.auto_complete = bool(cfg.auto_complete)
471
-
472
- # 聚合配置到 AgentConfig,作为后续单一事实来源(保持兼容,不改变既有属性使用)
473
- self.config = cfg
474
-
475
487
  def _setup_system_prompt(self):
476
488
  """设置系统提示词"""
477
489
  try:
@@ -731,14 +743,9 @@ class Agent:
731
743
  return message
732
744
 
733
745
  def _manage_conversation_length(self, message: str) -> str:
734
- """管理对话长度,必要时进行摘要"""
746
+ """管理对话长度计数;摘要触发由轮次在 AgentRunLoop 中统一处理。"""
735
747
  self.session.conversation_length += get_context_token_count(message)
736
748
 
737
- if self.session.conversation_length > self.max_token_count:
738
- summary = self._summarize_and_clear_history()
739
- if summary:
740
- message = join_prompts([summary, message])
741
- self.session.conversation_length = get_context_token_count(message)
742
749
 
743
750
  return message
744
751
 
@@ -1011,7 +1018,6 @@ class Agent:
1011
1018
  {complete_prompt}
1012
1019
  如果没有完成,请进行下一步操作:
1013
1020
  - 仅包含一个操作
1014
- - 不要询问用户是否继续,直接继续执行直至完成
1015
1021
  - 如果信息不明确,请请求用户补充
1016
1022
  - 如果执行过程中连续失败5次,请使用ask_user询问用户操作
1017
1023
  - 操作列表:{action_handlers}{memory_prompts}
@@ -1095,7 +1101,7 @@ class Agent:
1095
1101
  return self._complete_task(auto_completed=False)
1096
1102
 
1097
1103
  if any(handler.can_handle(current_response) for handler in self.output_handler):
1098
- if self.user_confirm("检测到有工具调用,是否继续处理工具调用?", True):
1104
+ if self.confirm_callback("检测到有工具调用,是否继续处理工具调用?", True):
1099
1105
  self.session.prompt = join_prompts([
1100
1106
  f"被用户中断,用户补充信息为:{user_input}",
1101
1107
  "用户同意继续工具调用。"
@@ -1322,7 +1328,7 @@ class Agent:
1322
1328
  f"并且存在3个以上标签重叠的记忆。\n"
1323
1329
  f"是否立即整理记忆库以优化性能和相关性?"
1324
1330
  )
1325
- if self.user_confirm(prompt, True):
1331
+ if self.confirm_callback(prompt, True):
1326
1332
  PrettyOutput.print(
1327
1333
  f"正在开始整理 '{scope_name}' ({memory_type}) 记忆库...",
1328
1334
  OutputType.INFO,
@@ -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
 
@@ -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
- self.repo_path = os.path.join(get_data_dir(), repo_name)
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
  """克隆或更新中心仓库"""
@@ -25,6 +25,7 @@ from jarvis.jarvis_utils.config import (
25
25
  is_enable_static_analysis,
26
26
  get_git_check_mode,
27
27
  set_config,
28
+ get_data_dir,
28
29
  )
29
30
  from jarvis.jarvis_utils.git_utils import (
30
31
  confirm_add_new_files,
@@ -86,6 +87,22 @@ class CodeAgent:
86
87
 
87
88
  tool_registry.use_tools(base_tools)
88
89
  code_system_prompt = self._get_system_prompt()
90
+ # 先加载全局规则(数据目录 rules),再加载项目规则(.jarvis/rules),并拼接为单一规则块注入
91
+ global_rules = self._read_global_rules()
92
+ project_rules = self._read_project_rules()
93
+
94
+ combined_parts: List[str] = []
95
+ if global_rules:
96
+ combined_parts.append(global_rules)
97
+ if project_rules:
98
+ combined_parts.append(project_rules)
99
+
100
+ if combined_parts:
101
+ merged_rules = "\n\n".join(combined_parts)
102
+ code_system_prompt = (
103
+ f"{code_system_prompt}\n\n"
104
+ f"<rules>\n{merged_rules}\n</rules>"
105
+ )
89
106
  self.agent = Agent(
90
107
  system_prompt=code_system_prompt,
91
108
  name="CodeAgent",
@@ -164,6 +181,32 @@ class CodeAgent:
164
181
  </say_to_llm>
165
182
  """
166
183
 
184
+ def _read_project_rules(self) -> Optional[str]:
185
+ """读取 .jarvis/rules 内容,如果存在则返回字符串,否则返回 None"""
186
+ try:
187
+ rules_path = os.path.join(self.root_dir, ".jarvis", "rules")
188
+ if os.path.exists(rules_path) and os.path.isfile(rules_path):
189
+ with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
190
+ content = f.read().strip()
191
+ return content if content else None
192
+ except Exception:
193
+ # 读取规则失败时忽略,不影响主流程
194
+ pass
195
+ return None
196
+
197
+ def _read_global_rules(self) -> Optional[str]:
198
+ """读取数据目录 rules 内容,如果存在则返回字符串,否则返回 None"""
199
+ try:
200
+ rules_path = os.path.join(get_data_dir(), "rules")
201
+ if os.path.exists(rules_path) and os.path.isfile(rules_path):
202
+ with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
203
+ content = f.read().strip()
204
+ return content if content else None
205
+ except Exception:
206
+ # 读取规则失败时忽略,不影响主流程
207
+ pass
208
+ return None
209
+
167
210
  def _check_git_config(self) -> None:
168
211
  """检查 git username 和 email 是否已设置,如果没有则提示并退出"""
169
212
  try:
@@ -672,12 +715,58 @@ class CodeAgent:
672
715
  def _build_per_file_patch_preview(modified_files: List[str]) -> str:
673
716
  status_map = _build_name_status_map()
674
717
  lines: List[str] = []
718
+
719
+ def _get_file_numstat(file_path: str) -> Tuple[int, int]:
720
+ """获取单文件的新增/删除行数,失败时返回(0,0)"""
721
+ head_exists = bool(get_latest_commit_hash())
722
+ try:
723
+ # 让未跟踪文件也能统计到新增行数
724
+ subprocess.run(["git", "add", "-N", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
725
+ cmd = ["git", "diff", "--numstat"] + (["HEAD"] if head_exists else []) + ["--", file_path]
726
+ res = subprocess.run(
727
+ cmd,
728
+ capture_output=True,
729
+ text=True,
730
+ encoding="utf-8",
731
+ errors="replace",
732
+ check=False,
733
+ )
734
+ if res.returncode == 0 and res.stdout:
735
+ for line in res.stdout.splitlines():
736
+ parts = line.strip().split("\t")
737
+ if len(parts) >= 3:
738
+ add_s, del_s = parts[0], parts[1]
739
+
740
+ def to_int(x: str) -> int:
741
+ try:
742
+ return int(x)
743
+ except Exception:
744
+ # 二进制或无法解析时显示为0
745
+ return 0
746
+
747
+ return to_int(add_s), to_int(del_s)
748
+ finally:
749
+ subprocess.run(["git", "reset", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
750
+ return (0, 0)
751
+
675
752
  for f in modified_files:
676
753
  status = status_map.get(f, "")
677
- # 删除文件:不展示diff,仅提示
754
+ adds, dels = _get_file_numstat(f)
755
+ total_changes = adds + dels
756
+
757
+ # 删除文件:不展示diff,仅提示(附带删除行数信息如果可用)
678
758
  if (status.startswith("D")) or (not os.path.exists(f)):
679
- lines.append(f"- {f} 文件被删除")
759
+ if dels > 0:
760
+ lines.append(f"- {f} 文件被删除(删除{dels}行)")
761
+ else:
762
+ lines.append(f"- {f} 文件被删除")
680
763
  continue
764
+
765
+ # 变更过大:仅提示新增/删除行数,避免输出超长diff
766
+ if total_changes > 300:
767
+ lines.append(f"- {f} 新增{adds}行/删除{dels}行(变更过大,预览已省略)")
768
+ continue
769
+
681
770
  # 其它情况:展示该文件的diff
682
771
  file_diff = _get_file_diff(f)
683
772
  if file_diff.strip():