Files
zhenxun_bot/zhenxun/services/ai/run/di.py
T
922d092650 ♻️ refactor(core): 重构 AI 能力与定时任务调度系统 (#2148)
* ♻️ refactor(core): 重构 AI 能力与定时任务调度系统

- 【AI 能力与工具】重构 Capability 注册与管理机制,引入 CapabilityManager 统一管理
- 移除全局能力注册表,改用声明式装饰器 `@capability` 进行解耦注册
- 重构工具解析器链,使用统一的 BaseToolResolver 代替原有的多个特定解析器
- 增强工具查询过滤,支持通配符匹配、工具箱过滤和排除标签
- 【定时任务调度】重构定时任务管理器,引入 SchedulerRegistry 统一管理任务元数据
- 引入 JobConfig 聚合定时任务配置,支持用户维度的定时任务调度
- 重构执行分发器,支持并发限制、串行间隔和随机延迟打散
- 【运行上下文】引入 ScheduledDeps 以支持后台和定时任务环境下的依赖注入
- 优化 RunContext,支持从定时任务上下文快速构造,并提供 emit 辅助方法
- 【日志与监控】引入 AILoggerProxy,实现 AI 各模块的专属日志输出
- 将各模块的全局 logger 替换为对应的模块专属日志代理
- 【其他优化】修复 Pydantic V1 兼容层中 model_validator 的装饰器兼容性问题
- 在非交互式环境(如定时任务)中自动隐藏 HITL 交互工具以节省 Token

* ♻️ refactor(core): 优化内部导入路径并提升 Pydantic 兼容性

- 【重构】将 `services/ai` 模块内的绝对导入重构为相对导入,优化包结构
- 【重构】移除不必要的 `if TYPE_CHECKING` 保护,通过 `from __future__ import annotations` 直接导入类型
- 【清理】清理 `core/messages/types.py` 中未使用的 `AssistantContentUnion` 等联合类型定义
- 【优化】在 `utils/pydantic_compat.py` 中新增 `model_rebuild` 兼容函数,统一 Pydantic V1/V2 的模型重建逻辑
- 【优化】将部分函数内部的延迟导入提升至模块顶部,规范代码结构

* ♻️ refactor(imports): 优化导入路径为相对导入并清理冗余导入

- 【重构】将 AI 服务相关模块中的绝对导入路径修改为相对导入,提升模块内聚性与可移植性
- 【清理】移除多处函数内部或类方法中未使用的冗余导入,避免循环引用和资源浪费
- 【格式化】微调部分工具装饰器和返回语句的格式与尾随逗号

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-10 09:14:06 +08:00

405 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from collections import defaultdict
from collections.abc import Awaitable, Callable
import inspect
from typing import Annotated, Any, ClassVar, cast
from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher
from nonebot.utils import is_coroutine_callable
from nonebot_plugin_session import EventSession, extract_session
from zhenxun.services.ai.context.memory.facades import AgentSessionFacade
from zhenxun.utils.utils import infer_plugin_namespace
from .blackboard import BlackboardManager
from .context import ProviderFunc, RunContext, _is_run_context_type
from .hitl import HITLController
from .ui import UIController
class Hidden:
"""
标记一个参数为隐藏参数。
在生成大模型工具 Schema 时,带有该标记的 Annotated 参数将被剔除。
"""
pass
class _InjectMarker:
"""内部魔术标记,用于识别 DI 类型糖"""
def __init__(self, key: str):
self.key = key
class _UpstreamResultMarker:
"""内部魔术标记,用于精准捕获上游特定节点的产出"""
def __init__(self, step_name: str):
self.step_name = step_name
CurrentUserId = Annotated[str | None, Hidden(), _InjectMarker("user_id")]
CurrentGroupId = Annotated[str | None, Hidden(), _InjectMarker("group_id")]
CurrentPlatform = Annotated[str, Hidden(), _InjectMarker("platform")]
CurrentBot = Annotated[Bot | None, Hidden(), _InjectMarker("bot")]
CurrentEvent = Annotated[Event | None, Hidden(), _InjectMarker("event")]
CurrentMatcher = Annotated[Matcher | None, Hidden(), _InjectMarker("matcher")]
CurrentSession = Annotated[EventSession | None, Hidden(), _InjectMarker("session")]
CurrentUI = Annotated[UIController, Hidden(), _InjectMarker("ui")]
CurrentHITL = Annotated[HITLController, Hidden(), _InjectMarker("hitl")]
CurrentModelName = Annotated[str | None, Hidden(), _InjectMarker("model_name")]
CurrentToolRetries = Annotated[int, Hidden(), _InjectMarker("tool_retries")]
CurrentState = Annotated[dict[str, Any], Hidden(), _InjectMarker("state")]
CurrentSharedState = Annotated[dict[str, Any], Hidden(), _InjectMarker("shared_state")]
CurrentOriginalInput = Annotated[str, Hidden(), _InjectMarker("original_input")]
UpstreamResults = Annotated[dict[str, Any], Hidden(), _InjectMarker("upstream_results")]
CurrentEventPayload = Annotated[Any, Hidden(), _InjectMarker("stream_event")]
CurrentBlackboard = Annotated[
BlackboardManager | None, Hidden(), _InjectMarker("blackboard")
]
CurrentMemory = Annotated[AgentSessionFacade, Hidden(), _InjectMarker("memory")]
CurrentSandbox = Annotated[Any, Hidden(), _InjectMarker("sandbox")]
class Inject:
"""
[命名空间] 大模型工具依赖注入类型糖。
用于在工具参数中快捷获取群聊/用户的上下文,或注册自定义的依赖提供者。
"""
_providers: ClassVar[dict[str, dict[str, ProviderFunc]]] = defaultdict(dict)
@classmethod
def register_provider(
cls, key: str, provider: ProviderFunc, scope: str | None = None
) -> None:
"""
(底层) 注册一个自定义的依赖提供者。
"""
ns = scope if scope is not None else infer_plugin_namespace()
cls._providers[key][ns] = provider
@classmethod
def provider(cls, key: str, scope: str | None = None):
def decorator(func: ProviderFunc) -> ProviderFunc:
cls.register_provider(key, func, scope)
return func
return decorator
@classmethod
def bind(cls, key: str, type_hint: Any = Any) -> Any:
"""
动态生成一个用于类型注解的 Inject 标记。
示例:DbSession = Inject.bind("db_session", AsyncSession)
"""
return Annotated[type_hint, Hidden(), _InjectMarker(key)]
@classmethod
def UpstreamResult(cls, step_name: str) -> Any:
"""
自动注入:精准获取工作流中指定前置节点的产出。可作为参数的默认值使用以通过静态检查。
示例: def my_step(data: str = Inject.UpstreamResult("AgentA")):
"""
return _UpstreamResultMarker(step_name)
CurrentBlackboardState = Annotated[Any, Hidden(), _InjectMarker("blackboard_state")]
BlackboardState = CurrentBlackboardState
"""自动注入:当前挂载的强类型黑板底层 Pydantic 实例(需配合 Annotated 使用)"""
UserId = CurrentUserId
"""自动注入:触发当前任务的用户 ID"""
GroupId = CurrentGroupId
"""自动注入:触发当前任务的群聊 ID(私聊时为 None)"""
Platform = CurrentPlatform
"""自动注入:当前对接的平台名称(如 qq)"""
Bot = CurrentBot
"""自动注入:当前的 NoneBot Bot 实例"""
Event = CurrentEvent
"""自动注入:当前的 NoneBot Event 实例"""
Matcher = CurrentMatcher
"""自动注入:当前的 NoneBot Matcher 实例"""
Session = CurrentSession
"""自动注入:当前的 nonebot_plugin_session 会话实例"""
UI = CurrentUI
"""自动注入:前端 UI 控制器实例"""
HITL = CurrentHITL
"""自动注入:人机协同控制器实例 (提供底层 waiter 交互封装)"""
ModelName = CurrentModelName
"""自动注入:当前大模型正在执行的底层模型名称"""
ToolRetries = CurrentToolRetries
"""自动注入:当前工具正在经历的重试次数 (Int)"""
State = CurrentState
"""自动注入:当前 Agent 轮次隔离的业务状态字典"""
OriginalInput = CurrentOriginalInput
"""自动注入:当前工作流最初始的用户输入文本"""
SharedState = CurrentSharedState
"""自动注入:当前 Team 或全局穿透的共享黑板状态字典"""
UpstreamResults = UpstreamResults
"""自动注入:当前工作流中所有上游节点的产出字典 (Key: Agent Name)"""
EventPayload = CurrentEventPayload
"""自动注入:当前生命周期钩子流转中的 EventBus 载荷实体"""
Blackboard = CurrentBlackboard
"""自动注入:当前工作流/团队挂载的强类型黑板 (BlackboardManager) 实例"""
Memory = CurrentMemory
"""自动注入:当前会话的持久化记忆存取门面 (AgentSessionFacade) 实例"""
Sandbox = CurrentSandbox
"""自动注入:当前沙箱环境管理器实例"""
class BaseParamResolver:
"""可插拔参数解析器基类协议"""
def match(
self,
name: str,
param: inspect.Parameter,
context: RunContext,
) -> bool:
return False
def static_match(self, param: inspect.Parameter) -> bool:
return False
async def resolve(
self,
name: str,
param: inspect.Parameter,
context: RunContext,
) -> Any:
raise NotImplementedError
class RunContextResolver(BaseParamResolver):
def match(self, name, param, context) -> bool:
return _is_run_context_type(param.annotation)
def static_match(self, param: inspect.Parameter) -> bool:
return _is_run_context_type(param.annotation)
async def resolve(self, name, param, context) -> Any:
return context
class TypeSugarResolver(BaseParamResolver):
def _get_marker(self, param: inspect.Parameter) -> Any | None:
anno = param.annotation
if hasattr(anno, "__metadata__"):
for arg in anno.__metadata__:
if isinstance(arg, _InjectMarker | _UpstreamResultMarker):
return arg
default_val = param.default
if isinstance(default_val, _InjectMarker | _UpstreamResultMarker):
return default_val
if hasattr(default_val, "__metadata__"):
for arg in default_val.__metadata__:
if isinstance(arg, _InjectMarker | _UpstreamResultMarker):
return arg
return None
def match(self, name, param, context) -> bool:
return self._get_marker(param) is not None
def static_match(self, param: inspect.Parameter) -> bool:
return self._get_marker(param) is not None
async def resolve(self, name, param, context) -> Any:
marker = self._get_marker(param)
if not marker:
raise ValueError(f"参数 {name} 缺失 Inject 标记")
if isinstance(marker, _UpstreamResultMarker):
return context.upstream_results.get(marker.step_name)
marker_key = marker.key
if marker_key in context.di_cache:
return context.di_cache[marker_key]
provider_dict = Inject._providers.get(marker_key, {})
ns = getattr(context.session, "namespace", "global")
provider = provider_dict.get(ns) or provider_dict.get("global")
if not provider:
raise ValueError(f"未知的类型糖标记或未注册的 Provider: {marker_key}")
if is_coroutine_callable(provider):
result = await provider(context)
else:
result = provider(context)
context.di_cache[marker_key] = result
return result
class DependencyInjector:
"""可插拔依赖注入管线 (Resolver Pipeline)"""
_resolvers: ClassVar[list[BaseParamResolver]] = []
@classmethod
def register(cls, resolver: BaseParamResolver) -> None:
cls._resolvers.append(resolver)
@classmethod
def can_resolve_statically(cls, param: inspect.Parameter) -> bool:
for resolver in cls._resolvers:
if resolver.static_match(param):
return True
return False
@classmethod
async def resolve_all(
cls,
sig: inspect.Signature,
call_kwargs: dict[str, Any],
context: RunContext,
) -> dict[str, Any]:
resolved_kwargs = dict(call_kwargs)
for k, v in call_kwargs.items():
context.di_cache[k] = v
for name, param in sig.parameters.items():
if name in ("self", "cls") or name in resolved_kwargs:
continue
if param.kind in (
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD,
):
continue
resolved = False
for resolver in cls._resolvers:
if resolver.match(name, param, context):
val = await resolver.resolve(name, param, context)
resolved_kwargs[name] = val
resolved = True
break
if not resolved:
if param.default is not inspect.Parameter.empty:
continue
raise ValueError(
f"参数 '{name}' 未被大模型提供且缺少显式的依赖注入标记"
"(如 Inject.* 或 RunContext)。"
)
return resolved_kwargs
@classmethod
async def invoke(
cls,
func: Callable[..., Any],
call_kwargs: dict[str, Any],
context: RunContext,
) -> Any:
"""统一执行带有依赖注入的函数 (支持同步/异步)"""
sig = inspect.signature(func)
resolved_kwargs = await cls.resolve_all(sig, call_kwargs, context)
filtered_kwargs = {
k: v for k, v in resolved_kwargs.items() if k in sig.parameters
}
if is_coroutine_callable(func):
return await cast(Callable[..., Awaitable[Any]], func)(**filtered_kwargs)
return cast(Callable[..., Any], func)(**filtered_kwargs)
DependencyInjector.register(RunContextResolver())
DependencyInjector.register(TypeSugarResolver())
Inject.register_provider("user_id", lambda ctx: ctx.get_user_id(), scope="global")
Inject.register_provider("group_id", lambda ctx: ctx.get_group_id(), scope="global")
Inject.register_provider("platform", lambda ctx: ctx.get_platform(), scope="global")
Inject.register_provider("bot", lambda ctx: ctx.get_bot(), scope="global")
Inject.register_provider("event", lambda ctx: ctx.get_event(), scope="global")
Inject.register_provider("matcher", lambda ctx: ctx.get_matcher(), scope="global")
Inject.register_provider("hitl", lambda ctx: HITLController(ctx), scope="global")
Inject.register_provider(
"model_name", lambda ctx: ctx.run.current_model, scope="global"
)
Inject.register_provider(
"tool_retries", lambda ctx: ctx.call.retry_count, scope="global"
)
Inject.register_provider(
"original_input", lambda ctx: ctx.run.user_input, scope="global"
)
Inject.register_provider("state", lambda ctx: ctx.state, scope="global")
Inject.register_provider(
"upstream_results", lambda ctx: ctx.upstream_results, scope="global"
)
Inject.register_provider(
"shared_state", lambda ctx: ctx.session.shared_state, scope="global"
)
Inject.register_provider("memory", lambda ctx: ctx.session.memory, scope="global")
def _resolve_blackboard(ctx: RunContext):
bb = ctx.session.blackboard
if bb is None:
raise ValueError(
"Inject.Blackboard 注入失败:"
"当前会话上下文中未挂载 BlackboardManager 实例。"
)
return bb
Inject.register_provider("blackboard", _resolve_blackboard, scope="global")
def _resolve_blackboard_state(ctx: RunContext):
bb = ctx.session.blackboard
if bb is None:
raise ValueError(
"Inject.BlackboardState 注入失败:"
"当前会话上下文中未挂载 BlackboardManager 实例。"
)
return bb._state
Inject.register_provider("blackboard_state", _resolve_blackboard_state, scope="global")
def _resolve_session(ctx):
bot = ctx.get_bot()
event = ctx.get_event()
return extract_session(bot, event) if bot and event else None
Inject.register_provider("session", _resolve_session, scope="global")
Inject.register_provider("ui", lambda ctx: UIController(ctx), scope="global")
from zhenxun.services.ai.sandbox.manager import sandbox_manager
Inject.register_provider("sandbox", lambda ctx: sandbox_manager, scope="global")
__all__ = [
"DependencyInjector",
"Hidden",
"Inject",
]