jarvis-ai-assistant 0.3.29__py3-none-any.whl → 0.3.31__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.3.29"
4
+ __version__ = "0.3.31"
@@ -4,6 +4,7 @@ import datetime
4
4
  import os
5
5
  import platform
6
6
  import re
7
+ import sys
7
8
  from pathlib import Path
8
9
  from enum import Enum
9
10
  from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union
@@ -48,6 +49,7 @@ from jarvis.jarvis_agent.events import (
48
49
  INTERRUPT_TRIGGERED,
49
50
  BEFORE_TOOL_FILTER,
50
51
  TOOL_FILTERED,
52
+ AFTER_TOOL_CALL,
51
53
  )
52
54
  from jarvis.jarvis_agent.user_interaction import UserInteractionHandler
53
55
  from jarvis.jarvis_agent.utils import join_prompts
@@ -68,6 +70,7 @@ from jarvis.jarvis_utils.config import (
68
70
  is_use_analysis,
69
71
  is_use_methodology,
70
72
  get_tool_filter_threshold,
73
+ get_after_tool_call_cb_dirs,
71
74
  )
72
75
  from jarvis.jarvis_utils.embedding import get_context_token_count
73
76
  from jarvis.jarvis_utils.globals import (
@@ -310,7 +313,7 @@ class Agent:
310
313
  self.first = True
311
314
  self.run_input_handlers_next_turn = False
312
315
  self.user_data: Dict[str, Any] = {}
313
- self.after_tool_call_cb: Optional[Callable[[Agent], None]] = None
316
+
314
317
 
315
318
  # 用户确认回调:默认使用 CLI 的 user_confirm,可由外部注入以支持 TUI/GUI
316
319
  self.user_confirm: Callable[[str, bool], bool] = (
@@ -361,6 +364,8 @@ class Agent:
361
364
  self.get_tool_registry(), # type: ignore
362
365
  platform_name=self.model.platform_name(), # type: ignore
363
366
  )
367
+ # 动态加载工具调用后回调
368
+ self._load_after_tool_callbacks()
364
369
 
365
370
  def _init_model(self, model_group: Optional[str]):
366
371
  """初始化模型平台(统一使用 normal 平台/模型)"""
@@ -502,13 +507,95 @@ class Agent:
502
507
  # Fallback for custom handlers that only accept one argument
503
508
  return self.multiline_inputer(tip) # type: ignore
504
509
 
505
- def set_after_tool_call_cb(self, cb: Callable[[Any], None]): # type: ignore
506
- """设置工具调用后回调函数。
507
-
508
- 参数:
509
- cb: 回调函数
510
+ def _load_after_tool_callbacks(self) -> None:
510
511
  """
511
- self.after_tool_call_cb = cb
512
+ 扫描 JARVIS_AFTER_TOOL_CALL_CB_DIRS 中的 Python 文件并动态注册回调。
513
+ 约定优先级(任一命中即注册):
514
+ - 模块级可调用对象: after_tool_call_cb
515
+ - 工厂方法返回单个或多个可调用对象: get_after_tool_call_cb(), register_after_tool_call_cb()
516
+ """
517
+ try:
518
+ dirs = get_after_tool_call_cb_dirs()
519
+ if not dirs:
520
+ return
521
+ for d in dirs:
522
+ p_dir = Path(d)
523
+ if not p_dir.exists() or not p_dir.is_dir():
524
+ continue
525
+ for file_path in p_dir.glob("*.py"):
526
+ if file_path.name == "__init__.py":
527
+ continue
528
+ parent_dir = str(file_path.parent)
529
+ added_path = False
530
+ try:
531
+ if parent_dir not in sys.path:
532
+ sys.path.insert(0, parent_dir)
533
+ added_path = True
534
+ module_name = file_path.stem
535
+ module = __import__(module_name)
536
+
537
+ candidates: List[Callable[[Any], None]] = []
538
+
539
+ # 1) 直接导出的回调
540
+ if hasattr(module, "after_tool_call_cb"):
541
+ obj = getattr(module, "after_tool_call_cb")
542
+ if callable(obj):
543
+ candidates.append(obj) # type: ignore[arg-type]
544
+
545
+ # 2) 工厂方法:get_after_tool_call_cb()
546
+ if hasattr(module, "get_after_tool_call_cb"):
547
+ factory = getattr(module, "get_after_tool_call_cb")
548
+ if callable(factory):
549
+ try:
550
+ ret = factory()
551
+ if callable(ret):
552
+ candidates.append(ret)
553
+ elif isinstance(ret, (list, tuple)):
554
+ for c in ret:
555
+ if callable(c):
556
+ candidates.append(c)
557
+ except Exception:
558
+ pass
559
+
560
+ # 3) 工厂方法:register_after_tool_call_cb()
561
+ if hasattr(module, "register_after_tool_call_cb"):
562
+ factory2 = getattr(module, "register_after_tool_call_cb")
563
+ if callable(factory2):
564
+ try:
565
+ ret2 = factory2()
566
+ if callable(ret2):
567
+ candidates.append(ret2)
568
+ elif isinstance(ret2, (list, tuple)):
569
+ for c in ret2:
570
+ if callable(c):
571
+ candidates.append(c)
572
+ except Exception:
573
+ pass
574
+
575
+ for cb in candidates:
576
+ try:
577
+ def _make_wrapper(callback):
578
+ def _wrapper(**kwargs: Any) -> None:
579
+ try:
580
+ agent = kwargs.get("agent")
581
+ callback(agent)
582
+ except Exception:
583
+ pass
584
+ return _wrapper
585
+ self.event_bus.subscribe(AFTER_TOOL_CALL, _make_wrapper(cb))
586
+ except Exception:
587
+ pass
588
+
589
+ except Exception as e:
590
+ PrettyOutput.print(f"从 {file_path} 加载回调失败: {e}", OutputType.WARNING)
591
+ finally:
592
+ if added_path:
593
+ try:
594
+ sys.path.remove(parent_dir)
595
+ except ValueError:
596
+ pass
597
+ except Exception as e:
598
+ PrettyOutput.print(f"加载回调目录时发生错误: {e}", OutputType.WARNING)
512
599
 
513
600
  def save_session(self) -> bool:
514
601
  """Saves the current session state by delegating to the session manager."""
@@ -642,7 +729,14 @@ class Agent:
642
729
  pass
643
730
 
644
731
  response = self.model.chat_until_success(message) # type: ignore
645
-
732
+ # 防御: 模型可能返回空响应(None或空字符串),统一为空字符串并告警
733
+ if not response:
734
+ try:
735
+ PrettyOutput.print("模型返回空响应,已使用空字符串回退。", OutputType.WARNING)
736
+ except Exception:
737
+ pass
738
+ response = ""
739
+
646
740
  # 事件:模型调用后
647
741
  try:
648
742
  self.event_bus.emit(
@@ -674,7 +768,13 @@ class Agent:
674
768
  summary = self.model.chat_until_success(
675
769
  self.session.prompt + "\n" + SUMMARY_REQUEST_PROMPT
676
770
  ) # type: ignore
677
-
771
+ # 防御: 可能返回空响应(None或空字符串),统一为空字符串并告警
772
+ if not summary:
773
+ try:
774
+ PrettyOutput.print("总结模型返回空响应,已使用空字符串回退。", OutputType.WARNING)
775
+ except Exception:
776
+ pass
777
+ summary = ""
678
778
  return summary
679
779
  except Exception:
680
780
  PrettyOutput.print("总结对话历史失败", OutputType.ERROR)
@@ -811,6 +911,13 @@ class Agent:
811
911
  if not self.model:
812
912
  raise RuntimeError("Model not initialized")
813
913
  ret = self.model.chat_until_success(self.session.prompt) # type: ignore
914
+ # 防御: 总结阶段模型可能返回空响应(None或空字符串),统一为空字符串并告警
915
+ if not ret:
916
+ try:
917
+ PrettyOutput.print("总结阶段模型返回空响应,已使用空字符串回退。", OutputType.WARNING)
918
+ except Exception:
919
+ pass
920
+ ret = ""
814
921
  result = ret
815
922
 
816
923
  # 广播完成总结事件