mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-07 21:00:21 +08:00
* ♻️ 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>
514 lines
20 KiB
Python
514 lines
20 KiB
Python
from collections.abc import Callable
|
|
import copy
|
|
import inspect
|
|
from typing import Any, cast
|
|
|
|
from nonebot.utils import is_coroutine_callable
|
|
|
|
from zhenxun.services.ai.capabilities import (
|
|
CombinedCapability,
|
|
)
|
|
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
|
|
from zhenxun.services.ai.context.memory.engine import MemoryReader, MemoryWriter
|
|
from zhenxun.services.ai.context.memory.models import MemoryConfig
|
|
from zhenxun.services.ai.context.memory.types import SessionMetadata
|
|
from zhenxun.services.ai.core.messages import LLMMessage, TextPart
|
|
from zhenxun.services.ai.core.options import GenerationConfig
|
|
from zhenxun.services.ai.core.templates import PromptTemplate
|
|
from zhenxun.services.ai.flow.agent.capabilities import (
|
|
OutputValidationCapability,
|
|
TaskTrackingCapability,
|
|
)
|
|
from zhenxun.services.ai.flow.agent.models import Persona
|
|
from zhenxun.services.ai.run import RunContext
|
|
from zhenxun.services.ai.run.di import DependencyInjector
|
|
from zhenxun.services.ai.tools.engine.registry import (
|
|
ToolCollection,
|
|
tool_provider_manager,
|
|
)
|
|
from zhenxun.services.ai.tools.models import ResolvedToolPayload
|
|
from zhenxun.services.ai.utils.scope import ScopeSelector
|
|
from zhenxun.utils.pydantic_compat import model_copy
|
|
|
|
|
|
class AgentProfileResolver:
|
|
"""Agent 配置解析器:负责提取与合并 Agent 的运行时 Profile"""
|
|
|
|
@staticmethod
|
|
def resolve_memory(
|
|
agent_memory_config: MemoryConfig, override_memory: Any | None
|
|
) -> MemoryConfig:
|
|
"""
|
|
解析并合并 Memory 记忆域的配置。
|
|
支持从外部覆盖配置并重新构建。
|
|
|
|
参数:
|
|
agent_memory_config: 预置的 Agent 默认记忆域配置对象。
|
|
override_memory: 运行时覆盖的记忆域配置,可为 dict, MemoryConfig 或其他合法结构。
|
|
|
|
返回:
|
|
MemoryConfig: 合并并生成的运行时记忆域配置实例。
|
|
""" # noqa: E501
|
|
if override_memory is not None:
|
|
return MemoryBuilder.resolve(override_memory)
|
|
return model_copy(agent_memory_config, deep=True)
|
|
|
|
@staticmethod
|
|
def resolve_generation_config(
|
|
base_config: GenerationConfig,
|
|
cap_config: GenerationConfig | None,
|
|
profile_config: GenerationConfig | None,
|
|
) -> GenerationConfig:
|
|
"""
|
|
解析并合并多层 GenerationConfig 模型生成配置。
|
|
优先级顺序由低到高为:基础配置 -> 拦截器能力配置 -> 运行时 Profile 覆盖配置。
|
|
|
|
参数:
|
|
base_config: 基础的模型生成配置对象。
|
|
cap_config: 拦截器能力中提取出的模型生成参数配置。
|
|
profile_config: 运行时传入的 Profile 覆盖参数配置。
|
|
|
|
返回:
|
|
GenerationConfig: 合并多层配置后生成的最终运行时生成配置实例。
|
|
"""
|
|
final_gen_config = model_copy(base_config, deep=True)
|
|
if cap_config:
|
|
final_gen_config = final_gen_config.merge_with(cap_config)
|
|
if profile_config:
|
|
final_gen_config = final_gen_config.merge_with(profile_config)
|
|
return final_gen_config
|
|
|
|
|
|
class CapabilityBuilder:
|
|
"""拦截器能力组装器:负责合并 Agent, AgentTask, Profile 和全局的中间件"""
|
|
|
|
@staticmethod
|
|
async def build_for_run(
|
|
agent_name: str,
|
|
namespace: str,
|
|
output_type: Any | None,
|
|
raw_schema: dict | None,
|
|
agent_guardrails: list,
|
|
task_guardrails: list,
|
|
task_obj: Any | None,
|
|
agent_capabilities: list,
|
|
profile_capabilities: list | None,
|
|
context: RunContext,
|
|
) -> CombinedCapability:
|
|
"""
|
|
为当前的 Agent 运行实例组装并实例化所有能力拦截器中间件。
|
|
整合全局能力、任务追踪、格式校验以及动态注入的能力。
|
|
|
|
参数:
|
|
agent_name: 执行推理的 Agent 标识名。
|
|
namespace: 会话所归属的命名空间。
|
|
output_type: 期待大模型返回的结构化 Pydantic 模型类型(支持 None 或 str)。
|
|
raw_schema: 原始结构化 Schema 定义字典。
|
|
agent_guardrails: Agent 自身定义的业务语义安全护栏列表。
|
|
task_guardrails: 本次任务定义的业务语义安全护栏列表。
|
|
task_obj: 被追踪的任务上下文对象实例。
|
|
agent_capabilities: Agent 定义的静态拦截器列表。
|
|
profile_capabilities: 运行时动态传入的能力或中间件列表。
|
|
context: 运行上下文对象实例。
|
|
|
|
返回:
|
|
CombinedCapability: 已经过运行初始化完毕的合并能力拦截器实例。
|
|
"""
|
|
dynamic_caps = []
|
|
combined_guardrails = agent_guardrails + task_guardrails
|
|
|
|
if output_type is not None and output_type is not str:
|
|
dynamic_caps.append(
|
|
OutputValidationCapability(output_type, combined_guardrails)
|
|
)
|
|
elif raw_schema is not None:
|
|
dynamic_caps.append(
|
|
OutputValidationCapability(
|
|
None, combined_guardrails, raw_schema=raw_schema
|
|
)
|
|
)
|
|
elif combined_guardrails:
|
|
dynamic_caps.append(OutputValidationCapability(None, combined_guardrails))
|
|
|
|
if task_obj:
|
|
dynamic_caps.append(TaskTrackingCapability(task_obj, agent_name))
|
|
|
|
from zhenxun.services.ai.capabilities.manager import capability_manager
|
|
|
|
run_level_caps = capability_manager.resolve_capabilities(
|
|
profile_capabilities or [], namespace
|
|
)
|
|
agent_level_caps = capability_manager.resolve_capabilities(
|
|
agent_capabilities or [], namespace
|
|
)
|
|
|
|
auto_caps = capability_manager.get_auto_apply_capabilities(namespace)
|
|
|
|
combined_cap = CombinedCapability(
|
|
auto_caps
|
|
+ getattr(context, "capabilities", [])
|
|
+ agent_level_caps
|
|
+ run_level_caps
|
|
+ dynamic_caps
|
|
)
|
|
return cast(CombinedCapability, await combined_cap.for_run(context))
|
|
|
|
|
|
class ContextBuilder:
|
|
"""系统提示词与上下文记忆构建器"""
|
|
|
|
@staticmethod
|
|
async def build_prompts(
|
|
instruction: str | PromptTemplate,
|
|
system_prompts: list[Any],
|
|
run_context: RunContext,
|
|
run_scoped_cap: CombinedCapability,
|
|
persona: Persona | None = None,
|
|
) -> tuple[str, list[Any]]:
|
|
"""
|
|
解析、合并并渲染 Agent 的系统提示词和上下文记忆。
|
|
包含对依赖参数的动态注入和 Jinja 模板渲染。
|
|
|
|
参数:
|
|
instruction: 任务级别的初始指令或提示词模板。
|
|
system_prompts: 系统提示词生成函数(支持依赖注入)列表。
|
|
run_context: 运行上下文对象实例。
|
|
run_scoped_cap: 运行域下的合并能力中间件。
|
|
persona: 设定的 Agent 人设配置实例。
|
|
|
|
返回:
|
|
tuple[str, list[Any]]: 包含 (渲染后的静态系统提示词文本, 渲染后的动态消息列表) 的元组。
|
|
""" # noqa: E501
|
|
static_instructions = []
|
|
dynamic_messages = []
|
|
|
|
for sp_func in system_prompts:
|
|
sig = inspect.signature(sp_func)
|
|
if len(sig.parameters) > 0:
|
|
injected_kwargs = await DependencyInjector.resolve_all(
|
|
sig=sig,
|
|
call_kwargs={},
|
|
context=run_context,
|
|
)
|
|
res = (
|
|
(await sp_func(**injected_kwargs))
|
|
if is_coroutine_callable(sp_func)
|
|
else sp_func(**injected_kwargs)
|
|
)
|
|
else:
|
|
res = (await sp_func()) if is_coroutine_callable(sp_func) else sp_func()
|
|
if res:
|
|
if isinstance(res, LLMMessage):
|
|
dynamic_messages.append(res)
|
|
elif isinstance(res, list) and all(
|
|
isinstance(m, LLMMessage) for m in res
|
|
):
|
|
dynamic_messages.extend(res)
|
|
else:
|
|
if isinstance(res, list):
|
|
for item in res:
|
|
if item:
|
|
dynamic_messages.append(LLMMessage.system(str(item)))
|
|
else:
|
|
dynamic_messages.append(LLMMessage.system(str(res)))
|
|
|
|
if persona:
|
|
persona_parts = [
|
|
f"## 扮演角色 (Role)\n{persona.role}",
|
|
f"## 核心目标 (Goal)\n{persona.goal}",
|
|
]
|
|
if persona.backstory:
|
|
persona_parts.append(f"## 角色背景 (Backstory)\n{persona.backstory}")
|
|
static_instructions.append("\n\n".join(persona_parts))
|
|
|
|
if instruction:
|
|
static_instructions.append("## 本次任务指令 (AgentTask)")
|
|
|
|
if instruction:
|
|
if isinstance(instruction, PromptTemplate):
|
|
static_instructions.append(instruction.format_with_context(run_context))
|
|
else:
|
|
static_instructions.append(str(instruction))
|
|
|
|
caps = (
|
|
run_scoped_cap.capabilities
|
|
if run_scoped_cap
|
|
else getattr(run_context, "capabilities", [])
|
|
)
|
|
for cap in caps:
|
|
cap_prompts = await cap.get_system_prompts(run_context)
|
|
for prompt_text in cap_prompts:
|
|
if prompt_text and prompt_text.strip():
|
|
dynamic_messages.append(LLMMessage.system(prompt_text))
|
|
|
|
static_text = "\n\n".join(static_instructions)
|
|
|
|
render_context = {
|
|
"deps": run_context.deps,
|
|
"bot": getattr(run_context.deps, "bot", None),
|
|
"event": getattr(run_context.deps, "event", None),
|
|
"matcher": getattr(run_context.deps, "matcher", None),
|
|
}
|
|
if run_context.state:
|
|
render_context.update(run_context.state)
|
|
|
|
rendered_dynamic_messages = []
|
|
|
|
for msg in dynamic_messages:
|
|
if msg.role == "system":
|
|
new_content = []
|
|
changed = False
|
|
for part in msg.content:
|
|
if isinstance(part, TextPart) and part.text:
|
|
try:
|
|
rendered_text = PromptTemplate(part.text).render(
|
|
**render_context
|
|
)
|
|
new_content.append(TextPart(text=rendered_text))
|
|
if rendered_text != part.text:
|
|
changed = True
|
|
except Exception:
|
|
new_content.append(part)
|
|
else:
|
|
new_content.append(part)
|
|
if changed:
|
|
new_msg = msg.model_copy(deep=True)
|
|
new_msg.content = new_content
|
|
rendered_dynamic_messages.append(new_msg)
|
|
else:
|
|
rendered_dynamic_messages.append(msg)
|
|
else:
|
|
rendered_dynamic_messages.append(msg)
|
|
|
|
return (
|
|
PromptTemplate(static_text).render(**render_context),
|
|
rendered_dynamic_messages,
|
|
)
|
|
|
|
|
|
class ToolBuilder:
|
|
"""系统工具集合解析与构建器"""
|
|
|
|
@staticmethod
|
|
async def resolve_tools(
|
|
tool_definitions: list[Any],
|
|
toolset_funcs: list[Any],
|
|
system_tools: list[Any],
|
|
namespace: str,
|
|
run_context: RunContext,
|
|
run_scoped_cap: CombinedCapability,
|
|
) -> ResolvedToolPayload:
|
|
"""
|
|
解析并合并来自静态定义、动态函数依赖以及能力的工具列表。
|
|
通过工具提供者管理器完成工具的具体实例化及参数绑定。
|
|
|
|
参数:
|
|
tool_definitions: 静态工具或工具集合的定义列表。
|
|
toolset_funcs: 待依赖注入解析的工具集生成函数列表。
|
|
system_tools: 系统默认强制集成的工具定义列表.
|
|
namespace: 会话命名空间。
|
|
run_context: 运行上下文对象实例.
|
|
run_scoped_cap: 运行域下的合并能力中间件,用于提供特定的能力工具。
|
|
|
|
返回:
|
|
ResolvedToolPayload: 解析完毕并附带依赖绑定关系的工具负载载体。
|
|
"""
|
|
defs_to_resolve = list(tool_definitions)
|
|
|
|
for ts_func in toolset_funcs:
|
|
sig = inspect.signature(ts_func)
|
|
injected_kwargs = {}
|
|
if len(sig.parameters) > 0:
|
|
injected_kwargs = await DependencyInjector.resolve_all(
|
|
sig=sig,
|
|
call_kwargs={},
|
|
context=run_context,
|
|
)
|
|
|
|
res = (
|
|
(await ts_func(**injected_kwargs))
|
|
if is_coroutine_callable(ts_func)
|
|
else ts_func(**injected_kwargs)
|
|
)
|
|
|
|
if res is not None:
|
|
if isinstance(res, list):
|
|
defs_to_resolve.extend(res)
|
|
else:
|
|
defs_to_resolve.append(res)
|
|
|
|
if system_tools:
|
|
for st in system_tools:
|
|
if st not in defs_to_resolve:
|
|
defs_to_resolve.append(st)
|
|
|
|
caps = (
|
|
run_scoped_cap.capabilities
|
|
if run_scoped_cap
|
|
else getattr(run_context, "capabilities", [])
|
|
)
|
|
for cap in caps:
|
|
cap_tools = await cap.get_tools(run_context)
|
|
defs_to_resolve.extend(cap_tools)
|
|
|
|
payload = await tool_provider_manager.resolve_tools(
|
|
defs_to_resolve, namespace, context=run_context
|
|
)
|
|
|
|
return payload
|
|
|
|
@staticmethod
|
|
async def prepare_effective_tools(
|
|
effective_tools: list[Any],
|
|
context: RunContext,
|
|
tool_filters: list[Callable],
|
|
run_scoped_cap: CombinedCapability,
|
|
) -> ToolCollection:
|
|
"""
|
|
在将工具发往模型执行器之前,触发最终的过滤器与能力拦截,进行 schema 的清洗。
|
|
|
|
参数:
|
|
effective_tools: 备选的工具执行实例列表。
|
|
context: 运行上下文对象实例。
|
|
tool_filters: 运行时自定义工具过滤与清洗函数列表。
|
|
run_scoped_cap: 运行域下的合并能力中间件,提供拦截入口。
|
|
|
|
返回:
|
|
ToolCollection: 准备就绪的、可直接发往模型的最终有效工具执行集。
|
|
"""
|
|
current_tool_defs = []
|
|
for t_exec in effective_tools:
|
|
if hasattr(t_exec, "get_definition"):
|
|
t_def = await t_exec.get_definition(context)
|
|
if t_def:
|
|
current_tool_defs.append(t_def)
|
|
|
|
if tool_filters:
|
|
for filter_func in tool_filters:
|
|
sig = inspect.signature(filter_func)
|
|
call_kwargs = {"tool_defs": current_tool_defs}
|
|
resolved_kwargs = await DependencyInjector.resolve_all(
|
|
sig, call_kwargs, context
|
|
)
|
|
filtered_kwargs = {
|
|
k: v for k, v in resolved_kwargs.items() if k in sig.parameters
|
|
}
|
|
_res = (
|
|
await filter_func(**filtered_kwargs)
|
|
if is_coroutine_callable(filter_func)
|
|
else filter_func(**filtered_kwargs)
|
|
)
|
|
if _res is not None:
|
|
current_tool_defs = list(_res)
|
|
|
|
_cap_res = await run_scoped_cap.prepare_tools(context, current_tool_defs)
|
|
if _cap_res is not None:
|
|
current_tool_defs = list(_cap_res)
|
|
|
|
final_defs_map = {d.name.lower(): d for d in current_tool_defs if d}
|
|
final_effective_tools_list = []
|
|
for t_exec in effective_tools:
|
|
t_name = getattr(t_exec, "name", "unknown")
|
|
if t_name.lower() in final_defs_map:
|
|
cloned_tool = copy.copy(t_exec)
|
|
cloned_tool._dynamic_def = final_defs_map[t_name.lower()]
|
|
final_effective_tools_list.append(cloned_tool)
|
|
return ToolCollection(final_effective_tools_list)
|
|
|
|
|
|
class SessionBuilder:
|
|
"""会话与记忆域构建器:负责隔离前缀计算和读写门面装配"""
|
|
|
|
@staticmethod
|
|
def build_session_and_memory(
|
|
context: RunContext,
|
|
namespace: str,
|
|
agent_name: str,
|
|
effective_memory: MemoryConfig,
|
|
) -> tuple[Any, Any, Any]:
|
|
"""
|
|
根据当前用户、群组和平台标识,动态隔离前缀,计算并构建会话与记忆存储的读写门面。
|
|
|
|
参数:
|
|
context: 运行上下文对象实例。
|
|
namespace: 当前会话的命名空间。
|
|
agent_name: 执行推理的 Agent 标识名。
|
|
effective_memory: 运行时最终生效的 MemoryConfig 配置对象。
|
|
|
|
返回:
|
|
tuple[Any, Any, Any]: 包含 (SessionMetadata 会话元数据, MemoryReader 记忆读取器, MemoryWriter 记忆写入器) 的元组。
|
|
""" # noqa: E501
|
|
bot_id = None
|
|
bot_inst = context.get_bot()
|
|
if bot_inst and hasattr(bot_inst, "self_id"):
|
|
bot_id = str(bot_inst.self_id)
|
|
|
|
selector = ScopeSelector(
|
|
user_id=context.get_user_id(),
|
|
group_id=context.get_group_id(),
|
|
platform=context.get_platform(),
|
|
bot_id=bot_id,
|
|
namespace=namespace,
|
|
agent_name=agent_name,
|
|
)
|
|
|
|
all_scopes = {"/"}
|
|
scope_name_mapping = {}
|
|
|
|
if effective_memory.short_term and effective_memory.short_term.isolation:
|
|
sel = effective_memory.short_term.isolation.resolve(
|
|
deps=context.deps,
|
|
prefix="",
|
|
default_namespace=namespace,
|
|
default_agent=agent_name,
|
|
)
|
|
all_scopes.add(sel.scope_prefix)
|
|
|
|
for config_part in [effective_memory.slots, effective_memory.long_term]:
|
|
if config_part and hasattr(config_part, "scopes") and config_part.scopes:
|
|
for name, builder in config_part.scopes.items():
|
|
sel = builder.resolve(
|
|
deps=context.deps,
|
|
prefix="",
|
|
default_namespace=namespace,
|
|
default_agent=agent_name,
|
|
)
|
|
all_scopes.add(sel.scope_prefix)
|
|
scope_name_mapping[sel.scope_prefix] = name
|
|
|
|
parts = selector.get_scope_parts()
|
|
for i in range(len(parts)):
|
|
all_scopes.add("/" + "/".join(parts[: i + 1]))
|
|
|
|
accessible_scopes = sorted(all_scopes, key=lambda x: len(x.split("/")))
|
|
|
|
short_term_builder = (
|
|
effective_memory.short_term.isolation
|
|
if effective_memory.short_term
|
|
else effective_memory.base_isolation
|
|
)
|
|
short_term_selector = short_term_builder.resolve(
|
|
deps=context.deps,
|
|
prefix="",
|
|
default_namespace=namespace,
|
|
default_agent=agent_name,
|
|
)
|
|
|
|
session_metadata = SessionMetadata(
|
|
session_id=short_term_selector.scope_prefix,
|
|
selector=selector,
|
|
scope_prefix=selector.scope_prefix,
|
|
accessible_scopes=accessible_scopes,
|
|
scope_name_mapping=scope_name_mapping,
|
|
)
|
|
reader = MemoryReader(
|
|
session_meta=session_metadata, memory_config=effective_memory
|
|
)
|
|
writer = MemoryWriter(
|
|
session_meta=session_metadata,
|
|
memory_config=effective_memory,
|
|
context=context,
|
|
)
|
|
|
|
return session_metadata, reader, writer
|