♻️ 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>
This commit is contained in:
Rumio
2026-07-10 09:14:06 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent 0b32d69c9c
commit 922d092650
132 changed files with 2372 additions and 1972 deletions
@@ -6,7 +6,14 @@ from zhenxun.models.level_user import LevelUser
from zhenxun.models.scheduled_job import ScheduledJob from zhenxun.models.scheduled_job import ScheduledJob
from zhenxun.services import scheduler_manager from zhenxun.services import scheduler_manager
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.services.scheduler.registry import scheduler_registry
from zhenxun.services.scheduler.repository import ScheduleRepository from zhenxun.services.scheduler.repository import ScheduleRepository
from zhenxun.services.scheduler.types import (
BaseTrigger,
ExecutionOptions,
JobConfig,
TargetType,
)
from zhenxun.utils.pydantic_compat import model_dump, model_validate from zhenxun.utils.pydantic_compat import model_dump, model_validate
from . import presenters from . import presenters
@@ -58,7 +65,7 @@ class SchedulerAdminService:
targets: list[str], targets: list[str],
creator_permission_level: int, creator_permission_level: int,
plugin_name: str, plugin_name: str,
trigger_info: tuple[str, dict], trigger: BaseTrigger,
job_kwargs: dict, job_kwargs: dict,
permission: int, permission: int,
bot_id: str, bot_id: str,
@@ -69,7 +76,6 @@ class SchedulerAdminService:
created_by: str, created_by: str,
) -> str: ) -> str:
"""创建或更新一个定时任务""" """创建或更新一个定时任务"""
trigger_type, trigger_config = trigger_info
success_targets = [] success_targets = []
failed_targets = [] failed_targets = []
permission_denied_targets = [] permission_denied_targets = []
@@ -103,7 +109,7 @@ class SchedulerAdminService:
) )
continue continue
if target_type in ["TAG", "ALL_GROUPS"]: if target_type in [TargetType.TAG.value, TargetType.ALL_GROUPS.value]:
logger.debug( logger.debug(
f"检测到多目标任务 (类型: {target_type})," f"检测到多目标任务 (类型: {target_type}),"
f"将所需权限强制提升至超级用户级别。" f"将所需权限强制提升至超级用户级别。"
@@ -111,18 +117,26 @@ class SchedulerAdminService:
permission = 9 permission = 9
try: try:
exec_opts = (
model_validate(ExecutionOptions, execution_options)
if execution_options
else model_validate(ExecutionOptions, {})
)
config = JobConfig(
trigger=trigger,
job_kwargs=job_kwargs,
bot_id=bot_id,
name=job_name,
created_by=created_by,
required_permission=permission,
execution_options=exec_opts,
)
schedule = await scheduler_manager.add_schedule( schedule = await scheduler_manager.add_schedule(
plugin_name=plugin_name, plugin_name=plugin_name,
target_type=target_type, target_type=target_type,
target_identifier=target_id, target_identifier=target_id,
trigger_type=trigger_type, config=config,
trigger_config=trigger_config,
job_kwargs=job_kwargs,
bot_id=bot_id,
required_permission=permission,
name=job_name,
created_by=created_by,
execution_options=execution_options if execution_options else None,
) )
if schedule: if schedule:
success_targets.append((target_desc, schedule.id)) success_targets.append((target_desc, schedule.id))
@@ -150,7 +164,10 @@ class SchedulerAdminService:
permission_denied = False permission_denied = False
if all_flag or global_flag: if all_flag or global_flag:
permission_denied = True permission_denied = True
elif targeter._filters.get("target_type") in ["TAG", "ALL_GROUPS"]: elif targeter._filters.get("target_type") in [
TargetType.TAG.value,
TargetType.ALL_GROUPS.value,
]:
permission_denied = True permission_denied = True
if permission_denied: if permission_denied:
@@ -202,11 +219,16 @@ class SchedulerAdminService:
) )
async def update_schedule( async def update_schedule(
self, schedule: ScheduledJob, trigger_info: tuple | None, kwargs_str: str | None self,
schedule: ScheduledJob,
trigger: BaseTrigger | None,
kwargs_str: str | None,
) -> str: ) -> str:
"""更新一个任务的配置""" """更新一个任务的配置"""
trigger_type = trigger_info[0] if trigger_info else None trigger_type = trigger.trigger_type if trigger else None
trigger_config = trigger_info[1] if trigger_info else None trigger_config = (
model_dump(trigger, exclude={"trigger_type"}) if trigger else None
)
job_kwargs = await self._parse_and_validate_kwargs_for_update( job_kwargs = await self._parse_and_validate_kwargs_for_update(
schedule.plugin_name, kwargs_str schedule.plugin_name, kwargs_str
) )
@@ -243,7 +265,7 @@ class SchedulerAdminService:
def _generate_view_title(self, filters: dict) -> str: def _generate_view_title(self, filters: dict) -> str:
title = "定时任务" title = "定时任务"
if filters.get("target_type") == "ALL_GROUPS": if filters.get("target_type") == TargetType.ALL_GROUPS.value:
title = "全局定时任务" title = "全局定时任务"
elif "target_identifier" in filters: elif "target_identifier" in filters:
title = f"群 {filters['target_identifier']} 的定时任务" title = f"群 {filters['target_identifier']} 的定时任务"
@@ -253,12 +275,12 @@ class SchedulerAdminService:
def _resolve_target_descriptor(self, target_desc: str) -> tuple[str, str]: def _resolve_target_descriptor(self, target_desc: str) -> tuple[str, str]:
if target_desc == scheduler_manager.ALL_GROUPS: if target_desc == scheduler_manager.ALL_GROUPS:
return "ALL_GROUPS", scheduler_manager.ALL_GROUPS return TargetType.ALL_GROUPS.value, scheduler_manager.ALL_GROUPS
if target_desc.startswith("tag:"): if target_desc.startswith("tag:"):
return "TAG", target_desc[4:] return TargetType.TAG.value, target_desc[4:]
if target_desc.isdigit(): if target_desc.isdigit():
return "GROUP", target_desc return TargetType.GROUP.value, target_desc
return "USER", target_desc return TargetType.USER.value, target_desc
def _format_set_result_message( def _format_set_result_message(
self, targets: list, success: list, failed: list, permission_denied: list self, targets: list, success: list, failed: list, permission_denied: list
@@ -286,7 +308,7 @@ class SchedulerAdminService:
if not kwargs_str: if not kwargs_str:
return {} return {}
task_meta = scheduler_manager._registered_tasks.get(plugin_name) task_meta = scheduler_registry.tasks.get(plugin_name)
if not task_meta: if not task_meta:
raise ValueError(f"插件 '{plugin_name}' 未注册。") raise ValueError(f"插件 '{plugin_name}' 未注册。")
@@ -22,6 +22,8 @@ from zhenxun.configs.config import Config
from zhenxun.models.level_user import LevelUser from zhenxun.models.level_user import LevelUser
from zhenxun.models.scheduled_job import ScheduledJob from zhenxun.models.scheduled_job import ScheduledJob
from zhenxun.services import scheduler_manager from zhenxun.services import scheduler_manager
from zhenxun.services.scheduler.registry import scheduler_registry
from zhenxun.services.scheduler.types import BaseTrigger, TargetType, Trigger
from zhenxun.utils.time_utils import TimeUtils from zhenxun.utils.time_utils import TimeUtils
@@ -103,7 +105,7 @@ def parse_daily_time(time_str: str) -> dict:
raise ValueError("时间格式错误,请使用 'HH:MM' 或 'HH:MM:SS' 格式。") raise ValueError("时间格式错误,请使用 'HH:MM' 或 'HH:MM:SS' 格式。")
def _parse_trigger_from_arparma(arp: Arparma) -> tuple[str, dict] | None: def _parse_trigger_from_arparma(arp: Arparma) -> BaseTrigger | None:
"""从 Arparma 中解析时间触发器配置""" """从 Arparma 中解析时间触发器配置"""
subcommand_name = next(iter(arp.subcommands.keys()), None) subcommand_name = next(iter(arp.subcommands.keys()), None)
if not subcommand_name: if not subcommand_name:
@@ -111,19 +113,22 @@ def _parse_trigger_from_arparma(arp: Arparma) -> tuple[str, dict] | None:
try: try:
if cron_expr := arp.query[str](f"{subcommand_name}.cron.cron_expr", None): if cron_expr := arp.query[str](f"{subcommand_name}.cron.cron_expr", None):
return "cron", dict( return Trigger.cron(
**dict(
zip( zip(
["minute", "hour", "day", "month", "day_of_week"], cron_expr.split() ["minute", "hour", "day", "month", "day_of_week"],
cron_expr.split(),
)
) )
) )
if interval_expr := arp.query[str]( if interval_expr := arp.query[str](
f"{subcommand_name}.interval.interval_expr", None f"{subcommand_name}.interval.interval_expr", None
): ):
return "interval", TimeUtils.parse_interval_to_dict(interval_expr) return Trigger.interval(**TimeUtils.parse_interval_to_dict(interval_expr))
if date_expr := arp.query[str](f"{subcommand_name}.date.date_expr", None): if date_expr := arp.query[str](f"{subcommand_name}.date.date_expr", None):
return "date", {"run_date": datetime.fromisoformat(date_expr)} return Trigger.date(run_date=datetime.fromisoformat(date_expr))
if daily_expr := arp.query[str](f"{subcommand_name}.daily.daily_expr", None): if daily_expr := arp.query[str](f"{subcommand_name}.daily.daily_expr", None):
return "cron", parse_daily_time(daily_expr) return Trigger.cron(**parse_daily_time(daily_expr))
except ValueError as e: except ValueError as e:
raise ValueError(f"时间参数解析错误: {e}") from e raise ValueError(f"时间参数解析错误: {e}") from e
return None return None
@@ -132,12 +137,12 @@ def _parse_trigger_from_arparma(arp: Arparma) -> tuple[str, dict] | None:
async def GetTriggerInfo( async def GetTriggerInfo(
matcher: AlconnaMatcher, matcher: AlconnaMatcher,
arp: Arparma = AlconnaMatches(), arp: Arparma = AlconnaMatches(),
) -> tuple[str, dict]: ) -> BaseTrigger:
"""依赖注入函数:解析并验证时间触发器""" """依赖注入函数:解析并验证时间触发器"""
try: try:
trigger_info = _parse_trigger_from_arparma(arp) trigger = _parse_trigger_from_arparma(arp)
if trigger_info: if trigger:
return trigger_info return trigger
except ValueError as e: except ValueError as e:
await matcher.finish(f"时间参数解析错误: {e}") await matcher.finish(f"时间参数解析错误: {e}")
@@ -199,24 +204,24 @@ async def GetTargeter(
filters["plugin_name"] = plugin_name.result filters["plugin_name"] = plugin_name.result
if global_flag: if global_flag:
filters["target_type"] = "ALL_GROUPS" filters["target_type"] = TargetType.ALL_GROUPS.value
filters["target_identifier"] = scheduler_manager.ALL_GROUPS filters["target_identifier"] = scheduler_manager.ALL_GROUPS
elif user_id.available: elif user_id.available:
filters["target_type"] = "USER" filters["target_type"] = TargetType.USER.value
filters["target_identifier"] = user_id.result filters["target_identifier"] = user_id.result
elif all_enabled: elif all_enabled:
pass pass
elif tag_name.available: elif tag_name.available:
filters["target_type"] = "TAG" filters["target_type"] = TargetType.TAG.value
filters["target_identifier"] = tag_name.result filters["target_identifier"] = tag_name.result
elif group_ids.available: elif group_ids.available:
gids = [str(gid) for gid in group_ids.result] gids = [str(gid) for gid in group_ids.result]
filters["target_type"] = "GROUP" filters["target_type"] = TargetType.GROUP.value
filters["target_identifier__in"] = gids filters["target_identifier__in"] = gids
else: else:
current_group_id = getattr(event, "group_id", None) current_group_id = getattr(event, "group_id", None)
if current_group_id: if current_group_id:
filters["target_type"] = "GROUP" filters["target_type"] = TargetType.GROUP.value
filters["target_identifier"] = str(current_group_id) filters["target_identifier"] = str(current_group_id)
return scheduler_manager.target(**filters) return scheduler_manager.target(**filters)
@@ -230,7 +235,7 @@ async def GetValidatedJobKwargs(
) -> dict: ) -> dict:
"""依赖注入函数:解析、合并和验证任务的关键字参数""" """依赖注入函数:解析、合并和验证任务的关键字参数"""
p_name = plugin_name.result p_name = plugin_name.result
task_meta = scheduler_manager._registered_tasks.get(p_name) task_meta = scheduler_registry.tasks.get(p_name)
if not task_meta: if not task_meta:
await matcher.finish(f"插件 '{p_name}' 未注册可定时执行的任务。") await matcher.finish(f"插件 '{p_name}' 未注册可定时执行的任务。")
@@ -311,7 +316,7 @@ async def GetFinalPermission(
else: else:
base_permission = effective_user_level base_permission = effective_user_level
task_meta = scheduler_manager._registered_tasks.get(plugin_name.result) task_meta = scheduler_registry.tasks.get(plugin_name.result)
if task_meta and "default_permission" in task_meta: if task_meta and "default_permission" in task_meta:
default_perm = task_meta.get("default_permission") default_perm = task_meta.get("default_permission")
if isinstance(default_perm, int): if isinstance(default_perm, int):
@@ -16,6 +16,8 @@ from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import Config from zhenxun.configs.config import Config
from zhenxun.models.scheduled_job import ScheduledJob from zhenxun.models.scheduled_job import ScheduledJob
from zhenxun.services import scheduler_manager from zhenxun.services import scheduler_manager
from zhenxun.services.scheduler.registry import scheduler_registry
from zhenxun.services.scheduler.types import BaseTrigger
from zhenxun.utils.message import MessageUtils from zhenxun.utils.message import MessageUtils
from .commands import schedule_cmd from .commands import schedule_cmd
@@ -66,7 +68,7 @@ async def handle_set(
interval: Match[int] = AlconnaMatch("interval_seconds"), interval: Match[int] = AlconnaMatch("interval_seconds"),
job_name: Match[str] = AlconnaMatch("job_name"), job_name: Match[str] = AlconnaMatch("job_name"),
bot_id_to_operate: str = Depends(GetBotId), bot_id_to_operate: str = Depends(GetBotId),
trigger_info: tuple[str, dict] = Depends(GetTriggerInfo), trigger: BaseTrigger = Depends(GetTriggerInfo),
job_kwargs: dict = Depends(GetValidatedJobKwargs), job_kwargs: dict = Depends(GetValidatedJobKwargs),
creator_permission_level: int = Depends(GetCreatorPermissionLevel), creator_permission_level: int = Depends(GetCreatorPermissionLevel),
final_permission: int = Depends(GetFinalPermission), final_permission: int = Depends(GetFinalPermission),
@@ -86,7 +88,7 @@ async def handle_set(
) )
if is_multi_target: if is_multi_target:
task_meta = scheduler_manager._registered_tasks.get(p_name) task_meta = scheduler_registry.tasks.get(p_name)
if jitter_val is None: if jitter_val is None:
if task_meta and task_meta.get("default_jitter") is not None: if task_meta and task_meta.get("default_jitter") is not None:
jitter_val = cast(int | None, task_meta["default_jitter"]) jitter_val = cast(int | None, task_meta["default_jitter"])
@@ -114,7 +116,7 @@ async def handle_set(
targets=target_groups, targets=target_groups,
creator_permission_level=creator_permission_level, creator_permission_level=creator_permission_level,
plugin_name=p_name, plugin_name=p_name,
trigger_info=trigger_info, trigger=trigger,
job_kwargs=job_kwargs, job_kwargs=job_kwargs,
permission=final_permission, permission=final_permission,
bot_id=bot_id_to_operate, bot_id=bot_id_to_operate,
@@ -210,14 +212,14 @@ async def handle_update(
kwargs_str: Match[str] = AlconnaMatch("kwargs_str"), kwargs_str: Match[str] = AlconnaMatch("kwargs_str"),
): ):
"""处理 '更新' 子命令""" """处理 '更新' 子命令"""
trigger_info = _parse_trigger_from_arparma(arp) trigger = _parse_trigger_from_arparma(arp)
if not trigger_info and not kwargs_str.available: if not trigger and not kwargs_str.available:
await schedule_cmd.finish( await schedule_cmd.finish(
"请提供需要更新的时间 (--cron/--interval/--date/--daily) 或参数 (--kwargs)" "请提供需要更新的时间 (--cron/--interval/--date/--daily) 或参数 (--kwargs)"
) )
result_message = await scheduler_admin_service.update_schedule( result_message = await scheduler_admin_service.update_schedule(
schedule, trigger_info, kwargs_str.result if kwargs_str.available else None schedule, trigger, kwargs_str.result if kwargs_str.available else None
) )
await schedule_cmd.finish(result_message) await schedule_cmd.finish(result_message)
@@ -3,6 +3,8 @@ from typing import Any
from zhenxun import ui from zhenxun import ui
from zhenxun.models.scheduled_job import ScheduledJob from zhenxun.models.scheduled_job import ScheduledJob
from zhenxun.services import scheduler_manager from zhenxun.services import scheduler_manager
from zhenxun.services.scheduler.registry import scheduler_registry
from zhenxun.services.scheduler.types import TargetType
from zhenxun.ui.models import StatusBadgeCell, TextCell from zhenxun.ui.models import StatusBadgeCell, TextCell
from zhenxun.utils.pydantic_compat import model_json_schema from zhenxun.utils.pydantic_compat import model_json_schema
@@ -173,15 +175,15 @@ async def format_schedule_list_as_image(
def format_target_info(target_type: str, target_identifier: str) -> str: def format_target_info(target_type: str, target_identifier: str) -> str:
"""格式化目标信息以供显示""" """格式化目标信息以供显示"""
if target_type == "GLOBAL": if target_type == TargetType.GLOBAL.value:
return "全局" return "全局"
elif target_type == "ALL_GROUPS": elif target_type == TargetType.ALL_GROUPS.value:
return "所有群组" return "所有群组"
elif target_type == "TAG": elif target_type == TargetType.TAG.value:
return f"标签: {target_identifier}" return f"标签: {target_identifier}"
elif target_type == "GROUP": elif target_type == TargetType.GROUP.value:
return f"群: {target_identifier}" return f"群: {target_identifier}"
elif target_type == "USER": elif target_type == TargetType.USER.value:
return f"用户: {target_identifier}" return f"用户: {target_identifier}"
else: else:
return f"{target_type}: {target_identifier}" return f"{target_type}: {target_identifier}"
@@ -215,7 +217,7 @@ async def format_plugins_list() -> str:
message_parts = ["📋 已注册的定时任务插件:"] message_parts = ["📋 已注册的定时任务插件:"]
for i, plugin_name in enumerate(registered_plugins, 1): for i, plugin_name in enumerate(registered_plugins, 1):
task_meta = scheduler_manager._registered_tasks[plugin_name] task_meta = scheduler_registry.tasks[plugin_name]
params_model = task_meta.get("model") params_model = task_meta.get("model")
param_info_str = "无参数" param_info_str = "无参数"
@@ -5,10 +5,13 @@ from .base import (
WrapToolExecuteHandler, WrapToolExecuteHandler,
WrapToolValidateHandler, WrapToolValidateHandler,
) )
from .manager import CapabilityQuery, CapabilitySource, capability
from .wrappers import CombinedCapability, DynamicCapability, WrapperCapability from .wrappers import CombinedCapability, DynamicCapability, WrapperCapability
__all__ = [ __all__ = [
"AbstractCapability", "AbstractCapability",
"CapabilityQuery",
"CapabilitySource",
"CombinedCapability", "CombinedCapability",
"DynamicCapability", "DynamicCapability",
"WrapModelRequestHandler", "WrapModelRequestHandler",
@@ -16,4 +19,5 @@ __all__ = [
"WrapToolExecuteHandler", "WrapToolExecuteHandler",
"WrapToolValidateHandler", "WrapToolValidateHandler",
"WrapperCapability", "WrapperCapability",
"capability",
] ]
+6 -40
View File
@@ -2,20 +2,20 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable, Sequence from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Union from typing import TYPE_CHECKING, Any, Literal, Union
from zhenxun.services.ai.core.messages import ChatRequest, ChatResponse from zhenxun.services.ai.core.messages import ChatRequest, ChatResponse
from zhenxun.services.ai.core.models import LLMContext
from zhenxun.services.ai.core.options import GenerationConfig from zhenxun.services.ai.core.options import GenerationConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.core.models import LLMContext
from zhenxun.services.ai.run import AgentRunResult, RunContext from zhenxun.services.ai.run import AgentRunResult, RunContext
WrapRunHandler = Callable[[], Awaitable["AgentRunResult[Any]"]] WrapRunHandler = Callable[[], Awaitable["AgentRunResult[Any]"]]
"""整个 Agent 运行过程包裹的处理函数类型""" """整个 Agent 运行过程包裹的处理函数类型"""
WrapModelRequestHandler = Callable[ WrapModelRequestHandler = Callable[
["LLMContext[ChatRequest, ChatResponse]"], Awaitable[ChatResponse] [LLMContext[ChatRequest, ChatResponse]], Awaitable[ChatResponse]
] ]
"""单次大模型 API 请求包裹的处理函数类型""" """单次大模型 API 请求包裹的处理函数类型"""
@@ -52,34 +52,14 @@ class CapabilityOrdering:
class AbstractCapability: class AbstractCapability:
""" """
Agent 能力组件基类协议。 Agent 能力组件基类协议。
所有业务逻辑拦截(限流、权限、动态 Prompt)请在此实现。
底层网络重试、并发控制等请勿在此处理。
""" """
@classmethod
def get_serialization_name(cls) -> str | None:
"""用于 YAML/JSON 反序列化的注册标识符"""
return cls.__name__
@classmethod
def from_spec(cls, **kwargs) -> "AbstractCapability":
"""从 Spec 的 kwargs 中实例化对象"""
return cls(**kwargs)
def __init_subclass__(cls, **kwargs):
"""自动将继承此类的所有拦截器注册到中心表"""
super().__init_subclass__(**kwargs)
CapabilityRegistry.register(cls)
def get_ordering(self) -> CapabilityOrdering | None: def get_ordering(self) -> CapabilityOrdering | None:
"""获取该拦截器的拓扑排序约束。子类可重写此方法以锁定执行顺序。""" """获取该拦截器的拓扑排序约束。子类可重写此方法以锁定执行顺序。"""
return None return None
async def for_run(self, context: RunContext) -> "AbstractCapability": async def for_run(self, context: RunContext) -> "AbstractCapability":
"""获取专用于单次运行的实例。 """获取专用于单次运行的实例,默认返回自身(无状态)。"""
默认返回自身(无状态)。
若需要记录单次运行的上下文状态,请返回深/浅拷贝(如 return copy.copy(self))。
"""
return self return self
async def get_generation_config( async def get_generation_config(
@@ -89,9 +69,11 @@ class AbstractCapability:
return None return None
async def get_system_prompts(self, context: RunContext) -> list[str]: async def get_system_prompts(self, context: RunContext) -> list[str]:
"""获取该能力提供的系统提示词列表。"""
return [] return []
async def get_tools(self, context: RunContext) -> list[Any]: async def get_tools(self, context: RunContext) -> list[Any]:
"""获取该能力附带的工具列表。"""
return [] return []
async def prepare_tools( async def prepare_tools(
@@ -135,19 +117,3 @@ class AbstractCapability:
) -> Any: ) -> Any:
"""包裹单一工具的执行 (洋葱模型)。""" """包裹单一工具的执行 (洋葱模型)。"""
return await handler(arguments) return await handler(arguments)
class CapabilityRegistry:
"""Capability 序列化注册表"""
_registry: ClassVar[dict[str, type[AbstractCapability]]] = {}
@classmethod
def register(cls, cap_cls: type[AbstractCapability]):
name = cap_cls.get_serialization_name()
if name:
cls._registry[name] = cap_cls
@classmethod
def get(cls, name: str) -> type[AbstractCapability] | None:
return cls._registry.get(name)
+14 -6
View File
@@ -5,11 +5,6 @@ import json
from typing import Any, cast from typing import Any, cast
from zhenxun.models.user_console import UserConsole from zhenxun.models.user_console import UserConsole
from zhenxun.services.ai.capabilities import (
AbstractCapability,
WrapModelRequestHandler,
WrapToolExecuteHandler,
)
from zhenxun.services.ai.config import get_llm_config from zhenxun.services.ai.config import get_llm_config
from zhenxun.services.ai.core.exceptions import ( from zhenxun.services.ai.core.exceptions import (
AbortException, AbortException,
@@ -33,10 +28,17 @@ from zhenxun.services.ai.core.messages import (
from zhenxun.services.ai.core.models import LLMContext from zhenxun.services.ai.core.models import LLMContext
from zhenxun.services.ai.run.context import RunContext from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.utils import PermissionUtils from zhenxun.services.ai.utils import PermissionUtils
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_capability as logger
from zhenxun.utils.enum import GoldHandle from zhenxun.utils.enum import GoldHandle
from zhenxun.utils.exception import InsufficientGold from zhenxun.utils.exception import InsufficientGold
from .base import (
AbstractCapability,
WrapModelRequestHandler,
WrapToolExecuteHandler,
)
from .manager import capability
def _get_tool_meta(tool: Any, key: str, default: Any = None) -> Any: def _get_tool_meta(tool: Any, key: str, default: Any = None) -> Any:
"""辅助方法:安全地提取工具元数据中指定的键值""" """辅助方法:安全地提取工具元数据中指定的键值"""
@@ -47,6 +49,7 @@ def _get_tool_meta(tool: Any, key: str, default: Any = None) -> Any:
return meta.get(key, default) return meta.get(key, default)
@capability(namespace="global", auto_apply=True)
class StuckDetectionCapability(AbstractCapability): class StuckDetectionCapability(AbstractCapability):
"""死循环检测:使用前置请求拦截防止 LLM 陷入无限重试""" """死循环检测:使用前置请求拦截防止 LLM 陷入无限重试"""
@@ -120,6 +123,7 @@ class StuckDetectionCapability(AbstractCapability):
return await handler(llm_context) return await handler(llm_context)
@capability(namespace="global", auto_apply=True)
class GlobalCycleLimitCapability(AbstractCapability): class GlobalCycleLimitCapability(AbstractCapability):
"""全局防死循环检测中间件:跨 Agent 追踪大模型调用总次数""" """全局防死循环检测中间件:跨 Agent 追踪大模型调用总次数"""
@@ -152,6 +156,7 @@ class GlobalCycleLimitCapability(AbstractCapability):
return await handler(llm_context) return await handler(llm_context)
@capability(namespace="global", auto_apply=True)
class PermissionCapability(AbstractCapability): class PermissionCapability(AbstractCapability):
"""权限校验中间件:在执行前根据确定参数进行动态鉴权""" """权限校验中间件:在执行前根据确定参数进行动态鉴权"""
@@ -182,6 +187,7 @@ class PermissionCapability(AbstractCapability):
return await handler(arguments) return await handler(arguments)
@capability(namespace="global", auto_apply=True)
class BillingCapability(AbstractCapability): class BillingCapability(AbstractCapability):
"""经济系统中间件:执行前扣除金币""" """经济系统中间件:执行前扣除金币"""
@@ -221,6 +227,7 @@ class BillingCapability(AbstractCapability):
return await handler(arguments) return await handler(arguments)
@capability(namespace="global", auto_apply=True)
class ToolRetryAndReflectionCapability(AbstractCapability): class ToolRetryAndReflectionCapability(AbstractCapability):
""" """
重试与自愈反思中间件。 重试与自愈反思中间件。
@@ -267,6 +274,7 @@ class ToolRetryAndReflectionCapability(AbstractCapability):
return ToolResult(output=f"执行发生异常: {e}").as_error() return ToolResult(output=f"执行发生异常: {e}").as_error()
@capability(namespace="global", auto_apply=True)
class ReflexionCapability(AbstractCapability): class ReflexionCapability(AbstractCapability):
"""自愈反思与验证引擎 (Reflexion Engine)。 """自愈反思与验证引擎 (Reflexion Engine)。
统一处理结构化解析失败和语义护栏拦截。""" 统一处理结构化解析失败和语义护栏拦截。"""
+206
View File
@@ -0,0 +1,206 @@
from collections.abc import Callable
from dataclasses import dataclass, field
import fnmatch
from typing import Any, cast
from typing_extensions import Self
from pydantic import BaseModel, Field
from zhenxun.services.ai.utils.logger import log_capability as logger
from zhenxun.services.ai.utils.utils import parse_routing_string
from zhenxun.utils.utils import infer_plugin_namespace
from .base import AbstractCapability
from .wrappers import DynamicCapability
class CapabilityQuery(BaseModel):
"""
拦截器/能力组件的声明式查询对象。
用于在 Agent 中精确或批量筛选加载特定命名空间、特定标签的能力。
"""
name: str | list[str] | None = Field(default=None)
"""如果提供,则能力的名称必须等于该字符串或在列表中。支持 * / ? 通配符。"""
tags: list[str] | None = Field(default=None)
"""如果提供,则能力必须包含这里列出的所有标签 (交集/AND匹配)。"""
exclude_tags: list[str] | None = Field(default=None)
"""如果提供,则能力不能包含这里列出的任何标签 (排斥过滤)。"""
namespace: str | None = Field(default=None)
"""限制搜索的插件命名空间。如果不指定,将自动推导为调用者所在的插件;
'global' 将跨全插件搜索,'*' 代表所有插件。"""
CapabilitySource = (
str | Callable | AbstractCapability | type[AbstractCapability] | CapabilityQuery
)
"""能力/拦截器来源
支持字符串别名/标签、普通函数、Capability类或实例,以及声明式 Query 对象。
"""
@dataclass
class CapabilityEntry:
"""能力组件元数据载体"""
cls: type[AbstractCapability]
"""能力类"""
name: str
"""能力名称"""
namespace: str
"""能力所在的命名空间"""
tags: list[str] = field(default_factory=list)
"""能力标签列表"""
auto_apply: bool = False
"""是否自动挂载该能力"""
class CapabilityManager:
"""能力组件全局注册与发现中心 (单例)"""
_instance: "CapabilityManager | None" = None
_entries: list[CapabilityEntry]
def __new__(cls) -> Self:
"""单例模式:获取或创建全局唯一的能力管理器实例"""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._entries = []
return cast(Self, cls._instance)
def register(
self,
cls: type[AbstractCapability],
name: str,
namespace: str,
tags: list[str],
auto_apply: bool,
) -> None:
"""注册一个能力组件到管理器中"""
self._entries.append(
CapabilityEntry(
cls=cls,
name=name,
namespace=namespace,
tags=tags,
auto_apply=auto_apply,
)
)
tag_str = f" | Tags: {tags}" if tags else ""
logger.debug(
f"已注册 Capability: '{name}' -> Namespace: '{namespace}'{tag_str})"
)
def get_auto_apply_capabilities(self, namespace: str) -> list[AbstractCapability]:
"""获取指定命名空间及其它全局命名空间下自动挂载的能力实例"""
instances = []
for entry in self._entries:
if entry.auto_apply and entry.namespace in ("global", namespace):
try:
instances.append(entry.cls())
except Exception as e:
logger.error(f"实例化自动装配能力 {entry.name} 失败: {e}")
return instances
def query_capabilities(
self, query: CapabilityQuery, default_namespace: str
) -> list[AbstractCapability]:
"""根据声明式查询条件筛选并实例化匹配的能力组件"""
matched = []
ns = query.namespace or default_namespace
for entry in self._entries:
if ns != "*" and entry.namespace != ns:
continue
if query.name:
names = [query.name] if isinstance(query.name, str) else query.name
name_matched = False
for pattern in names:
if fnmatch.fnmatch(entry.name, pattern):
name_matched = True
break
if not name_matched:
continue
if query.tags:
if not all(tag in entry.tags for tag in query.tags):
continue
if query.exclude_tags:
if any(tag in entry.tags for tag in query.exclude_tags):
continue
try:
matched.append(entry.cls())
except Exception as e:
logger.error(f"实例化能力 {entry.name} 失败: {e}")
return matched
def resolve_capabilities(
self, sources: list[Any], default_namespace: str
) -> list[AbstractCapability]:
"""解析多种类型的能力来源并实例化为能力组件列表"""
resolved = []
for source in sources:
if isinstance(source, AbstractCapability):
resolved.append(source)
elif callable(source) and not isinstance(source, type):
resolved.append(DynamicCapability(source))
elif isinstance(source, CapabilityQuery):
resolved.extend(self.query_capabilities(source, default_namespace))
elif isinstance(source, str):
s = cast(str, source)
parsed_args = parse_routing_string(s, default_namespace)
q = CapabilityQuery(**parsed_args)
resolved.extend(self.query_capabilities(q, default_namespace))
elif isinstance(source, type) and issubclass(source, AbstractCapability):
try:
resolved.append(source())
except Exception as e:
logger.error(f"实例化能力 {source.__name__} 失败: {e}")
else:
raise TypeError(f"不支持的 Capability 来源: {type(source)}")
return resolved
capability_manager = CapabilityManager()
def capability(
name: str | None = None,
tags: list[str] | None = None,
auto_apply: bool = False,
namespace: str | None = None,
) -> Callable:
"""
类装饰器:声明式地注册一个 Capability 到全局能力池中。
允许第三方插件通过字符串别名或标签进行引用,彻底解耦模块依赖。
参数:
name: 能力的名称,如果为None则默认使用类名。
tags: 能力的标签列表,用于分类或批量筛选。
auto_apply: 是否自动应用挂载该能力。
namespace: 能力的命名空间,如果为None则自动推导为调用者所在的插件。
返回:
Callable: 装饰器函数,用于包装 AbstractCapability 类。
"""
def decorator(cls: type[AbstractCapability]):
"""装饰器内部函数,实现类注册"""
final_name = name or cls.__name__
final_tags = tags or []
ns = (
namespace
if namespace is not None
else infer_plugin_namespace(default="global")
)
capability_manager.register(
cls, name=final_name, namespace=ns, tags=final_tags, auto_apply=auto_apply
)
return cls
return decorator
+1 -11
View File
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
from nonebot.utils import is_coroutine_callable from nonebot.utils import is_coroutine_callable
from zhenxun.services.ai.core.messages import ChatRequest, ChatResponse from zhenxun.services.ai.core.messages import ChatRequest, ChatResponse
from zhenxun.services.ai.core.models import LLMContext
from zhenxun.services.ai.core.options import GenerationConfig from zhenxun.services.ai.core.options import GenerationConfig
from .base import ( from .base import (
@@ -20,7 +21,6 @@ from .base import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.core.models import LLMContext
from zhenxun.services.ai.run import AgentRunResult, RunContext from zhenxun.services.ai.run import AgentRunResult, RunContext
@@ -266,11 +266,6 @@ class DynamicCapability(AbstractCapability):
"""初始化动态能力""" """初始化动态能力"""
self.capability_func = capability_func self.capability_func = capability_func
@classmethod
def get_serialization_name(cls) -> str | None:
"""获取反序列化标识"""
return None
async def for_run(self, context: RunContext) -> "AbstractCapability": async def for_run(self, context: RunContext) -> "AbstractCapability":
"""在运行时基于当前上下文动态实例化并执行真正的 Capability""" """在运行时基于当前上下文动态实例化并执行真正的 Capability"""
if is_coroutine_callable(self.capability_func): if is_coroutine_callable(self.capability_func):
@@ -292,11 +287,6 @@ class WrapperCapability(AbstractCapability):
"""初始化代理包装器""" """初始化代理包装器"""
self.wrapped = wrapped self.wrapped = wrapped
@classmethod
def get_serialization_name(cls) -> str | None:
"""获取反序列化标识"""
return None
async def for_run(self, context: RunContext) -> "AbstractCapability": async def for_run(self, context: RunContext) -> "AbstractCapability":
"""对内部包裹的实例执行运行时解析并深度克隆""" """对内部包裹的实例执行运行时解析并深度克隆"""
new_wrapped = await self.wrapped.for_run(context) new_wrapped = await self.wrapped.for_run(context)
+1
View File
@@ -24,6 +24,7 @@ def get_default_providers() -> list[dict[str, Any]]:
"models": [ "models": [
{ {
"model_name": "deepseek-v4-pro", "model_name": "deepseek-v4-pro",
"reasoning_effort": "high",
}, },
{ {
"model_name": "deepseek-v4-flash", "model_name": "deepseek-v4-flash",
+2 -4
View File
@@ -107,11 +107,9 @@ class ProviderConfig(BaseModel):
"""API 基础 URL 路径""" """API 基础 URL 路径"""
api_type: str = "openai" api_type: str = "openai"
"""API 协议类型 (openai/gemini/zhipu/etc.)""" """API 协议类型 (openai/gemini/zhipu/etc.)"""
openai_compat: bool = False temperature: float | None = None
"""是否强制使用 OpenAI 兼容模式"""
temperature: float | None = 0.7
"""该提供商下模型的默认温度""" """该提供商下模型的默认温度"""
generation_max_tokens: int | None = None max_output_tokens: int | None = None
"""该提供商下模型的默认最大输出限制""" """该提供商下模型的默认最大输出限制"""
models: list[ModelDetail] models: list[ModelDetail]
"""该提供商提供的具体模型列表""" """该提供商提供的具体模型列表"""
@@ -5,7 +5,7 @@ from typing import Any
from zhenxun.services.ai.tools.core.decorators import tool from zhenxun.services.ai.tools.core.decorators import tool
from zhenxun.services.ai.tools.models import ToolResult from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_knowledge as logger
from .base import BaseKnowledge from .base import BaseKnowledge
@@ -3,7 +3,7 @@ import csv
from pathlib import Path from pathlib import Path
from zhenxun.services.ai.context.rag.models import BaseRecord from zhenxun.services.ai.context.rag.models import BaseRecord
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_knowledge as logger
class BaseReader: class BaseReader:
@@ -5,12 +5,6 @@ import anyio
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from zhenxun.services.ai.context.knowledge.base import BaseKnowledge
from zhenxun.services.ai.context.knowledge.readers import (
BaseReader,
CSVReader,
TextReader,
)
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.context.rag.models import BaseRecord from zhenxun.services.ai.context.rag.models import BaseRecord
from zhenxun.services.ai.core.messages import LLMMessage from zhenxun.services.ai.core.messages import LLMMessage
@@ -18,7 +12,14 @@ from zhenxun.services.ai.llm.api import generate_structured
from zhenxun.services.ai.run import RunContext from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.tools.core.decorators import tool from zhenxun.services.ai.tools.core.decorators import tool
from zhenxun.services.ai.tools.models import ToolkitConfig, ToolResult from zhenxun.services.ai.tools.models import ToolkitConfig, ToolResult
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_knowledge as logger
from .base import BaseKnowledge
from .readers import (
BaseReader,
CSVReader,
TextReader,
)
class QueryAnalysis(BaseModel): class QueryAnalysis(BaseModel):
@@ -312,7 +313,7 @@ class VectorKnowledge(BaseKnowledge):
for result in results: for result in results:
doc_name = result.record.metadata.get("name", "未命名文档") doc_name = result.record.metadata.get("name", "未命名文档")
formatted_results.append( formatted_results.append(
f"📄 来源: {doc_name}\n" f"片段内容:\n{result.record.content}" f"📄 来源: {doc_name}\n片段内容:\n{result.record.content}"
) )
final_text = "\n\n======\n\n".join(formatted_results) final_text = "\n\n======\n\n".join(formatted_results)
+18 -20
View File
@@ -1,33 +1,31 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from typing import TYPE_CHECKING, Any from typing import Any
from typing_extensions import Self from typing_extensions import Self
from pydantic import BaseModel from pydantic import BaseModel
if TYPE_CHECKING:
from zhenxun.services.ai.context.memory.models import MemorySlot
from zhenxun.services.ai.context.memory.storage.interfaces import (
BaseChatContext,
BaseMemoryIngestionMiddleware,
BaseSlotContext,
)
from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.context.memory.compression import MemoryPolicy from zhenxun.services.ai.context.memory.compression import MemoryPolicy
from zhenxun.services.ai.context.memory.models import ( from zhenxun.services.ai.context.memory.models import (
ContextCompressionConfig, ContextCompressionConfig,
IngestionConfig, IngestionConfig,
LongTermConfig, LongTermConfig,
MemoryConfig, MemoryConfig,
MemorySlot,
ShortTermConfig, ShortTermConfig,
SlotMemoryConfig, SlotMemoryConfig,
) )
from zhenxun.services.ai.context.memory.storage.interfaces import (
BaseChatContext,
BaseMemoryIngestionMiddleware,
BaseSlotContext,
)
from zhenxun.services.ai.context.memory.types import ( from zhenxun.services.ai.context.memory.types import (
AutoRecallPolicy, AutoRecallPolicy,
) )
from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.utils.scope import ScopeBuilder from zhenxun.services.ai.utils.scope import ScopeBuilder
@@ -51,7 +49,7 @@ class MemoryBuilder:
) )
@classmethod @classmethod
def auto(cls) -> "MemoryBuilder": def auto(cls) -> MemoryBuilder:
""" """
创建一个开箱即用的默认记忆配置构建器。 创建一个开箱即用的默认记忆配置构建器。
@@ -61,7 +59,7 @@ class MemoryBuilder:
@classmethod @classmethod
def resolve( def resolve(
cls, memory: bool | MemoryConfig | "MemoryBuilder" | None cls, memory: bool | MemoryConfig | MemoryBuilder | None
) -> MemoryConfig: ) -> MemoryConfig:
if isinstance(memory, MemoryConfig): if isinstance(memory, MemoryConfig):
return memory return memory
@@ -85,7 +83,7 @@ class MemoryBuilder:
self, self,
enable: bool = True, enable: bool = True,
isolation: ScopeBuilder | None = None, isolation: ScopeBuilder | None = None,
backend: "str | BaseChatContext | None" = None, backend: str | BaseChatContext | None = None,
) -> Self: ) -> Self:
""" """
配置短期对话历史记忆。 配置短期对话历史记忆。
@@ -107,8 +105,8 @@ class MemoryBuilder:
self, self,
enable: bool = True, enable: bool = True,
scopes: dict[str, ScopeBuilder] | None = None, scopes: dict[str, ScopeBuilder] | None = None,
default_slots: list["MemorySlot"] | None = None, default_slots: list[MemorySlot] | None = None,
backend: "str | BaseSlotContext | None" = None, backend: str | BaseSlotContext | None = None,
instructions: str | None = None, instructions: str | None = None,
) -> Self: ) -> Self:
""" """
@@ -136,9 +134,9 @@ class MemoryBuilder:
self, self,
enable: bool = True, enable: bool = True,
scopes: dict[str, ScopeBuilder] | None = None, scopes: dict[str, ScopeBuilder] | None = None,
engine: "ScopedRAGClient | None" = None, engine: ScopedRAGClient | None = None,
backend: "str | StorageBackend | None" = None, backend: str | StorageBackend | None = None,
embedder: "Embedder | str | None" = None, embedder: Embedder | str | None = None,
agentic: bool = True, agentic: bool = True,
auto_recall: AutoRecallPolicy = False, auto_recall: AutoRecallPolicy = False,
instructions: str | None = None, instructions: str | None = None,
@@ -250,7 +248,7 @@ class MemoryBuilder:
return self return self
def with_ingestion_middlewares( def with_ingestion_middlewares(
self, *middlewares: "BaseMemoryIngestionMiddleware" self, *middlewares: BaseMemoryIngestionMiddleware
) -> Self: ) -> Self:
""" """
配置记忆入库管线中间件。 配置记忆入库管线中间件。
@@ -4,9 +4,6 @@ from typing import Any, Generic, TypeVar
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from zhenxun.services.ai.context.memory.storage.interfaces import (
BaseMemoryReducer,
)
from zhenxun.services.ai.core.engine.token_counter import token_counter from zhenxun.services.ai.core.engine.token_counter import token_counter
from zhenxun.services.ai.core.messages import ( from zhenxun.services.ai.core.messages import (
AudioPart, AudioPart,
@@ -18,9 +15,13 @@ from zhenxun.services.ai.core.messages import (
VideoPart, VideoPart,
) )
from zhenxun.services.ai.llm.manager import get_default_model from zhenxun.services.ai.llm.manager import get_default_model
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_memory as logger
from zhenxun.utils.pydantic_compat import model_copy from zhenxun.utils.pydantic_compat import model_copy
from .storage.interfaces import (
BaseMemoryReducer,
)
class MultimodalPlaceholderReducer(BaseMemoryReducer): class MultimodalPlaceholderReducer(BaseMemoryReducer):
"""视觉媒体降级:将超过一定轮数的老图片/视频替换为 <图片> 占位符文本""" """视觉媒体降级:将超过一定轮数的老图片/视频替换为 <图片> 占位符文本"""
@@ -126,7 +127,7 @@ class MessageDropper(BaseMemoryReducer):
return messages, False, current_tokens return messages, False, current_tokens
logger.info( logger.info(
"✂️ [MemoryCompression] 触发硬截断丢弃策略 | 原因: " "✂️ 触发硬截断丢弃策略 | 原因: "
f"当前 Token 预估 ({current_tokens}) 仍超过硬性上限 ({self.trigger_tokens})," # noqa: E501 f"当前 Token 预估 ({current_tokens}) 仍超过硬性上限 ({self.trigger_tokens})," # noqa: E501
"开始丢弃最旧的历史对话..." "开始丢弃最旧的历史对话..."
) )
@@ -198,9 +199,7 @@ class ToolPrunerReducer(BaseMemoryReducer):
if is_turn_exceeded: if is_turn_exceeded:
reasons.append(f"工具调用轮数超限 ({tool_turns} > {self.max_turns})") reasons.append(f"工具调用轮数超限 ({tool_turns} > {self.max_turns})")
logger.info( logger.info(f"✂️ 触发工具结果修剪策略 | 原因: {' 且 '.join(reasons)}")
f"✂️ [MemoryCompression] 触发工具结果修剪策略 | 原因: {' 且 '.join(reasons)}"
)
from zhenxun.services.ai.core.messages import ToolReturnPart from zhenxun.services.ai.core.messages import ToolReturnPart
+8 -7
View File
@@ -1,17 +1,18 @@
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, cast from typing import Any, cast
from zhenxun.services.ai.context.memory.compression import (
CondenserPipeline,
)
from zhenxun.services.ai.context.memory.manager import memory_manager
from zhenxun.services.ai.context.memory.models import MemoryConfig
from zhenxun.services.ai.context.memory.types import SessionMetadata
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
from zhenxun.services.ai.core.messages import AgentMessage from zhenxun.services.ai.core.messages import AgentMessage
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_memory as logger
from zhenxun.utils.pydantic_compat import model_copy from zhenxun.utils.pydantic_compat import model_copy
from .compression import (
CondenserPipeline,
)
from .manager import memory_manager
from .models import MemoryConfig
from .types import SessionMetadata
class MemoryReader: class MemoryReader:
""" """
+15 -14
View File
@@ -1,29 +1,30 @@
from collections.abc import Sequence from __future__ import annotations
from typing import TYPE_CHECKING, Literal
from collections.abc import Sequence
from typing import Literal
from zhenxun.services.ai.context.memory.types import (
MemorySlot,
SessionMetadata,
)
from zhenxun.services.ai.core.messages import AgentMessage, LLMMessage from zhenxun.services.ai.core.messages import AgentMessage, LLMMessage
if TYPE_CHECKING: from .manager import GlobalMemoryManager
from zhenxun.services.ai.context.memory.manager import GlobalMemoryManager from .storage.interfaces import (
from zhenxun.services.ai.context.memory.storage.interfaces import (
BaseChatContext, BaseChatContext,
BaseSlotContext, BaseSlotContext,
) )
from .types import (
MemorySlot,
SessionMetadata,
)
class ChatHistoryFacade: class ChatHistoryFacade:
"""短期对话历史门面""" """短期对话历史门面"""
def __init__(self, manager: "GlobalMemoryManager", session_meta: SessionMetadata): def __init__(self, manager: GlobalMemoryManager, session_meta: SessionMetadata):
self.manager = manager self.manager = manager
self.session_meta = session_meta self.session_meta = session_meta
@property @property
def _backend(self) -> "BaseChatContext | None": def _backend(self) -> BaseChatContext | None:
return self.manager.get_chat_context( return self.manager.get_chat_context(
None, self.session_meta.namespace or "global" None, self.session_meta.namespace or "global"
) )
@@ -56,12 +57,12 @@ class ChatHistoryFacade:
class SlotFacade: class SlotFacade:
"""中期记忆槽门面""" """中期记忆槽门面"""
def __init__(self, manager: "GlobalMemoryManager", session_meta: SessionMetadata): def __init__(self, manager: GlobalMemoryManager, session_meta: SessionMetadata):
self.manager = manager self.manager = manager
self.session_meta = session_meta self.session_meta = session_meta
@property @property
def _backend(self) -> "BaseSlotContext | None": def _backend(self) -> BaseSlotContext | None:
"""获取底层槽位存储后端""" """获取底层槽位存储后端"""
return self.manager.get_slot_context( return self.manager.get_slot_context(
None, self.session_meta.namespace or "global" None, self.session_meta.namespace or "global"
@@ -115,7 +116,7 @@ class AgentSessionFacade:
提供给第三方开发者的会话记忆访问聚合门面 (Facade)。 提供给第三方开发者的会话记忆访问聚合门面 (Facade)。
""" """
def __init__(self, manager: "GlobalMemoryManager", session_meta: SessionMetadata): def __init__(self, manager: GlobalMemoryManager, session_meta: SessionMetadata):
self.manager = manager self.manager = manager
self.session_meta = session_meta self.session_meta = session_meta
self.history = ChatHistoryFacade(manager, session_meta) self.history = ChatHistoryFacade(manager, session_meta)
+10 -11
View File
@@ -1,18 +1,20 @@
from collections.abc import Callable from collections.abc import Callable
from typing import Any, cast from typing import Any, cast
from zhenxun.services.ai.context.memory.models import MemoryConfig from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.context.memory.storage.backends import ( from zhenxun.services.ai.utils.logger import log_memory as logger
from zhenxun.services.ai.utils.scope import BaseScopeBuilder
from zhenxun.utils.utils import infer_plugin_namespace
from .models import MemoryConfig
from .storage.backends import (
InMemoryChatContext, InMemoryChatContext,
MemoryScope, MemoryScope,
) )
from zhenxun.services.ai.context.memory.storage.interfaces import ( from .storage.interfaces import (
BaseChatContext, BaseChatContext,
BaseSlotContext, BaseSlotContext,
) )
from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.utils.scope import BaseScopeBuilder
from zhenxun.utils.utils import infer_plugin_namespace
class MemoryCleaner(BaseScopeBuilder["MemoryCleaner"]): class MemoryCleaner(BaseScopeBuilder["MemoryCleaner"]):
@@ -64,14 +66,11 @@ class MemoryCleaner(BaseScopeBuilder["MemoryCleaner"]):
async def clear_all(self): async def clear_all(self):
"""一键清理指定范围下的所有生命周期记忆(对话、槽位、RAG)""" """一键清理指定范围下的所有生命周期记忆(对话、槽位、RAG)"""
from zhenxun.services.log import logger
await self.clear_short_term() await self.clear_short_term()
await self.clear_slots() await self.clear_slots()
await self.clear_long_term() await self.clear_long_term()
logger.info( logger.info(
f"🧹 [MemoryCleaner] 成功清理作用域 '{self._selector.scope_prefix}'" f"🧹 成功清理作用域 '{self._selector.scope_prefix}'下的所有记忆痕迹!"
"下的所有记忆痕迹!"
) )
@@ -189,7 +188,7 @@ class GlobalMemoryManager:
if embedder: if embedder:
builder.with_embedder(embedder) builder.with_embedder(embedder)
from zhenxun.services.ai.context.memory.models import MemoryScoringConfig from .models import MemoryScoringConfig
scoring_cfg = MemoryScoringConfig() scoring_cfg = MemoryScoringConfig()
+6 -5
View File
@@ -6,21 +6,22 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.context.memory.storage.interfaces import ( from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.utils.scope import ScopeBuilder
from .storage.interfaces import (
BaseChatContext, BaseChatContext,
BaseMemoryIngestionMiddleware, BaseMemoryIngestionMiddleware,
BaseMemoryReducer, BaseMemoryReducer,
BaseSlotContext, BaseSlotContext,
) )
from zhenxun.services.ai.context.memory.types import ( from .types import (
AutoRecallPolicy, AutoRecallPolicy,
Isolation, Isolation,
MemorySlot, MemorySlot,
SessionMetadata, SessionMetadata,
) )
from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.utils.scope import ScopeBuilder
class SlotMemoryConfig(BaseModel): class SlotMemoryConfig(BaseModel):
@@ -1,26 +1,22 @@
from __future__ import annotations
import asyncio import asyncio
import base64 import base64
from collections.abc import Callable from collections.abc import Callable
import datetime import datetime
from pathlib import Path from pathlib import Path
import time import time
from typing import TYPE_CHECKING, Any, cast from typing import Any, cast
if TYPE_CHECKING:
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from nonebot.utils import is_coroutine_callable from nonebot.utils import is_coroutine_callable
from tortoise import fields from tortoise import fields
from tortoise.timezone import now from tortoise.timezone import now
from zhenxun.services.ai.context.memory.storage.interfaces import (
BaseChatContext,
BaseSlotContext,
)
from zhenxun.services.ai.context.memory.types import ( from zhenxun.services.ai.context.memory.types import (
MemorySlot, MemorySlot,
SessionMetadata, SessionMetadata,
) )
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient
from zhenxun.services.ai.context.rag.models import BaseRecord, SearchResult from zhenxun.services.ai.context.rag.models import BaseRecord, SearchResult
from zhenxun.services.ai.core.messages import ( from zhenxun.services.ai.core.messages import (
AssistantMessage, AssistantMessage,
@@ -35,6 +31,11 @@ from zhenxun.services.ai.utils.scope import ScopeSelector
from zhenxun.services.db_context import Model from zhenxun.services.db_context import Model
from zhenxun.utils.pydantic_compat import TypeAdapter, model_dump from zhenxun.utils.pydantic_compat import TypeAdapter, model_dump
from .interfaces import (
BaseChatContext,
BaseSlotContext,
)
class AbstractMemoryRecord(Model): class AbstractMemoryRecord(Model):
"""Tortoise ORM 短期记忆持久化基类 (Mixin)。""" """Tortoise ORM 短期记忆持久化基类 (Mixin)。"""
@@ -129,7 +130,7 @@ class MemoryScope:
def __init__( def __init__(
self, self,
rag_client: "ScopedRAGClient", rag_client: ScopedRAGClient,
): ):
"""初始化长期记忆作用域与 RAG 客户端""" """初始化长期记忆作用域与 RAG 客户端"""
self.rag_client = rag_client self.rag_client = rag_client
@@ -6,7 +6,7 @@ from typing import Any, Literal, Protocol, runtime_checkable
from zhenxun.services.ai.core.messages import EmbedBatch from zhenxun.services.ai.core.messages import EmbedBatch
from zhenxun.services.ai.llm.api import embed as api_embed from zhenxun.services.ai.llm.api import embed as api_embed
from zhenxun.services.ai.message_builder import MessageBuilder from zhenxun.services.ai.message_builder import MessageBuilder
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_rag as logger
EmbedTaskType = Literal[ EmbedTaskType = Literal[
"general", "query", "document", "similarity", "classification", "clustering" "general", "query", "document", "similarity", "classification", "clustering"
@@ -11,6 +11,7 @@ from zhenxun.services.ai.context.rag.models import (
SearchResult, SearchResult,
) )
from zhenxun.services.ai.context.rag.retrieval import FilterEvaluator from zhenxun.services.ai.context.rag.retrieval import FilterEvaluator
from zhenxun.services.ai.utils.logger import log_rag as logger
from zhenxun.services.ai.utils.scope import ScopeSelector from zhenxun.services.ai.utils.scope import ScopeSelector
from zhenxun.services.db_context import Model from zhenxun.services.db_context import Model
@@ -124,8 +125,6 @@ class DictStorageBackend(StorageBackend):
SearchResult(record=self._records[r_id], score=float(score)) SearchResult(record=self._records[r_id], score=float(score))
) )
except ValueError as e: except ValueError as e:
from zhenxun.services.log import logger
logger.warning( logger.warning(
"⚠️ DictStorage 中缓存的向量维度与当前查询维度不匹配," "⚠️ DictStorage 中缓存的向量维度与当前查询维度不匹配,"
f"跳过向量检索。原因: {e}" f"跳过向量检索。原因: {e}"
@@ -284,8 +283,6 @@ class TortoiseStorageBackend(StorageBackend):
) )
) )
except ValueError as e: except ValueError as e:
from zhenxun.services.log import logger
logger.warning( logger.warning(
"⚠️ 数据库中缓存的向量维度与当前模型查询维度不匹配," "⚠️ 数据库中缓存的向量维度与当前模型查询维度不匹配,"
f"已安全跳过向量检索(降级为稀疏匹配)。原因: {e}" f"已安全跳过向量检索(降级为稀疏匹配)。原因: {e}"
@@ -579,8 +576,6 @@ class LanceDBStorageBackend(StorageBackend):
.to_list() .to_list()
) )
except Exception as e: except Exception as e:
from zhenxun.services.log import logger
logger.warning(f"LanceDB FTS 检索失败(可能是由于尚未创建FTS索引): {e}") logger.warning(f"LanceDB FTS 检索失败(可能是由于尚未创建FTS索引): {e}")
return [] return []
else: else:
+9 -7
View File
@@ -1,9 +1,11 @@
from typing import Any from typing import Any
from zhenxun.services.ai.context.rag.backends import StorageBackend from zhenxun.services.ai.utils.logger import log_rag as logger
from zhenxun.services.ai.context.rag.configs import RAGConfig
from zhenxun.services.ai.context.rag.engine import ScopedRAGClient from .backends import StorageBackend
from zhenxun.services.ai.context.rag.ingestion import ( from .configs import RAGConfig
from .engine import ScopedRAGClient
from .ingestion import (
ChunkingStrategy, ChunkingStrategy,
DedupNode, DedupNode,
DocumentChunking, DocumentChunking,
@@ -12,7 +14,7 @@ from zhenxun.services.ai.context.rag.ingestion import (
IndexPipeline, IndexPipeline,
StorageCommitNode, StorageCommitNode,
) )
from zhenxun.services.ai.context.rag.retrieval import ( from .retrieval import (
BaseRetriever, BaseRetriever,
DatabaseSparseRetriever, DatabaseSparseRetriever,
HybridRetriever, HybridRetriever,
@@ -23,7 +25,6 @@ from zhenxun.services.ai.context.rag.retrieval import (
RerankRetriever, RerankRetriever,
VectorDBRetriever, VectorDBRetriever,
) )
from zhenxun.services.log import logger
class RAGBuilder: class RAGBuilder:
@@ -201,9 +202,10 @@ class RAGBuilder:
embedder = cfg.embedder embedder = cfg.embedder
if not embedder: if not embedder:
from zhenxun.services.ai.context.rag.backends import DefaultEmbedder
from zhenxun.services.ai.llm.manager import get_default_model from zhenxun.services.ai.llm.manager import get_default_model
from .backends import DefaultEmbedder
embedder = DefaultEmbedder(model_name=get_default_model("embedding")) embedder = DefaultEmbedder(model_name=get_default_model("embedding"))
logger.debug("RAGBuilder: 未指定 Embedder,已使用系统默认 Embedder。") logger.debug("RAGBuilder: 未指定 Embedder,已使用系统默认 Embedder。")
+3 -3
View File
@@ -2,9 +2,9 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.context.rag.backends import Embedder, StorageBackend from .backends import Embedder, StorageBackend
from zhenxun.services.ai.context.rag.ingestion import ChunkingStrategy from .ingestion import ChunkingStrategy
from zhenxun.services.ai.context.rag.retrieval import ( from .retrieval import (
BaseRetriever, BaseRetriever,
PostProcessor, PostProcessor,
PreProcessor, PreProcessor,
+7 -6
View File
@@ -1,20 +1,21 @@
from typing import Any from typing import Any
from zhenxun.services.ai.context.rag.backends import ( from zhenxun.services.ai.utils.scope import normalize_scope_path
from .backends import (
StorageBackend, StorageBackend,
) )
from zhenxun.services.ai.context.rag.configs import RAGConfig from .configs import RAGConfig
from zhenxun.services.ai.context.rag.ingestion import ( from .ingestion import (
IndexPipeline, IndexPipeline,
) )
from zhenxun.services.ai.context.rag.models import ( from .models import (
BaseRecord, BaseRecord,
SearchResult, SearchResult,
) )
from zhenxun.services.ai.context.rag.retrieval import ( from .retrieval import (
BaseRetriever, BaseRetriever,
) )
from zhenxun.services.ai.utils.scope import normalize_scope_path
class ScopedRAGClient: class ScopedRAGClient:
+4 -3
View File
@@ -2,9 +2,10 @@ from abc import ABC, abstractmethod
import asyncio import asyncio
import re import re
from zhenxun.services.ai.context.rag.models import BaseRecord from zhenxun.services.ai.utils.logger import log_rag as logger
from zhenxun.services.ai.context.rag.utils import cosine_similarity
from zhenxun.services.log import logger from .models import BaseRecord
from .utils import cosine_similarity
class ChunkingStrategy(ABC): class ChunkingStrategy(ABC):
+4 -3
View File
@@ -3,11 +3,12 @@ import asyncio
import time import time
from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable
from zhenxun.services.ai.context.rag.models import QueryRequest, SearchResult from zhenxun.services.ai.utils.logger import log_rag as logger
from zhenxun.services.log import logger
from .models import QueryRequest, SearchResult
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.context.rag.backends.storages import StorageBackend from .backends.storages import StorageBackend
def normalize_query_text(query: Any) -> str: def normalize_query_text(query: Any) -> str:
+14 -1
View File
@@ -22,19 +22,23 @@ class StablePrefix:
""" """
def __init__(self): def __init__(self):
"""初始化稳定前缀实例。"""
self._snapshot: StablePrefixSnapshot | None = None self._snapshot: StablePrefixSnapshot | None = None
self._version = 0 self._version = 0
@property @property
def fingerprint(self) -> str: def fingerprint(self) -> str:
"""获取当前快照的唯一指纹。"""
return self._snapshot.fingerprint if self._snapshot else "<unbuilt>" return self._snapshot.fingerprint if self._snapshot else "<unbuilt>"
@property @property
def version(self) -> int: def version(self) -> int:
"""获取当前前缀的版本号。"""
return self._version return self._version
@property @property
def built(self) -> bool: def built(self) -> bool:
"""判断当前是否已构建快照。"""
return self._snapshot is not None return self._snapshot is not None
def build(self, system_prompt: list[str], tools: list[Any]) -> bool: def build(self, system_prompt: list[str], tools: list[Any]) -> bool:
@@ -50,9 +54,11 @@ class StablePrefix:
return True return True
def invalidate(self): def invalidate(self):
"""使当前前缀快照失效。"""
self._snapshot = None self._snapshot = None
def to_context(self) -> tuple[list[str], list[Any]]: def to_context(self) -> tuple[list[str], list[Any]]:
"""将前缀快照导出为上下文元组。"""
if not self._snapshot: if not self._snapshot:
raise RuntimeError("StablePrefix.to_context() called before build()") raise RuntimeError("StablePrefix.to_context() called before build()")
return self._snapshot.system_prompt, self._snapshot.tools return self._snapshot.system_prompt, self._snapshot.tools
@@ -60,6 +66,7 @@ class StablePrefix:
def _take_snapshot( def _take_snapshot(
self, system_prompt: list[str], tools: list[Any] self, system_prompt: list[str], tools: list[Any]
) -> StablePrefixSnapshot: ) -> StablePrefixSnapshot:
"""为系统提示词与工具列表生成指纹快照。"""
parsed_tools = [] parsed_tools = []
for t in tools: for t in tools:
if hasattr(t, "name"): if hasattr(t, "name"):
@@ -83,19 +90,24 @@ class AppendOnlyLog:
"""追加写入模式 of 对话日志管理器""" """追加写入模式 of 对话日志管理器"""
def __init__(self): def __init__(self):
"""初始化追加对话日志。"""
self._entries: list[Any] = [] self._entries: list[Any] = []
@property @property
def length(self) -> int: def length(self) -> int:
"""获取当前对话日志的条数。"""
return len(self._entries) return len(self._entries)
def append(self, message: Any): def append(self, message: Any):
"""向对话日志中追加单条消息。"""
self._entries.append(message) self._entries.append(message)
def extend(self, messages: list[Any]): def extend(self, messages: list[Any]):
"""批量追加多条消息到对话日志。"""
self._entries.extend(messages) self._entries.extend(messages)
def clear(self): def clear(self):
"""清空所有对话日志。"""
self._entries.clear() self._entries.clear()
def to_messages(self) -> list[Any]: def to_messages(self) -> list[Any]:
@@ -110,6 +122,7 @@ class AppendOnlyContextManager:
""" """
def __init__(self): def __init__(self):
"""初始化上下文管理器。"""
self.prefix = StablePrefix() self.prefix = StablePrefix()
self.log = AppendOnlyLog() self.log = AppendOnlyLog()
self._last_sync_count = 0 self._last_sync_count = 0
@@ -158,7 +171,7 @@ class AppendOnlyContextManager:
def _compute_digest(self, messages: list[Any]) -> int: def _compute_digest(self, messages: list[Any]) -> int:
"""核心:计算消息列表 of 指纹,用于识别内容篡改。包含 role 与 content。""" """核心:计算消息列表 of 指纹,用于识别内容篡改。包含 role 与 content。"""
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter from .context_renderer import ContextConverter
payloads = [] payloads = []
flattened = ContextConverter.flatten_to_llm_messages(messages) flattened = ContextConverter.flatten_to_llm_messages(messages)
@@ -2,7 +2,7 @@ from collections.abc import Sequence
from typing import Any from typing import Any
from zhenxun.services.ai.core.messages import AgentEvent, AgentMessage, LLMMessage from zhenxun.services.ai.core.messages import AgentEvent, AgentMessage, LLMMessage
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_core as logger
class ContextConverter: class ContextConverter:
@@ -14,7 +14,7 @@ from zhenxun.services.ai.core.models import ToolDefinition
from zhenxun.services.ai.run.models import OutputDataT from zhenxun.services.ai.run.models import OutputDataT
from zhenxun.services.ai.tools.core.tool import BaseTool from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.services.ai.tools.models import StructuredSubmissionResult, ToolResult from zhenxun.services.ai.tools.models import StructuredSubmissionResult, ToolResult
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_core as logger
from zhenxun.utils.pydantic_compat import model_json_schema, model_validate from zhenxun.utils.pydantic_compat import model_json_schema, model_validate
DEFAULT_IVR_TEMPLATE = ( DEFAULT_IVR_TEMPLATE = (
+8 -15
View File
@@ -2,7 +2,7 @@
消息与响应域类型定义 - 统一导出门面 消息与响应域类型定义 - 统一导出门面
""" """
from nonebot.compat import PYDANTIC_V2 from zhenxun.utils.pydantic_compat import model_rebuild
from .context_events import ( from .context_events import (
AgentEvent, AgentEvent,
@@ -59,30 +59,25 @@ from .shared import (
from .types import ( from .types import (
AgentMessage, AgentMessage,
AnyLLMMessage, AnyLLMMessage,
AssistantContentUnion,
ContentT, ContentT,
PromptInput, PromptInput,
RoleT, RoleT,
SystemContentUnion,
ToolContentUnion,
UserContentUnion, UserContentUnion,
) )
if PYDANTIC_V2: model_rebuild(ChatResponse)
ChatResponse.model_rebuild() model_rebuild(LLMMessage)
LLMMessage.model_rebuild() model_rebuild(SystemMessage)
SystemMessage.model_rebuild() model_rebuild(UserMessage)
UserMessage.model_rebuild() model_rebuild(AssistantMessage)
AssistantMessage.model_rebuild() model_rebuild(ToolMessage)
ToolMessage.model_rebuild() model_rebuild(RerankResponse)
RerankResponse.model_rebuild()
__all__ = [ __all__ = [
"AgentEvent", "AgentEvent",
"AgentMessage", "AgentMessage",
"AnyLLMMessage", "AnyLLMMessage",
"AssistantContentUnion",
"AssistantMessage", "AssistantMessage",
"AudioPart", "AudioPart",
"AudioResponse", "AudioResponse",
@@ -112,7 +107,6 @@ __all__ = [
"RerankResult", "RerankResult",
"RoleT", "RoleT",
"SpeechRequest", "SpeechRequest",
"SystemContentUnion",
"SystemMessage", "SystemMessage",
"TaskLifecycleEvent", "TaskLifecycleEvent",
"TextDeltaPart", "TextDeltaPart",
@@ -121,7 +115,6 @@ __all__ = [
"ThoughtPart", "ThoughtPart",
"ToolCallDeltaPart", "ToolCallDeltaPart",
"ToolCallPart", "ToolCallPart",
"ToolContentUnion",
"ToolMessage", "ToolMessage",
"ToolReturnPart", "ToolReturnPart",
"UsageInfo", "UsageInfo",
+1 -1
View File
@@ -7,6 +7,7 @@ from typing_extensions import Self
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from zhenxun.services.ai.utils.logger import log_core as logger
from zhenxun.utils.pydantic_compat import model_validator from zhenxun.utils.pydantic_compat import model_validator
@@ -309,7 +310,6 @@ class EmbedBatch(BaseModel):
def to_text_only(self, context_name: str) -> list[str]: def to_text_only(self, context_name: str) -> list[str]:
"""降级工具:将多模态的批量向量安全剔除图片等内容,回退为纯文本数组""" """降级工具:将多模态的批量向量安全剔除图片等内容,回退为纯文本数组"""
from zhenxun.services.log import logger
texts = [] texts = []
for payload in self.payloads: for payload in self.payloads:
@@ -12,33 +12,12 @@ from .parts import (
ImagePart, ImagePart,
LLMContentPart, LLMContentPart,
TextPart, TextPart,
ThoughtPart,
ToolCallPart,
ToolReturnPart,
VideoPart, VideoPart,
) )
SystemContentUnion = TextPart
"""系统消息允许的内容片段联合类型"""
UserContentUnion = TextPart | ImagePart | AudioPart | VideoPart | FilePart UserContentUnion = TextPart | ImagePart | AudioPart | VideoPart | FilePart
"""用户消息允许的多模态内容片段联合类型""" """用户消息允许的多模态内容片段联合类型"""
AssistantContentUnion = (
TextPart
| ThoughtPart
| ToolCallPart
| ToolReturnPart
| ImagePart
| AudioPart
| VideoPart
| FilePart
)
"""助手回复允许的内容片段联合类型"""
ToolContentUnion = ToolReturnPart | ImagePart | AudioPart | VideoPart | FilePart
"""工具消息允许的内容片段联合类型"""
RoleT = TypeVar("RoleT", default=str, covariant=True) RoleT = TypeVar("RoleT", default=str, covariant=True)
"""泛型:消息参与者角色类型变量""" """泛型:消息参与者角色类型变量"""
@@ -72,12 +51,9 @@ AgentMessage = LLMMessage | AgentEvent
__all__ = [ __all__ = [
"AgentMessage", "AgentMessage",
"AnyLLMMessage", "AnyLLMMessage",
"AssistantContentUnion",
"ContentT", "ContentT",
"LLMContentPart", "LLMContentPart",
"PromptInput", "PromptInput",
"RoleT", "RoleT",
"SystemContentUnion",
"ToolContentUnion",
"UserContentUnion", "UserContentUnion",
] ]
+8 -2
View File
@@ -9,7 +9,7 @@ from typing import Any, Generic, Literal, TypeVar
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.core.options import GenerationConfig from .options import GenerationConfig
ModelName = str | None ModelName = str | None
@@ -131,6 +131,8 @@ class ModelCapabilities(BaseModel):
"""模型支持的输出模态集合。""" """模型支持的输出模态集合。"""
supports_tool_calling: bool = False supports_tool_calling: bool = False
"""是否支持工具调用能力。""" """是否支持工具调用能力。"""
supports_thinking_toggle: bool = False
"""是否支持通过 {"thinking": {"type": "enabled/disabled"}} 显式控制思考模式。"""
is_embedding_model: bool = False is_embedding_model: bool = False
"""是否为嵌入模型。""" """是否为嵌入模型。"""
is_rerank_model: bool = False is_rerank_model: bool = False
@@ -187,7 +189,7 @@ class ModelDetail(BaseModel):
"""模型是否可用。""" """模型是否可用。"""
temperature: float | None = None temperature: float | None = None
"""采样温度参数。""" """采样温度参数。"""
generation_max_tokens: int | None = None max_output_tokens: int | None = None
"""单次生成最大 Token 数。""" """单次生成最大 Token 数。"""
api_type: str | None = None api_type: str | None = None
"""API 类型标识。""" """API 类型标识。"""
@@ -197,6 +199,10 @@ class ModelDetail(BaseModel):
"""显式声明的主任务类型 (如 'image_generation')。""" """显式声明的主任务类型 (如 'image_generation')。"""
path_prefix: str | None = Field(default=None) path_prefix: str | None = Field(default=None)
"""中转路由前缀,例如 '/cogvideox' 或 '/minimax'。""" """中转路由前缀,例如 '/cogvideox' 或 '/minimax'。"""
max_input_tokens: int | None = None
"""最大输入上下文窗口(用于控制记忆压缩策略)"""
reasoning_effort: str | None = None
"""该模型的默认思考/推理等级(如 'high', 'low', 'none')"""
__all__ = [ __all__ = [
-9
View File
@@ -212,13 +212,6 @@ class GeminiOptions(BaseProviderOption):
"""检索定位配置,如 LBS 经纬度信息,配合 Google Maps 工具使用""" """检索定位配置,如 LBS 经纬度信息,配合 Google Maps 工具使用"""
class DeepSeekOptions(BaseProviderOption):
"""DeepSeek 专属特权参数"""
thinking: bool | None = Field(default=None)
"""是否强制开启或关闭 R1 模型的思维链过程"""
class OpenAITTSOptions(BaseProviderOption): class OpenAITTSOptions(BaseProviderOption):
"""OpenAI TTS 专属特权参数""" """OpenAI TTS 专属特权参数"""
@@ -331,8 +324,6 @@ class GenerationConfig(BaseModel):
"""OpenAI 厂商专属请求参数集""" """OpenAI 厂商专属请求参数集"""
gemini_options: GeminiOptions = Field(default_factory=GeminiOptions) gemini_options: GeminiOptions = Field(default_factory=GeminiOptions)
"""Gemini 厂商专属请求参数集""" """Gemini 厂商专属请求参数集"""
deepseek_options: DeepSeekOptions = Field(default_factory=DeepSeekOptions)
"""DeepSeek 厂商专属请求参数集"""
enable_caching: bool | None = Field(default=None) enable_caching: bool | None = Field(default=None)
"""是否在此次生成中开启上下文缓存 (Context Caching)""" """是否在此次生成中开启上下文缓存 (Context Caching)"""
@@ -67,8 +67,6 @@ class ToolProvider(Protocol):
async def discover_tools( async def discover_tools(
self, self,
allowed_servers: list[str] | None = None,
excluded_servers: list[str] | None = None,
) -> dict[str, ToolExecutable]: ) -> dict[str, ToolExecutable]:
""" """
异步发现此提供者提供的所有工具。 异步发现此提供者提供的所有工具。
+1 -1
View File
@@ -8,7 +8,7 @@ from typing import Any, TypeVar
from nonebot.utils import is_coroutine_callable from nonebot.utils import is_coroutine_callable
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_core as logger
class AgentStreamEvent(BaseModel): class AgentStreamEvent(BaseModel):
+1 -1
View File
@@ -4,7 +4,7 @@ from typing_extensions import TypeVar
from jinja2 import Environment from jinja2 import Environment
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_core as logger
from zhenxun.utils.pydantic_compat import model_dump from zhenxun.utils.pydantic_compat import model_dump
if TYPE_CHECKING: if TYPE_CHECKING:
+10 -22
View File
@@ -5,9 +5,8 @@ from pathlib import Path
from typing import Any, Generic, cast from typing import Any, Generic, cast
from zhenxun.services.ai.capabilities import ( from zhenxun.services.ai.capabilities import (
AbstractCapability, CapabilitySource,
CombinedCapability, CombinedCapability,
DynamicCapability,
) )
from zhenxun.services.ai.config import get_llm_config from zhenxun.services.ai.config import get_llm_config
from zhenxun.services.ai.context.knowledge.base import BaseKnowledge from zhenxun.services.ai.context.knowledge.base import BaseKnowledge
@@ -67,7 +66,7 @@ from zhenxun.services.ai.tools.providers.skills.capabilities import (
) )
from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource
from zhenxun.services.ai.utils import ContextUtils from zhenxun.services.ai.utils import ContextUtils
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.pydantic_compat import ( from zhenxun.utils.pydantic_compat import (
model_construct, model_construct,
model_copy, model_copy,
@@ -78,7 +77,9 @@ from zhenxun.utils.utils import infer_plugin_namespace
from .engine.builders import ( from .engine.builders import (
AgentProfileResolver, AgentProfileResolver,
CapabilityBuilder,
ContextBuilder, ContextBuilder,
SessionBuilder,
ToolBuilder, ToolBuilder,
) )
from .engine.executor import BaseAgentExecutor, StandardAgentExecutor from .engine.executor import BaseAgentExecutor, StandardAgentExecutor
@@ -94,9 +95,6 @@ ToolSource = (
) )
"""任何可以作为工具提供给大模型的实体对象(函数、基础工具类、字典定义、工具名、工具箱、声明式查询对象)""" """任何可以作为工具提供给大模型的实体对象(函数、基础工具类、字典定义、工具名、工具箱、声明式查询对象)"""
CapabilitySource = Callable | AbstractCapability
"""能力/拦截器来源(函数或 AbstractCapability 实例)"""
class AgentBuilder(Generic[AgentDepsT, OutputDataT]): class AgentBuilder(Generic[AgentDepsT, OutputDataT]):
""" """
@@ -148,7 +146,7 @@ class AgentBuilder(Generic[AgentDepsT, OutputDataT]):
return self return self
def with_tools( def with_tools(
self, *tools: ToolSource | list[ToolSource] self, *tools: ToolSource | Sequence[ToolSource]
) -> "AgentBuilder[AgentDepsT, OutputDataT]": ) -> "AgentBuilder[AgentDepsT, OutputDataT]":
""" """
配置可供智能体调用的工具列表。 配置可供智能体调用的工具列表。
@@ -158,7 +156,7 @@ class AgentBuilder(Generic[AgentDepsT, OutputDataT]):
""" """
current_tools = self._kwargs.setdefault("tools", []) current_tools = self._kwargs.setdefault("tools", [])
for t in tools: for t in tools:
if isinstance(t, list): if isinstance(t, Sequence) and not isinstance(t, str):
current_tools.extend(t) current_tools.extend(t)
else: else:
current_tools.append(t) current_tools.append(t)
@@ -353,7 +351,7 @@ class Agent(
description: str | None = None, description: str | None = None,
persona: Persona | dict | None = None, persona: Persona | dict | None = None,
model: str | Callable[[], str] | None = None, model: str | Callable[[], str] | None = None,
tools: list[ToolSource] | None = None, tools: Sequence[ToolSource] | None = None,
skills: Sequence[str | Path | Skill | SkillSource] | None = None, skills: Sequence[str | Path | Skill | SkillSource] | None = None,
generation_config: GenerationConfig | IntentBuilder | dict | None = None, generation_config: GenerationConfig | IntentBuilder | dict | None = None,
response_model: BaseOutputDefinition | type[OutputDataT] | None = None, response_model: BaseOutputDefinition | type[OutputDataT] | None = None,
@@ -458,14 +456,14 @@ class Agent(
def _assemble_plugins(self, tools, knowledge, capabilities, skills): def _assemble_plugins(self, tools, knowledge, capabilities, skills):
"""私有方法:集中处理各类能力、知识与技能的挂载,消解冗余样板代码""" """私有方法:集中处理各类能力、知识与技能的挂载,消解冗余样板代码"""
self.tool_definitions = tools or [] self.tool_definitions = list(tools) if tools else []
if knowledge: if knowledge:
if not isinstance(knowledge, list): if not isinstance(knowledge, list):
knowledge = [knowledge] knowledge = [knowledge]
self.tool_definitions.extend(knowledge) self.tool_definitions.extend(knowledge)
self.capabilities: list[AbstractCapability] = [] self.capabilities: list[CapabilitySource] = []
if self.memory_config.long_term.enable and self.memory_config.long_term.agentic: if self.memory_config.long_term.enable and self.memory_config.long_term.agentic:
self.capabilities.append( self.capabilities.append(
@@ -478,11 +476,7 @@ class Agent(
) )
if capabilities: if capabilities:
for cap in capabilities: self.capabilities.extend(capabilities)
if isinstance(cap, AbstractCapability):
self.capabilities.append(cap)
elif callable(cap):
self.capabilities.append(DynamicCapability(cap))
if self.config.enable_hitl: if self.config.enable_hitl:
self.tool_definitions.append(HITLToolkit()) self.tool_definitions.append(HITLToolkit())
@@ -789,11 +783,6 @@ class Agent(
**kwargs: Any, **kwargs: Any,
) -> tuple[AgentState, AgentRunResources]: ) -> tuple[AgentState, AgentRunResources]:
"""解析任务意图,初始化隔离域与基础状态载体""" """解析任务意图,初始化隔离域与基础状态载体"""
from zhenxun.services.ai.flow.agent.engine.builders import (
AgentProfileResolver,
CapabilityBuilder,
SessionBuilder,
)
if context is None: if context is None:
raise ValueError("RunContext 不能为空") raise ValueError("RunContext 不能为空")
@@ -900,7 +889,6 @@ class Agent(
toolset_funcs=getattr(self, "toolset_funcs", []), toolset_funcs=getattr(self, "toolset_funcs", []),
system_tools=[], system_tools=[],
namespace=self.namespace or "unknown", namespace=self.namespace or "unknown",
tool_filter=resources.config.tool_filter,
run_context=context, run_context=context,
run_scoped_cap=run_scoped_cap, run_scoped_cap=run_scoped_cap,
) )
+19 -10
View File
@@ -1,6 +1,7 @@
import asyncio import asyncio
from typing import Any, Generic, cast from typing import Any, Generic, cast
from typing_extensions import TypeVar from typing_extensions import TypeVar
import uuid
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
@@ -16,8 +17,9 @@ from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.run import AgentRunResult, RunContext from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunError from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunError
from zhenxun.services.ai.run.ui import UIController from zhenxun.services.ai.run.ui import UIController
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.message import MessageUtils from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
T_Deps = TypeVar("T_Deps", default=Any) T_Deps = TypeVar("T_Deps", default=Any)
T_Out = TypeVar("T_Out", default=str) T_Out = TypeVar("T_Out", default=str)
@@ -46,8 +48,6 @@ class AgentRunner(Generic[T_Out]):
) )
if is_stateless and self.context.session_id: if is_stateless and self.context.session_id:
if not self.context.session_id.startswith("stateless_"): if not self.context.session_id.startswith("stateless_"):
import uuid
self.context.session_id = ( self.context.session_id = (
f"stateless_{self.context.session_id}_{uuid.uuid4().hex[:8]}" f"stateless_{self.context.session_id}_{uuid.uuid4().hex[:8]}"
) )
@@ -126,15 +126,24 @@ class AgentRunner(Generic[T_Out]):
await MessageUtils.build_message(f"❌ 运行发生错误: {e}").send() await MessageUtils.build_message(f"❌ 运行发生错误: {e}").send()
raise e raise e
if final_result and final_result.output and self._bot and self._event: if final_result and final_result.output and self._bot:
if isinstance(final_result.output, UniMessage): msg_to_send = (
await final_result.output.send( final_result.output
self._event, bot=self._bot, reply_to=reply_to if isinstance(final_result.output, UniMessage)
else MessageUtils.build_message(str(final_result.output))
) )
final_result.output = final_result.output.extract_plain_text() if self._event:
await msg_to_send.send(self._event, bot=self._bot, reply_to=reply_to)
else: else:
final_msg = str(final_result.output) target = PlatformUtils.get_target(
await MessageUtils.build_message(final_msg).send() user_id=self.context.get_user_id(),
group_id=self.context.get_group_id(),
)
if target:
await msg_to_send.send(target=target, bot=self._bot)
if isinstance(final_result.output, UniMessage):
final_result.output = final_result.output.extract_plain_text()
if final_result is None: if final_result is None:
raise RuntimeError("智能体运行流异常结束:未返回最终结果。") raise RuntimeError("智能体运行流异常结束:未返回最终结果。")
@@ -12,7 +12,7 @@ from zhenxun.services.ai.core.messages import TaskLifecycleEvent
from zhenxun.services.ai.core.options import BaseOutputDefinition, ToolOutput from zhenxun.services.ai.core.options import BaseOutputDefinition, ToolOutput
from zhenxun.services.ai.guardrails import parse_guardrails from zhenxun.services.ai.guardrails import parse_guardrails
from zhenxun.services.ai.run import AgentRunResult, AgentTask, RunContext from zhenxun.services.ai.run import AgentRunResult, AgentTask, RunContext
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_agent as logger
class OutputValidationCapability(AbstractCapability): class OutputValidationCapability(AbstractCapability):
@@ -6,9 +6,7 @@ from typing import Any, cast
from nonebot.utils import is_coroutine_callable from nonebot.utils import is_coroutine_callable
from zhenxun.services.ai.capabilities import ( from zhenxun.services.ai.capabilities import (
AbstractCapability,
CombinedCapability, CombinedCapability,
DynamicCapability,
) )
from zhenxun.services.ai.context.memory.builder import MemoryBuilder 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.engine import MemoryReader, MemoryWriter
@@ -22,13 +20,13 @@ from zhenxun.services.ai.flow.agent.capabilities import (
TaskTrackingCapability, TaskTrackingCapability,
) )
from zhenxun.services.ai.flow.agent.models import Persona from zhenxun.services.ai.flow.agent.models import Persona
from zhenxun.services.ai.run import GLOBAL_CAPABILITIES, RunContext from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.run.di import DependencyInjector from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.ai.tools.engine.registry import ( from zhenxun.services.ai.tools.engine.registry import (
ToolCollection, ToolCollection,
tool_provider_manager, tool_provider_manager,
) )
from zhenxun.services.ai.tools.models import GlobalToolFilter, ResolvedToolPayload from zhenxun.services.ai.tools.models import ResolvedToolPayload
from zhenxun.services.ai.utils.scope import ScopeSelector from zhenxun.services.ai.utils.scope import ScopeSelector
from zhenxun.utils.pydantic_compat import model_copy from zhenxun.utils.pydantic_compat import model_copy
@@ -135,22 +133,21 @@ class CapabilityBuilder:
if task_obj: if task_obj:
dynamic_caps.append(TaskTrackingCapability(task_obj, agent_name)) dynamic_caps.append(TaskTrackingCapability(task_obj, agent_name))
run_level_caps = [] from zhenxun.services.ai.capabilities.manager import capability_manager
if profile_capabilities:
for cap in profile_capabilities:
if isinstance(cap, AbstractCapability):
run_level_caps.append(cap)
elif callable(cap):
run_level_caps.append(DynamicCapability(cap))
base_caps = GLOBAL_CAPABILITIES.get("global", []).copy() run_level_caps = capability_manager.resolve_capabilities(
if namespace != "global" and namespace in GLOBAL_CAPABILITIES: profile_capabilities or [], namespace
base_caps.extend(GLOBAL_CAPABILITIES[namespace]) )
agent_level_caps = capability_manager.resolve_capabilities(
agent_capabilities or [], namespace
)
auto_caps = capability_manager.get_auto_apply_capabilities(namespace)
combined_cap = CombinedCapability( combined_cap = CombinedCapability(
base_caps auto_caps
+ getattr(context, "capabilities", []) + getattr(context, "capabilities", [])
+ agent_capabilities + agent_level_caps
+ run_level_caps + run_level_caps
+ dynamic_caps + dynamic_caps
) )
@@ -298,7 +295,6 @@ class ToolBuilder:
toolset_funcs: list[Any], toolset_funcs: list[Any],
system_tools: list[Any], system_tools: list[Any],
namespace: str, namespace: str,
tool_filter: GlobalToolFilter | None,
run_context: RunContext, run_context: RunContext,
run_scoped_cap: CombinedCapability, run_scoped_cap: CombinedCapability,
) -> ResolvedToolPayload: ) -> ResolvedToolPayload:
@@ -309,9 +305,8 @@ class ToolBuilder:
参数: 参数:
tool_definitions: 静态工具或工具集合的定义列表。 tool_definitions: 静态工具或工具集合的定义列表。
toolset_funcs: 待依赖注入解析的工具集生成函数列表。 toolset_funcs: 待依赖注入解析的工具集生成函数列表。
system_tools: 系统默认强制集成的工具定义列表。 system_tools: 系统默认强制集成的工具定义列表.
namespace: 会话命名空间。 namespace: 会话命名空间。
tool_filter: 全局的工具过滤条件,此处为占位。
run_context: 运行上下文对象实例. run_context: 运行上下文对象实例.
run_scoped_cap: 运行域下的合并能力中间件,用于提供特定的能力工具。 run_scoped_cap: 运行域下的合并能力中间件,用于提供特定的能力工具。
@@ -411,14 +406,14 @@ class ToolBuilder:
current_tool_defs = list(_cap_res) current_tool_defs = list(_cap_res)
final_defs_map = {d.name.lower(): d for d in current_tool_defs if d} final_defs_map = {d.name.lower(): d for d in current_tool_defs if d}
final_effective_tools = ToolCollection() final_effective_tools_list = []
for t_exec in effective_tools: for t_exec in effective_tools:
t_name = getattr(t_exec, "name", "unknown") t_name = getattr(t_exec, "name", "unknown")
if t_name.lower() in final_defs_map: if t_name.lower() in final_defs_map:
cloned_tool = copy.copy(t_exec) cloned_tool = copy.copy(t_exec)
cloned_tool._dynamic_def = final_defs_map[t_name.lower()] cloned_tool._dynamic_def = final_defs_map[t_name.lower()]
final_effective_tools.append(cloned_tool) final_effective_tools_list.append(cloned_tool)
return final_effective_tools return ToolCollection(final_effective_tools_list)
class SessionBuilder: class SessionBuilder:
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
from zhenxun.services.ai.run.models import AgentRunResult, HandoffPayload from zhenxun.services.ai.run.models import AgentRunResult, HandoffPayload
from zhenxun.services.ai.tools.models import ToolResult from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.pydantic_compat import model_construct from zhenxun.utils.pydantic_compat import model_construct
from zhenxun.utils.utils import infer_plugin_namespace from zhenxun.utils.utils import infer_plugin_namespace
@@ -63,7 +63,7 @@ async def handle_submit_structured(
parsed_obj = ( parsed_obj = (
tool_res.directive.payload.get("parsed_obj") if tool_res.directive else None tool_res.directive.payload.get("parsed_obj") if tool_res.directive else None
) )
logger.info("✅ 拦截到结构化结果提交,结束循环。") logger.debug("✅ 拦截到结构化结果提交,结束循环。")
state.is_finished = True state.is_finished = True
state.final_result = model_construct( state.final_result = model_construct(
AgentRunResult, AgentRunResult,
@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import asyncio import asyncio
import json import json
from typing import Any, cast from typing import Any
from zhenxun.services.ai.capabilities import CombinedCapability from zhenxun.services.ai.capabilities import CombinedCapability
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
@@ -15,7 +15,6 @@ from zhenxun.services.ai.core.exceptions import (
) )
from zhenxun.services.ai.core.messages import ( from zhenxun.services.ai.core.messages import (
AgentMessage, AgentMessage,
AssistantContentUnion,
AssistantMessage, AssistantMessage,
AudioPart, AudioPart,
ChatRequest, ChatRequest,
@@ -35,19 +34,20 @@ from zhenxun.services.ai.core.stream_events import (
LLMStartEvent, LLMStartEvent,
ToolStreamChunkEvent, ToolStreamChunkEvent,
) )
from zhenxun.services.ai.flow.agent.engine.directive import (
DirectiveHandlerFunc,
directive_manager,
)
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
from zhenxun.services.ai.run import AgentRunResult, RunContext from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.session import session_manager from zhenxun.services.ai.run.session import session_manager
from zhenxun.services.ai.tools.engine.executor import ToolExecutor from zhenxun.services.ai.tools.engine.executor import ToolExecutor
from zhenxun.services.ai.tools.models import ToolResult from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.pydantic_compat import dump_json_safely, model_construct from zhenxun.utils.pydantic_compat import dump_json_safely, model_construct
from .directive import (
DirectiveHandlerFunc,
directive_manager,
)
class BaseAgentExecutor(ABC): class BaseAgentExecutor(ABC):
""" """
@@ -219,8 +219,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
messages, run_context messages, run_context
) )
if run_context.run.event_bus: await run_context.run.emit(
await run_context.run.event_bus.emit(
LLMStartEvent( LLMStartEvent(
model_name=resources.model_name or "unknown", model_name=resources.model_name or "unknown",
messages=flattened_messages, messages=flattened_messages,
@@ -238,8 +237,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
cancellation_token=cancellation_token, cancellation_token=cancellation_token,
) )
if run_context.run.event_bus: await run_context.run.emit(LLMEndEvent(response=response))
await run_context.run.event_bus.emit(LLMEndEvent(response=response))
assistant_content = ( assistant_content = (
response.content_parts if response.content_parts else response.text response.content_parts if response.content_parts else response.text
@@ -252,9 +250,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
part.metadata["thought_signature"] = response.thought_signature part.metadata["thought_signature"] = response.thought_signature
break break
assistant_message = AssistantMessage( assistant_message = AssistantMessage(content=response.content_parts)
content=cast(list[AssistantContentUnion], response.content_parts)
)
if hasattr(response, "parsed_obj") and response.parsed_obj is not None: if hasattr(response, "parsed_obj") and response.parsed_obj is not None:
if not isinstance(response.parsed_obj, str): if not isinstance(response.parsed_obj, str):
@@ -376,7 +372,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
state.messages, resources.model_name or "", base_overhead=0 state.messages, resources.model_name or "", base_overhead=0
) )
logger.debug( logger.debug(
f"[TokenTracker] (Iter {state.current_cycle + 1}) " f"(Iter {state.current_cycle + 1}) "
f"预估将消耗 {est_tokens} Token " f"预估将消耗 {est_tokens} Token "
f"(Model: {resources.model_name or 'Unknown'})" f"(Model: {resources.model_name or 'Unknown'})"
) )
@@ -444,7 +440,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
return return
if not response.tool_calls: if not response.tool_calls:
logger.debug("✅ AgentExecutor:模型未请求工具调用,推理循环结束。") logger.debug("✅ 模型未请求工具调用,推理循环结束。")
state.is_finished = True state.is_finished = True
state.final_result = model_construct( state.final_result = model_construct(
AgentRunResult, AgentRunResult,
@@ -476,8 +472,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
if is_server_side: if is_server_side:
logger.debug( logger.debug(
"☁️ [AgentExecutor] 检测到云端工具调用: " f"☁️ 检测到云端工具调用: {call.tool_name},已跳过本地执行。"
f"{call.tool_name},已跳过本地执行。"
) )
async with self.tool_executor._tool_stream_scope( async with self.tool_executor._tool_stream_scope(
event_bus, event_bus,
@@ -500,7 +495,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
client_tool_calls.append(call) client_tool_calls.append(call)
if not client_tool_calls: if not client_tool_calls:
logger.info("✅ AgentExecutor:无本地客户端工具需执行,推理循环平滑结束。") logger.info("✅ 无本地客户端工具需执行,推理循环平滑结束。")
state.is_finished = True state.is_finished = True
state.final_result = model_construct( state.final_result = model_construct(
@@ -638,7 +633,6 @@ class StandardAgentExecutor(BaseAgentExecutor):
self, state: AgentState, resources: AgentRunResources self, state: AgentState, resources: AgentRunResources
) -> AgentRunResult[Any]: ) -> AgentRunResult[Any]:
run_context = resources.run_context run_context = resources.run_context
event_bus = run_context.run.event_bus
if not resources.config.enable_fallback_summary: if not resources.config.enable_fallback_summary:
raise UpstreamServerException( raise UpstreamServerException(
@@ -646,12 +640,10 @@ class StandardAgentExecutor(BaseAgentExecutor):
) )
logger.warning( logger.warning(
f"AgentExecutor 达到最大循环次数 ({resources.config.max_cycles})," f"达到最大循环次数 ({resources.config.max_cycles}),触发兜底总结机制。"
"触发兜底总结机制。"
) )
if event_bus: await run_context.run.emit(
await event_bus.emit(
ToolStreamChunkEvent( ToolStreamChunkEvent(
tool_name="System", tool_name="System",
content="⏳ 思考过程过于复杂,正在强制生成最终总结...", content="⏳ 思考过程过于复杂,正在强制生成最终总结...",
+19 -16
View File
@@ -1,12 +1,15 @@
""" from __future__ import annotations
Agent 相关静态声明类型定义
"""
from collections.abc import Sequence from collections.abc import Sequence
from pathlib import Path
from typing import Any from typing import Any
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.capabilities import CapabilitySource, 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.context.memory.types import SessionMetadata
from zhenxun.services.ai.core.messages import ( from zhenxun.services.ai.core.messages import (
AgentMessage, AgentMessage,
@@ -18,8 +21,9 @@ from zhenxun.services.ai.core.messages import (
from zhenxun.services.ai.core.options import GenerationConfig from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.flow.base import BaseRuntimeConfig from zhenxun.services.ai.flow.base import BaseRuntimeConfig
from zhenxun.services.ai.run import RunContext from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.run.models import AgentRunResult, AgentTask, HandoffPayload
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.engine.registry import ToolCollection from zhenxun.services.ai.tools.engine.registry import ToolCollection
from zhenxun.services.ai.tools.models import GlobalToolFilter
from zhenxun.utils.pydantic_compat import model_copy from zhenxun.utils.pydantic_compat import model_copy
@@ -60,15 +64,14 @@ class AgentConfig(BaseRuntimeConfig):
message_history: Sequence[AgentMessage] | None = Field(default=None) message_history: Sequence[AgentMessage] | None = Field(default=None)
"""初始化的底层对话历史记录。""" """初始化的底层对话历史记录。"""
tool_filter: GlobalToolFilter | None = Field(default=None)
"""全局工具过滤器,限制本次运行可用的工具池。""" memory: MemoryConfig | MemoryBuilder | bool | None = Field(default=None)
memory: Any | None = Field(default=None)
"""单次运行级别的记忆门面覆盖 (支持 bool, MemoryConfig, MemoryBuilder)。""" """单次运行级别的记忆门面覆盖 (支持 bool, MemoryConfig, MemoryBuilder)。"""
generation_config: GenerationConfig | None = Field(default=None) generation_config: GenerationConfig | None = Field(default=None)
"""单次运行覆盖的大模型生成配置。""" """单次运行覆盖的大模型生成配置。"""
capabilities: list[Any] | None = Field(default=None) capabilities: list[CapabilitySource] | None = Field(default=None)
"""仅针对本次运行动态注入的临时拦截器/能力组件列表。""" """仅针对本次运行动态注入的临时拦截器/能力组件列表。"""
skills: Sequence[Any] | None = Field(default=None) skills: Sequence[str | Path | Any] | None = Field(default=None)
"""仅针对本次运行动态注入的临时技能集合。""" """仅针对本次运行动态注入的临时技能集合。"""
executor: Any | None = Field(default=None) executor: Any | None = Field(default=None)
"""单次运行覆盖的核心执行引擎策略 (BaseAgentExecutor)。""" """单次运行覆盖的核心执行引擎策略 (BaseAgentExecutor)。"""
@@ -128,11 +131,11 @@ class AgentState(BaseModel):
"""拦截到的早期终止输出结果""" """拦截到的早期终止输出结果"""
should_terminate: bool = False should_terminate: bool = False
"""标记是否应提前终止循环""" """标记是否应提前终止循环"""
handoff_triggered: Any | None = None handoff_triggered: HandoffPayload | None = None
"""标记是否触发了移交""" """标记是否触发了移交"""
is_finished: bool = False is_finished: bool = False
"""标记大模型循环是否彻底结束""" """标记大模型循环是否彻底结束"""
final_result: Any | None = None final_result: AgentRunResult[Any] | None = None
"""最终的运行结果 (AgentRunResult)""" """最终的运行结果 (AgentRunResult)"""
origin_msg_len: int = 0 origin_msg_len: int = 0
"""初始进入循环时的消息历史长度 (用于增量保存记忆)""" """初始进入循环时的消息历史长度 (用于增量保存记忆)"""
@@ -160,15 +163,15 @@ class AgentRunResources(BaseModel):
"""保留依赖注入(DI)与黑板引用的全局运行时上下文""" """保留依赖注入(DI)与黑板引用的全局运行时上下文"""
session_meta: SessionMetadata | None = None session_meta: SessionMetadata | None = None
"""隔离会话的元信息(Session ID, 命名空间, 权限等)""" """隔离会话的元信息(Session ID, 命名空间, 权限等)"""
memory_reader: Any | None = None memory_reader: MemoryReader | None = None
"""用于读取短/中/长期上下文记忆的读取器""" """用于读取短/中/长期上下文记忆的读取器"""
memory_writer: Any | None = None memory_writer: MemoryWriter | None = None
"""用于将对话历史安全落盘的写入器""" """用于将对话历史安全落盘的写入器"""
run_scoped_cap: Any | None = None run_scoped_cap: CombinedCapability | None = None
"""聚合了 Agent/AgentTask/全局 的复合能力拦截器 (CombinedCapability)""" """聚合了 Agent/AgentTask/全局 的复合能力拦截器 (CombinedCapability)"""
task_obj: Any | None = None task_obj: AgentTask | None = None
"""(如有) 解析后的结构化数据任务契约""" """(如有) 解析后的结构化数据任务契约"""
toolkits: list[Any] = Field(default_factory=list) toolkits: list[BaseToolkit] = Field(default_factory=list)
"""当前轮次生效的工具箱列表 (需要执行生命周期挂载)""" """当前轮次生效的工具箱列表 (需要执行生命周期挂载)"""
config: AgentConfig = Field(default_factory=AgentConfig) config: AgentConfig = Field(default_factory=AgentConfig)
"""Agent 全局与运行时的统一策略配置""" """Agent 全局与运行时的统一策略配置"""
+9 -7
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import asyncio import asyncio
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
@@ -7,12 +9,14 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from zhenxun.services.ai.core.exceptions import ControlFlowExit
from zhenxun.services.ai.run.context import RunContext from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.run.models import StreamedRunResult
from zhenxun.services.ai.run.ui import UIController from zhenxun.services.ai.run.ui import UIController
from zhenxun.services.ai.utils.logger import log_flow as logger
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.flow.agent.models import Persona from .agent.models import Persona
from zhenxun.services.ai.run.models import StreamedRunResult
from zhenxun.services.ai.core.messages import PromptInput from zhenxun.services.ai.core.messages import PromptInput
@@ -92,7 +96,7 @@ class BaseRunnable(ABC, Generic[T_RunResult]):
"""DI 注入语法糖:返回 Depends,自动绑定当前上下文""" """DI 注入语法糖:返回 Depends,自动绑定当前上下文"""
from nonebot.params import Depends from nonebot.params import Depends
from zhenxun.services.ai.flow.agent.bridge import AgentRunner from .agent.bridge import AgentRunner
async def _dependency() -> AgentRunner[Any]: async def _dependency() -> AgentRunner[Any]:
return AgentRunner[Any](self, **kwargs) return AgentRunner[Any](self, **kwargs)
@@ -108,7 +112,7 @@ class BaseRunnable(ABC, Generic[T_RunResult]):
**kwargs: Any, **kwargs: Any,
) -> T_RunResult: ) -> T_RunResult:
"""交互执行语法糖,自动渲染流式进度并最终将结果回复给终端用户""" """交互执行语法糖,自动渲染流式进度并最终将结果回复给终端用户"""
from zhenxun.services.ai.flow.agent.bridge import AgentRunner from .agent.bridge import AgentRunner
runner = AgentRunner(self, context=context, **kwargs) runner = AgentRunner(self, context=context, **kwargs)
return cast(T_RunResult, await runner.reply(prompt=prompt, reply_to=reply_to)) return cast(T_RunResult, await runner.reply(prompt=prompt, reply_to=reply_to))
@@ -121,8 +125,6 @@ class BaseRunnable(ABC, Generic[T_RunResult]):
**kwargs: Any, **kwargs: Any,
) -> T_RunResult: ) -> T_RunResult:
"""阻塞式核心运行入口,安全捕获内部抛出的静默退出信号""" """阻塞式核心运行入口,安全捕获内部抛出的静默退出信号"""
from zhenxun.services.ai.core.exceptions import ControlFlowExit
from zhenxun.services.log import logger
try: try:
async with self.run_stream( async with self.run_stream(
@@ -144,6 +146,6 @@ class BaseRunnable(ABC, Generic[T_RunResult]):
*, *,
context: RunContext | None = None, context: RunContext | None = None,
**kwargs: Any, **kwargs: Any,
) -> "AsyncIterator[StreamedRunResult[Any]]": ) -> AsyncIterator[StreamedRunResult[Any]]:
"""流式运行入口,返回上下文管理器,用于消费底层执行流事件 (StreamedRunResult)""" """流式运行入口,返回上下文管理器,用于消费底层执行流事件 (StreamedRunResult)"""
yield cast(Any, None) yield cast(Any, None)
+1 -1
View File
@@ -9,7 +9,7 @@ from zhenxun.services.ai.core.exceptions import (
from zhenxun.services.ai.core.models import CancellationToken from zhenxun.services.ai.core.models import CancellationToken
from zhenxun.services.ai.run.models import AgentTask from zhenxun.services.ai.run.models import AgentTask
from zhenxun.services.ai.run.session import LockContext, session_manager from zhenxun.services.ai.run.session import LockContext, session_manager
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_flow as logger
from .base import ConcurrencyPolicy, InterventionPolicy from .base import ConcurrencyPolicy, InterventionPolicy
@@ -6,8 +6,11 @@ from nonebot.utils import is_coroutine_callable
from zhenxun.services.ai.capabilities import AbstractCapability from zhenxun.services.ai.capabilities import AbstractCapability
from zhenxun.services.ai.run import RunContext from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.ai.tools.bridges.handoff import HandoffTool from zhenxun.services.ai.tools.bridges.handoff import HandoffTool
from .models import Transition
class TeamRoutingCapability(AbstractCapability): class TeamRoutingCapability(AbstractCapability):
"""团队路由能力组件:动态向所有团队成员""" """团队路由能力组件:动态向所有团队成员"""
@@ -17,10 +20,12 @@ class TeamRoutingCapability(AbstractCapability):
team_name: str, team_name: str,
members: list[Any], members: list[Any],
state_flow: Mapping[str, Sequence[Any]] | Callable | None = None, state_flow: Mapping[str, Sequence[Any]] | Callable | None = None,
max_handoffs: int = 3,
): ):
self.team_name = team_name self.team_name = team_name
self.members = members self.members = members
self.state_flow = state_flow self.state_flow = state_flow
self.max_handoffs = max_handoffs
async def _get_allowed_transitions(self, context: RunContext) -> list[Any] | None: async def _get_allowed_transitions(self, context: RunContext) -> list[Any] | None:
"""核心FSM解析:解析静态字典或动态执行函数获取允许的 Transition 列表""" """核心FSM解析:解析静态字典或动态执行函数获取允许的 Transition 列表"""
@@ -34,15 +39,12 @@ class TeamRoutingCapability(AbstractCapability):
current_speaker, current_speaker,
[m.name for m in self.members if m.name != current_speaker], [m.name for m in self.members if m.name != current_speaker],
) )
from zhenxun.services.ai.flow.team.models import Transition
return [ return [
Transition(target=t) if isinstance(t, str) else t for t in raw_targets Transition(target=t) if isinstance(t, str) else t for t in raw_targets
] ]
if callable(self.state_flow): if callable(self.state_flow):
from zhenxun.services.ai.run.di import DependencyInjector
sig = inspect.signature(self.state_flow) sig = inspect.signature(self.state_flow)
kwargs = await DependencyInjector.resolve_all( kwargs = await DependencyInjector.resolve_all(
sig, call_kwargs={}, context=context sig, call_kwargs={}, context=context
@@ -55,7 +57,6 @@ class TeamRoutingCapability(AbstractCapability):
if result is None: if result is None:
return None return None
from zhenxun.services.ai.flow.team.models import Transition
return [Transition(target=t) if isinstance(t, str) else t for t in result] return [Transition(target=t) if isinstance(t, str) else t for t in result]
@@ -97,6 +98,7 @@ class TeamRoutingCapability(AbstractCapability):
target_name=m.name, target_name=m.name,
target_description=desc, target_description=desc,
input_schema=input_schema, input_schema=input_schema,
max_handoffs=self.max_handoffs,
) )
) )
return tools return tools
+7 -4
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from enum import Enum from enum import Enum
from typing import Any from typing import Any
@@ -7,7 +9,8 @@ from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.core.messages import AgentMessage from zhenxun.services.ai.core.messages import AgentMessage
from zhenxun.services.ai.core.options import BaseOutputDefinition from zhenxun.services.ai.core.options import BaseOutputDefinition
from zhenxun.services.ai.flow.base import BaseRuntimeConfig from zhenxun.services.ai.flow.base import BaseRunnable, BaseRuntimeConfig
from zhenxun.services.ai.run import AgentTask
class TeamRuntimeConfig(BaseRuntimeConfig): class TeamRuntimeConfig(BaseRuntimeConfig):
@@ -45,7 +48,7 @@ class Transition(BaseModel):
trigger_regex: str | None = None trigger_regex: str | None = None
"""(可选) 正则表达式。 """(可选) 正则表达式。
如果用户的输入匹配此正则,将触发极速硬路由,跳过大模型思考。""" 如果用户的输入匹配此正则,将触发极速硬路由,跳过大模型思考。"""
trigger_func: Callable[..., Any] | None = None trigger_func: Callable[..., bool | str | None] | None = None
"""(可选) 自定义校验函数。返回 True 或目标名称时触发硬路由。支持依赖注入。""" """(可选) 自定义校验函数。返回 True 或目标名称时触发硬路由。支持依赖注入。"""
model_config = ConfigDict(arbitrary_types_allowed=True) model_config = ConfigDict(arbitrary_types_allowed=True)
@@ -62,9 +65,9 @@ class CallAction(TeamAction):
调度动作:呼叫指定的 Agent 执行任务 调度动作:呼叫指定的 Agent 执行任务
""" """
agent: str | Any agent: str | BaseRunnable[Any]
"""目标 Agent 的名称(字符串)或动态生成的 Agent 实例""" """目标 Agent 的名称(字符串)或动态生成的 Agent 实例"""
task: str | Any task: str | AgentTask
"""派发给该 Agent 的具体任务或提示词""" """派发给该 Agent 的具体任务或提示词"""
history: Sequence[AgentMessage] | None = None history: Sequence[AgentMessage] | None = None
"""需要传递给该 Agent 的上下文历史记录(可选)""" """需要传递给该 Agent 的上下文历史记录(可选)"""
+12 -4
View File
@@ -8,10 +8,11 @@ from nonebot.utils import is_coroutine_callable
from zhenxun.services.ai.core.messages import AgentMessage from zhenxun.services.ai.core.messages import AgentMessage
from zhenxun.services.ai.core.templates import PromptTemplate from zhenxun.services.ai.core.templates import PromptTemplate
from zhenxun.services.ai.flow.team.models import RouteDecision, Transition
from zhenxun.services.ai.run import AgentTask, RunContext from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.run.di import DependencyInjector from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_team as logger
from .models import RouteDecision, Transition
class BaseRouter(ABC): class BaseRouter(ABC):
@@ -148,6 +149,7 @@ class LLMRouter(BaseRouter):
runtime_config: Any = None, runtime_config: Any = None,
custom_prompt: str | None = None, custom_prompt: str | None = None,
allowed_transitions: list[Transition] | None = None, allowed_transitions: list[Transition] | None = None,
max_handoffs: int = 3,
): ):
""" """
初始化基于大模型的意图路由器。 初始化基于大模型的意图路由器。
@@ -161,6 +163,7 @@ class LLMRouter(BaseRouter):
runtime_config: 团队级别的运行时全局配置。 runtime_config: 团队级别的运行时全局配置。
custom_prompt: 自定义的系统提示词模板,用以覆盖默认的路由系统指令。 custom_prompt: 自定义的系统提示词模板,用以覆盖默认的路由系统指令。
allowed_transitions: 允许的状态移交规则与前置条件列表。 allowed_transitions: 允许的状态移交规则与前置条件列表。
max_handoffs: 同一会话中允许连续移交的最大次数。
""" """
self.team_name = team_name self.team_name = team_name
self.members = members self.members = members
@@ -170,6 +173,7 @@ class LLMRouter(BaseRouter):
self.runtime_config = runtime_config self.runtime_config = runtime_config
self.custom_prompt = custom_prompt self.custom_prompt = custom_prompt
self.allowed_transitions = allowed_transitions self.allowed_transitions = allowed_transitions
self.max_handoffs = max_handoffs
async def route( async def route(
self, self,
@@ -179,7 +183,8 @@ class LLMRouter(BaseRouter):
) -> RouteDecision | None: ) -> RouteDecision | None:
from zhenxun.services.ai.flow.agent.agent import Agent from zhenxun.services.ai.flow.agent.agent import Agent
from zhenxun.services.ai.flow.agent.models import AgentConfig from zhenxun.services.ai.flow.agent.models import AgentConfig
from zhenxun.services.ai.flow.team.capabilities import TeamRoutingCapability
from .capabilities import TeamRoutingCapability
default_system_prompt = """## 角色与目标 default_system_prompt = """## 角色与目标
你是一个高级任务路由器 (所在团队: {{ team_name }})。 你是一个高级任务路由器 (所在团队: {{ team_name }})。
@@ -200,7 +205,10 @@ class LLMRouter(BaseRouter):
route_prompt = PromptTemplate(template).render(team_name=self.team_name) route_prompt = PromptTemplate(template).render(team_name=self.team_name)
routing_cap = TeamRoutingCapability( routing_cap = TeamRoutingCapability(
team_name=self.team_name, members=self.members, state_flow=self.state_flow team_name=self.team_name,
members=self.members,
state_flow=self.state_flow,
max_handoffs=self.max_handoffs,
) )
leader_config = AgentConfig( leader_config = AgentConfig(
+11 -12
View File
@@ -8,17 +8,19 @@ from zhenxun.services.ai.core.exceptions import (
LLMException, LLMException,
) )
from zhenxun.services.ai.core.messages import UsageInfo from zhenxun.services.ai.core.messages import UsageInfo
from zhenxun.services.ai.flow.team.capabilities import TeamRoutingCapability from zhenxun.services.ai.flow.agent.models import AgentConfig
from zhenxun.services.ai.flow.team.models import ( from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.models import AgentRunEnd
from zhenxun.services.ai.utils.logger import log_team as logger
from zhenxun.utils.pydantic_compat import model_construct
from .capabilities import TeamRoutingCapability
from .models import (
CallAction, CallAction,
ConcurrentCallAction, ConcurrentCallAction,
FinishAction, FinishAction,
) )
from zhenxun.services.ai.flow.team.strategy import BaseTeamStrategy from .strategy import BaseTeamStrategy, RouteStrategy
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.models import AgentRunEnd
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import model_construct
class TeamRunner: class TeamRunner:
@@ -47,7 +49,7 @@ class TeamRunner:
(m for m in self.team.members if m.name == action.agent), None (m for m in self.team.members if m.name == action.agent), None
) )
if not target_agent: if not target_agent:
logger.error(f"❌ [TeamRunner] 找不到团队成员: {action.agent}") logger.error(f"❌ 找不到团队成员: {action.agent}")
await queue.put( await queue.put(
( (
"result", "result",
@@ -67,13 +69,12 @@ class TeamRunner:
sub_context = context.clone_for_member(target_agent.name) sub_context = context.clone_for_member(target_agent.name)
sub_context.capabilities = list(sub_context.capabilities) sub_context.capabilities = list(sub_context.capabilities)
from zhenxun.services.ai.flow.team.strategy import RouteStrategy
if isinstance(self.strategy, RouteStrategy): if isinstance(self.strategy, RouteStrategy):
routing_cap = TeamRoutingCapability( routing_cap = TeamRoutingCapability(
team_name=self.team.name, team_name=self.team.name,
members=self.team.members, members=self.team.members,
state_flow=getattr(self.strategy, "state_flow", None), state_flow=getattr(self.strategy, "state_flow", None),
max_handoffs=getattr(self.strategy, "max_handoffs", 3),
) )
sub_context.capabilities.append(routing_cap) sub_context.capabilities.append(routing_cap)
@@ -82,8 +83,6 @@ class TeamRunner:
agent_res = None agent_res = None
try: try:
from zhenxun.services.ai.flow.agent.models import AgentConfig
async with target_agent.run_stream( async with target_agent.run_stream(
prompt=action.task, prompt=action.task,
context=sub_context, context=sub_context,
+52 -42
View File
@@ -10,19 +10,26 @@ from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent
from zhenxun.services.ai.core.templates import PromptTemplate from zhenxun.services.ai.core.templates import PromptTemplate
from zhenxun.services.ai.flow.agent.agent import Agent, ToolSource from zhenxun.services.ai.flow.agent.agent import Agent, ToolSource
from zhenxun.services.ai.flow.agent.models import AgentConfig from zhenxun.services.ai.flow.agent.models import AgentConfig
from zhenxun.services.ai.flow.team.models import ( from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.run.blackboard import BlackboardManager
from zhenxun.services.ai.tools.bridges.delegate import DelegateTool
from zhenxun.services.ai.tools.providers.builtin.blackboard import BlackboardToolkit
from zhenxun.services.ai.utils.logger import log_team as logger
from .models import (
CallAction, CallAction,
ConcurrentCallAction, ConcurrentCallAction,
FinishAction, FinishAction,
TaskBoardState,
TaskNodeStatus,
TeamAction, TeamAction,
Transition,
) )
from zhenxun.services.ai.flow.team.router import BaseRouter from .router import BaseRouter
from zhenxun.services.ai.run import AgentTask, RunContext from .task_tools import TaskPlanningToolkit
from zhenxun.services.ai.tools.bridges.delegate import DelegateTool
from zhenxun.services.log import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.flow.team.team import Team from .team import Team
class BaseTeamStrategy(ABC): class BaseTeamStrategy(ABC):
@@ -106,6 +113,7 @@ class RouteStrategy(BaseTeamStrategy):
leader_model: str | None = None, leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None, leader_tools: list[ToolSource] | None = None,
custom_prompt: str | None = None, custom_prompt: str | None = None,
max_handoffs: int = 3,
): ):
""" """
路由策略初始化,基于挂载的 Router 进行最合适的专家分发。 路由策略初始化,基于挂载的 Router 进行最合适的专家分发。
@@ -117,16 +125,16 @@ class RouteStrategy(BaseTeamStrategy):
leader_model: 路由节点 (Leader) 使用的大模型名称,若为空则默认继承全局。 leader_model: 路由节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给路由节点 (Leader) 的专属工具列表。 leader_tools: 挂载给路由节点 (Leader) 的专属工具列表。
custom_prompt: 自定义系统提示词,用于覆盖默认的路由系统提示词。 custom_prompt: 自定义系统提示词,用于覆盖默认的路由系统提示词。
max_handoffs: 同一会话中允许连续移交的最大次数。
""" """
super().__init__(custom_prompt=custom_prompt) super().__init__(custom_prompt=custom_prompt)
self.selector_func = selector_func self.selector_func = selector_func
self.router = router self.router = router
self.leader_model = leader_model self.leader_model = leader_model
self.leader_tools = leader_tools or [] self.leader_tools = leader_tools or []
self.max_handoffs = max_handoffs
if isinstance(state_flow, dict): if isinstance(state_flow, dict):
from zhenxun.services.ai.flow.team.models import Transition
normalized_flow = {} normalized_flow = {}
for k, targets in state_flow.items(): for k, targets in state_flow.items():
normalized_targets = [] normalized_targets = []
@@ -163,6 +171,7 @@ class RouteStrategy(BaseTeamStrategy):
state_flow=self.state_flow, state_flow=self.state_flow,
runtime_config=getattr(team, "runtime_config", None), runtime_config=getattr(team, "runtime_config", None),
custom_prompt=self.custom_prompt, custom_prompt=self.custom_prompt,
max_handoffs=self.max_handoffs,
) )
) )
router = ChainRouter(routers) router = ChainRouter(routers)
@@ -171,13 +180,11 @@ class RouteStrategy(BaseTeamStrategy):
exec_config = kwargs.get("config") exec_config = kwargs.get("config")
max_cycles = getattr(exec_config, "max_cycles", 15) if exec_config else 15 max_cycles = getattr(exec_config, "max_cycles", 15) if exec_config else 15
logger.info(f"🛣️ [RouteStrategy] '{team.name}' 正在获取初始路由决策...") logger.debug(f"🛣️ '{team.name}' 正在获取初始路由决策...")
decision = await router.route(context, [], prompt) decision = await router.route(context, [], prompt)
if not decision: if not decision:
logger.warning( logger.warning(f"🚨 Team '{team.name}' 的所有路由策略未能命中目标。")
f"🚨 [RouteStrategy] Team '{team.name}' 的所有路由策略未能命中目标。"
)
raise AbortException( raise AbortException(
reason=f"Team '{team.name}' 无法找到合适的路由节点处理该任务", reason=f"Team '{team.name}' 无法找到合适的路由节点处理该任务",
display="🚨 团队协作失败,无法分配任务。", display="🚨 团队协作失败,无法分配任务。",
@@ -191,7 +198,7 @@ class RouteStrategy(BaseTeamStrategy):
cycle_count += 1 cycle_count += 1
if cycle_count > max_cycles: if cycle_count > max_cycles:
logger.error( logger.error(
f"🚨 [RouteStrategy] Team '{team.name}' 路由陷入死循环!" f"🚨 Team '{team.name}' 路由陷入死循环!"
f"已达到最大限制 {max_cycles} 次。" f"已达到最大限制 {max_cycles} 次。"
) )
raise AbortException( raise AbortException(
@@ -229,7 +236,7 @@ class RouteStrategy(BaseTeamStrategy):
run_result = yield CallAction( run_result = yield CallAction(
agent=current_target, agent=current_target,
task=prompt, task=prompt or "",
history=handoff_history_messages, history=handoff_history_messages,
kwargs=kwargs, kwargs=kwargs,
) )
@@ -268,7 +275,7 @@ class RouteStrategy(BaseTeamStrategy):
pass pass
if fast_routed: if fast_routed:
logger.info( logger.debug(
f"🛣️ **路由决策**: 委派给专员 👨💼`{current_target}`" f"🛣️ **路由决策**: 委派给专员 👨💼`{current_target}`"
"(系统拦截:正则/函数状态流发生转移)" "(系统拦截:正则/函数状态流发生转移)"
) )
@@ -291,18 +298,21 @@ class CoordinateStrategy(BaseTeamStrategy):
leader_model: str | None = None, leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None, leader_tools: list[ToolSource] | None = None,
custom_prompt: str | None = None, custom_prompt: str | None = None,
max_delegations: int = 3,
): ):
""" """
协作策略初始化,Leader 主动拆解任务,委派给 Sub-Agents 并汇总结果。 协作策略初始化,Leader 主动拆解任务,委派给 Sub-Agents 并汇总结果。
参数: 参数:
leader_model: 协调节点 (Leader) 使用的大模型名称,若为空则默认继承全局。 leader_model: 协调节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给协调节点 (Leader) 的专属工具列表。 leader_tools: 挂载给协调节点 (Leader) 的专属工具列表.
custom_prompt: 自定义系统提示词,用于覆盖默认的协调系统提示词。 custom_prompt: 自定义系统提示词,用于覆盖默认的协调系统提示词。
max_delegations: 允许向同一个专员连续委派失败的最大重试次数。
""" """
super().__init__(custom_prompt=custom_prompt) super().__init__(custom_prompt=custom_prompt)
self.leader_model = leader_model self.leader_model = leader_model
self.leader_tools = leader_tools or [] self.leader_tools = leader_tools or []
self.max_delegations = max_delegations
async def generate_plan( async def generate_plan(
self, self,
@@ -323,6 +333,7 @@ class CoordinateStrategy(BaseTeamStrategy):
runnable=m, runnable=m,
name=f"delegate_to_{m.name}", name=f"delegate_to_{m.name}",
description=f"将子任务委派给专员 [{m.name}] 处理。专长:{desc}", description=f"将子任务委派给专员 [{m.name}] 处理。专长:{desc}",
max_delegations=self.max_delegations,
) )
) )
@@ -338,8 +349,7 @@ class CoordinateStrategy(BaseTeamStrategy):
logger.debug(f"✨ **团队 [{team.name}] Leader** 正在汇总各方报告...") logger.debug(f"✨ **团队 [{team.name}] Leader** 正在汇总各方报告...")
if context.run.event_bus: await context.run.emit(
await context.run.event_bus.emit(
ToolStreamChunkEvent( ToolStreamChunkEvent(
tool_name="Team Leader", tool_name="Team Leader",
content="✨ 团队 Leader 正在汇总各方报告...", content="✨ 团队 Leader 正在汇总各方报告...",
@@ -348,7 +358,7 @@ class CoordinateStrategy(BaseTeamStrategy):
logger.debug(f"👨💼 [CoordinateStrategy] '{team.name}' 正在启动协调推理循环...") logger.debug(f"👨💼 [CoordinateStrategy] '{team.name}' 正在启动协调推理循环...")
leader_res = yield CallAction(agent=leader_agent, task=prompt) leader_res = yield CallAction(agent=leader_agent, task=prompt or "")
yield FinishAction(result=leader_res) yield FinishAction(result=leader_res)
@@ -389,8 +399,7 @@ class BroadcastStrategy(BaseTeamStrategy):
prompt.description if isinstance(prompt, AgentTask) else (prompt or "") prompt.description if isinstance(prompt, AgentTask) else (prompt or "")
) )
if context.run.event_bus: await context.run.emit(
await context.run.event_bus.emit(
ToolStreamChunkEvent( ToolStreamChunkEvent(
tool_name="Team Broadcaster", tool_name="Team Broadcaster",
content=f"🚀 正在并发广播任务给 {len(team.members)} 位专家...", content=f"🚀 正在并发广播任务给 {len(team.members)} 位专家...",
@@ -400,8 +409,7 @@ class BroadcastStrategy(BaseTeamStrategy):
actions = [CallAction(agent=m.name, task=task_desc_str) for m in team.members] actions = [CallAction(agent=m.name, task=task_desc_str) for m in team.members]
results = yield ConcurrentCallAction(actions=actions) results = yield ConcurrentCallAction(actions=actions)
if context.run.event_bus: await context.run.emit(
await context.run.event_bus.emit(
ToolStreamChunkEvent( ToolStreamChunkEvent(
tool_name="Team Leader", tool_name="Team Leader",
content="✨ 所有专家汇报完毕,Leader 正在融合各方观点...", content="✨ 所有专家汇报完毕,Leader 正在融合各方观点...",
@@ -451,8 +459,7 @@ class TaskStrategy(BaseTeamStrategy):
leader_model: str | None = None, leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None, leader_tools: list[ToolSource] | None = None,
max_iterations: int = 15, max_iterations: int = 15,
blackboard_schema: type[BaseModel] | None = None, blackboard: type[BaseModel] | BaseModel | None = None,
initial_blackboard_state: BaseModel | None = None,
custom_prompt: str | None = None, custom_prompt: str | None = None,
): ):
""" """
@@ -463,10 +470,9 @@ class TaskStrategy(BaseTeamStrategy):
leader_model: 规划节点 (Leader) 使用的大模型名称,若为空则默认继承全局。 leader_model: 规划节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给规划节点 (Leader) 的专属附加工具列表。 leader_tools: 挂载给规划节点 (Leader) 的专属附加工具列表。
max_iterations: 引擎驱动的状态机最大迭代/循环次数,防止死循环。 max_iterations: 引擎驱动的状态机最大迭代/循环次数,防止死循环。
blackboard_schema: 团队共享黑板的数据结构类型 (Pydantic Model 类)。 blackboard: (可选) 团队共享黑板。可传入 Schema 类型类,或直接传入带有初始数据的 Schema 实例对象。
initial_blackboard_state: 共享黑板的初始数据状态实例。
custom_prompt: 自定义系统提示词,用于覆盖默认的规划系统提示词。 custom_prompt: 自定义系统提示词,用于覆盖默认的规划系统提示词。
""" """ # noqa: E501
super().__init__(custom_prompt=custom_prompt) super().__init__(custom_prompt=custom_prompt)
self.leader_model = leader_model self.leader_model = leader_model
self.leader_tools = leader_tools or [] self.leader_tools = leader_tools or []
@@ -474,14 +480,21 @@ class TaskStrategy(BaseTeamStrategy):
self.blackboard = None self.blackboard = None
self.bb_toolkit = None self.bb_toolkit = None
if blackboard_schema is not None: if blackboard is not None:
from zhenxun.services.ai.run.blackboard import BlackboardManager schema = None
from zhenxun.services.ai.tools.providers.builtin.blackboard import ( initial_state = None
BlackboardToolkit, if isinstance(blackboard, type) and issubclass(blackboard, BaseModel):
schema = blackboard
elif isinstance(blackboard, BaseModel):
schema = type(blackboard)
initial_state = blackboard
else:
raise ValueError(
"blackboard 参数必须是 Pydantic BaseModel 的子类(类型)或其实例"
) )
self.blackboard = BlackboardManager( self.blackboard = BlackboardManager(
schema=blackboard_schema, initial_state=initial_blackboard_state schema=schema, initial_state=initial_state
) )
self.bb_toolkit = BlackboardToolkit(self.blackboard) self.bb_toolkit = BlackboardToolkit(self.blackboard)
self.leader_tools.append(self.bb_toolkit) self.leader_tools.append(self.bb_toolkit)
@@ -493,9 +506,6 @@ class TaskStrategy(BaseTeamStrategy):
context: RunContext, context: RunContext,
**kwargs, **kwargs,
) -> AsyncGenerator[TeamAction, Any]: ) -> AsyncGenerator[TeamAction, Any]:
from zhenxun.services.ai.flow.team.models import TaskBoardState, TaskNodeStatus
from zhenxun.services.ai.flow.team.task_tools import TaskPlanningToolkit
if self.blackboard is not None: if self.blackboard is not None:
context.session.blackboard = self.blackboard context.session.blackboard = self.blackboard
@@ -541,9 +551,7 @@ class TaskStrategy(BaseTeamStrategy):
) )
if "__task_board__" not in context.session.shared_state: if "__task_board__" not in context.session.shared_state:
context.session.shared_state["__task_board__"] = ( context.session.shared_state["__task_board__"] = TaskBoardState()
self.blackboard._state if self.blackboard else TaskBoardState()
)
board = cast(TaskBoardState, context.session.shared_state["__task_board__"]) board = cast(TaskBoardState, context.session.shared_state["__task_board__"])
max_iterations = self.max_iterations max_iterations = self.max_iterations
@@ -576,15 +584,17 @@ class TaskStrategy(BaseTeamStrategy):
请检查是否有 failed 的任务需要修复重新指派?或者如果所有任务均已 completed, 请检查是否有 failed 的任务需要修复重新指派?或者如果所有任务均已 completed,
请立刻调用 `mark_all_complete` 汇报总结。""" 请立刻调用 `mark_all_complete` 汇报总结。"""
logger.info(f"🧠 [TaskStrategy] 唤醒 Planner (Iter: {iteration})") logger.debug(f"🧠 [TaskStrategy] 唤醒 Planner (Iter: {iteration})")
leader_res = yield CallAction(agent=leader_agent, task=planner_prompt) leader_res = yield CallAction(
agent=leader_agent, task=planner_prompt or ""
)
if board.is_goal_complete: if board.is_goal_complete:
yield FinishAction(result=board.final_summary or leader_res.output) yield FinishAction(result=board.final_summary or leader_res.output)
return return
continue continue
logger.info( logger.debug(
f"🚀 [TaskStrategy] 引擎接管:并发执行 {len(available_tasks)} 个任务..." f"🚀 [TaskStrategy] 引擎接管:并发执行 {len(available_tasks)} 个任务..."
) )
actions = [] actions = []
+2 -4
View File
@@ -6,8 +6,8 @@ from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.run.context import RunContext from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.tools.core.decorators import tool from zhenxun.services.ai.tools.core.decorators import tool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import ToolResult from zhenxun.services.ai.tools.models import EndRunResult, ToolResult
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_team as logger
from .models import TaskBoardState, TaskNodeStatus from .models import TaskBoardState, TaskNodeStatus
@@ -188,8 +188,6 @@ class TaskPlanningToolkit(BaseToolkit):
], ],
context: RunContext, context: RunContext,
) -> ToolResult: ) -> ToolResult:
from zhenxun.services.ai.tools.models import EndRunResult
board = self._get_board(context) board = self._get_board(context)
board.is_goal_complete = True board.is_goal_complete = True
board.final_summary = summary board.final_summary = summary
+39 -40
View File
@@ -1,27 +1,42 @@
import asyncio import asyncio
from collections.abc import Callable, Mapping, Sequence from collections.abc import Callable, Mapping, Sequence
import contextlib
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from typing_extensions import Self from typing_extensions import Self
from pydantic import BaseModel from pydantic import BaseModel
from zhenxun.services.ai.capabilities import AbstractCapability, DynamicCapability from zhenxun.services.ai.capabilities import (
AbstractCapability,
CapabilitySource,
DynamicCapability,
)
from zhenxun.services.ai.core.exceptions import ConcurrencyInterruptException from zhenxun.services.ai.core.exceptions import ConcurrencyInterruptException
from zhenxun.services.ai.core.messages import PromptInput from zhenxun.services.ai.core.messages import PromptInput
from zhenxun.services.ai.core.models import CancellationToken from zhenxun.services.ai.core.models import CancellationToken
from zhenxun.services.ai.core.stream_events import EventBus from zhenxun.services.ai.core.stream_events import EventBus
from zhenxun.services.ai.flow.agent.agent import CapabilitySource, ToolSource from zhenxun.services.ai.flow.agent.agent import ToolSource
from zhenxun.services.ai.flow.agent.models import Persona from zhenxun.services.ai.flow.agent.models import Persona
from zhenxun.services.ai.flow.base import BaseRunnable from zhenxun.services.ai.flow.base import BaseRunnable, ConcurrencyPolicy
from zhenxun.services.ai.flow.team.models import TeamRuntimeConfig, Transition from zhenxun.services.ai.flow.concurrency import apply_concurrency_policy
from zhenxun.services.ai.flow.team.router import BaseRouter from zhenxun.services.ai.run import (
from zhenxun.services.ai.flow.team.strategy import ( AgentRunResult,
AgentTask,
RunContext,
StreamedRunResult,
)
from zhenxun.services.ai.run.models import AgentRunError
from zhenxun.services.ai.tools.providers.skills.capabilities import SkillCapability
from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource
from zhenxun.services.ai.utils import ContextUtils
from zhenxun.utils.utils import infer_plugin_namespace
from .models import TeamRuntimeConfig, Transition
from .router import BaseRouter
from .strategy import (
BaseTeamStrategy, BaseTeamStrategy,
) )
from zhenxun.services.ai.run import AgentRunResult, AgentTask, RunContext
from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource
from zhenxun.utils.utils import infer_plugin_namespace
class Team(BaseRunnable[AgentRunResult[Any]]): class Team(BaseRunnable[AgentRunResult[Any]]):
@@ -115,6 +130,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: str | None = None, leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None, leader_tools: list[ToolSource] | None = None,
custom_prompt: str | None = None, custom_prompt: str | None = None,
max_handoffs: int = 3,
) -> Self: ) -> Self:
""" """
应用路由策略,基于挂载的 Router 进行最合适的专家动态分发。 应用路由策略,基于挂载的 Router 进行最合适的专家动态分发。
@@ -128,8 +144,9 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: 路由节点 (Leader) 使用的大模型名称,若为空则默认继承全局。 leader_model: 路由节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给路由节点 (Leader) 的专属工具列表。 leader_tools: 挂载给路由节点 (Leader) 的专属工具列表。
custom_prompt: 自定义系统提示词,用于覆盖默认的路由系统提示词。 custom_prompt: 自定义系统提示词,用于覆盖默认的路由系统提示词。
max_handoffs: 同一会话中允许连续移交的最大次数,防止无限踢皮球。
""" """
from zhenxun.services.ai.flow.team.strategy import RouteStrategy from .strategy import RouteStrategy
self.strategy = RouteStrategy( self.strategy = RouteStrategy(
state_flow=state_flow, state_flow=state_flow,
@@ -138,6 +155,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model=leader_model, leader_model=leader_model,
leader_tools=leader_tools, leader_tools=leader_tools,
custom_prompt=custom_prompt, custom_prompt=custom_prompt,
max_handoffs=max_handoffs,
) )
self.selector_func = selector_func self.selector_func = selector_func
return self return self
@@ -147,6 +165,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: str | None = None, leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None, leader_tools: list[ToolSource] | None = None,
custom_prompt: str | None = None, custom_prompt: str | None = None,
max_delegations: int = 3,
) -> Self: ) -> Self:
""" """
应用协作策略,Leader 自主规划并主动将子任务委派给 Sub-Agents,最后汇总结果。 应用协作策略,Leader 自主规划并主动将子任务委派给 Sub-Agents,最后汇总结果。
@@ -158,13 +177,15 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: 协调节点 (Leader) 使用的大模型名称,若为空则默认继承全局。 leader_model: 协调节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给协调节点 (Leader) 的专属附加工具列表。 leader_tools: 挂载给协调节点 (Leader) 的专属附加工具列表。
custom_prompt: 自定义系统提示词,用于覆盖默认的协调系统提示词。 custom_prompt: 自定义系统提示词,用于覆盖默认的协调系统提示词。
max_delegations: 允许向同一个专员连续委派失败的最大重试次数。
""" """
from zhenxun.services.ai.flow.team.strategy import CoordinateStrategy from .strategy import CoordinateStrategy
self.strategy = CoordinateStrategy( self.strategy = CoordinateStrategy(
leader_model=leader_model, leader_model=leader_model,
leader_tools=leader_tools, leader_tools=leader_tools,
custom_prompt=custom_prompt, custom_prompt=custom_prompt,
max_delegations=max_delegations,
) )
return self return self
@@ -184,7 +205,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_tools: 挂载给总结节点 (Leader) 的专属附加工具列表。 leader_tools: 挂载给总结节点 (Leader) 的专属附加工具列表。
custom_prompt: 自定义系统提示词,用于覆盖默认的广播总结系统提示词。 custom_prompt: 自定义系统提示词,用于覆盖默认的广播总结系统提示词。
""" """
from zhenxun.services.ai.flow.team.strategy import BroadcastStrategy from .strategy import BroadcastStrategy
self.strategy = BroadcastStrategy( self.strategy = BroadcastStrategy(
leader_model=leader_model, leader_model=leader_model,
@@ -198,8 +219,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: str | None = None, leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None, leader_tools: list[ToolSource] | None = None,
max_iterations: int = 15, max_iterations: int = 15,
blackboard_schema: type[BaseModel] | None = None, blackboard: type[BaseModel] | BaseModel | None = None,
initial_blackboard_state: BaseModel | None = None,
custom_prompt: str | None = None, custom_prompt: str | None = None,
) -> Self: ) -> Self:
""" """
@@ -213,18 +233,16 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: 规划节点 (Leader) 使用的大模型名称,若为空则默认继承全局。 leader_model: 规划节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给规划节点 (Leader) 的专属附加工具列表。 leader_tools: 挂载给规划节点 (Leader) 的专属附加工具列表。
max_iterations: 引擎驱动的状态机最大迭代/循环次数,防止死循环。 max_iterations: 引擎驱动的状态机最大迭代/循环次数,防止死循环。
blackboard_schema: 团队共享黑板的数据结构类型 (Pydantic Model 类)。 blackboard: (可选) 团队共享黑板。可传入 Schema 类型类,或直接传入带有初始数据的 Schema 实例对象。
initial_blackboard_state: 共享黑板的初始数据状态实例。
custom_prompt: 自定义系统提示词,用于覆盖默认的规划系统提示词。 custom_prompt: 自定义系统提示词,用于覆盖默认的规划系统提示词。
""" """ # noqa: E501
from zhenxun.services.ai.flow.team.strategy import TaskStrategy from .strategy import TaskStrategy
self.strategy = TaskStrategy( self.strategy = TaskStrategy(
leader_model=leader_model, leader_model=leader_model,
leader_tools=leader_tools, leader_tools=leader_tools,
max_iterations=max_iterations, max_iterations=max_iterations,
blackboard_schema=blackboard_schema, blackboard=blackboard,
initial_blackboard_state=initial_blackboard_state,
custom_prompt=custom_prompt, custom_prompt=custom_prompt,
) )
return self return self
@@ -260,10 +278,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
""" """
self._ensure_strategy() self._ensure_strategy()
if skills: if skills:
from zhenxun.services.ai.tools.providers.skills.capabilities import (
SkillCapability,
)
capabilities = list(capabilities) if capabilities else [] capabilities = list(capabilities) if capabilities else []
capabilities.append( capabilities.append(
SkillCapability(skills=skills, namespace=self.namespace) SkillCapability(skills=skills, namespace=self.namespace)
@@ -273,8 +287,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
prompt=prompt, context=context, capabilities=capabilities, **kwargs prompt=prompt, context=context, capabilities=capabilities, **kwargs
) )
import contextlib
@contextlib.asynccontextmanager @contextlib.asynccontextmanager
async def run_stream( async def run_stream(
self, self,
@@ -297,10 +309,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
context.capabilities.extend(self.capabilities) context.capabilities.extend(self.capabilities)
if skills: if skills:
from zhenxun.services.ai.tools.providers.skills.capabilities import (
SkillCapability,
)
capabilities = list(capabilities) if capabilities else [] capabilities = list(capabilities) if capabilities else []
capabilities.append( capabilities.append(
SkillCapability(skills=skills, namespace=self.namespace) SkillCapability(skills=skills, namespace=self.namespace)
@@ -313,8 +321,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
elif callable(cap): elif callable(cap):
context.capabilities.append(DynamicCapability(cap)) context.capabilities.append(DynamicCapability(cap))
from zhenxun.services.ai.flow.team.runner import TeamRunner from .runner import TeamRunner
from zhenxun.services.ai.run import StreamedRunResult
event_bus = EventBus() event_bus = EventBus()
context.run.event_bus = event_bus context.run.event_bus = event_bus
@@ -323,8 +330,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
policy = getattr(self.runtime_config, "concurrency_policy", None) policy = getattr(self.runtime_config, "concurrency_policy", None)
if policy is None: if policy is None:
from zhenxun.services.ai.flow.base import ConcurrencyPolicy
policy = ( policy = (
ConcurrencyPolicy.ALLOW ConcurrencyPolicy.ALLOW
if getattr(self.runtime_config, "stateless", True) if getattr(self.runtime_config, "stateless", True)
@@ -333,8 +338,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
intervention_policy = getattr(self.runtime_config, "intervention_policy", None) intervention_policy = getattr(self.runtime_config, "intervention_policy", None)
from zhenxun.services.ai.utils import ContextUtils
lock_id = ContextUtils.extract_concurrency_lock_id( lock_id = ContextUtils.extract_concurrency_lock_id(
context, context,
getattr(self.runtime_config, "concurrency_scope", None), getattr(self.runtime_config, "concurrency_scope", None),
@@ -342,8 +345,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
) )
async def _execution_task(): async def _execution_task():
from zhenxun.services.ai.flow.concurrency import apply_concurrency_policy
cancel_token = context.run.cancellation_token or CancellationToken() cancel_token = context.run.cancellation_token or CancellationToken()
context.run.cancellation_token = cancel_token context.run.cancellation_token = cancel_token
@@ -359,8 +360,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
async for event in runner.run_stream(prompt, context, **kwargs): async for event in runner.run_stream(prompt, context, **kwargs):
await event_bus.emit(event) await event_bus.emit(event)
except BaseException as e: except BaseException as e:
from zhenxun.services.ai.run.models import AgentRunError
if isinstance(e, asyncio.CancelledError): if isinstance(e, asyncio.CancelledError):
e = ConcurrencyInterruptException("团队执行已被新请求打断并接管") e = ConcurrencyInterruptException("团队执行已被新请求打断并接管")
await event_bus.emit(AgentRunError(error=e)) await event_bus.emit(AgentRunError(error=e))
+5 -4
View File
@@ -8,18 +8,19 @@ from zhenxun.services.ai.core.exceptions import (
ControlFlowExit, ControlFlowExit,
ToolFatalError, ToolFatalError,
) )
from zhenxun.services.ai.flow.workflow.policies import ( from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.utils.logger import log_flow as logger
from .policies import (
AbortPolicy, AbortPolicy,
BaseFailurePolicy, BaseFailurePolicy,
PolicyAction, PolicyAction,
) )
from zhenxun.services.ai.flow.workflow.types import ( from .types import (
StepInput, StepInput,
StepOutput, StepOutput,
StepType, StepType,
) )
from zhenxun.services.ai.run import RunContext
from zhenxun.services.log import logger
class BaseNode(ABC): class BaseNode(ABC):
+53 -12
View File
@@ -5,20 +5,16 @@ from typing import TYPE_CHECKING, Any
import uuid import uuid
from nonebot.params import Depends from nonebot.params import Depends
from pydantic import BaseModel
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.flow.workflow.nodes import NodeSource from .nodes import NodeSource
from zhenxun.services.ai.core.exceptions import ControlFlowExit, ToolRetryError from zhenxun.services.ai.core.exceptions import ControlFlowExit, ToolRetryError
from zhenxun.services.ai.core.messages import PromptInput, UsageInfo from zhenxun.services.ai.core.messages import PromptInput, UsageInfo
from zhenxun.services.ai.core.stream_events import EventBus from zhenxun.services.ai.core.stream_events import EventBus
from zhenxun.services.ai.flow.base import BaseRunnable, BaseRuntimeConfig from zhenxun.services.ai.flow.base import BaseRunnable, BaseRuntimeConfig
from zhenxun.services.ai.flow.workflow.nodes import Steps from zhenxun.services.ai.run.blackboard import BlackboardManager
from zhenxun.services.ai.flow.workflow.types import (
StepInput,
StepOutput,
WorkflowRunResult,
)
from zhenxun.services.ai.run.context import RunContext from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.run.models import ( from zhenxun.services.ai.run.models import (
AgentRunEnd, AgentRunEnd,
@@ -27,9 +23,16 @@ from zhenxun.services.ai.run.models import (
StreamedRunResult, StreamedRunResult,
) )
from zhenxun.services.ai.tools.core.tool import FunctionTool from zhenxun.services.ai.tools.core.tool import FunctionTool
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_flow as logger
from zhenxun.utils.message import MessageUtils from zhenxun.utils.message import MessageUtils
from .nodes import Steps
from .types import (
StepInput,
StepOutput,
WorkflowRunResult,
)
class Workflow(BaseRunnable[WorkflowRunResult]): class Workflow(BaseRunnable[WorkflowRunResult]):
""" """
@@ -37,7 +40,13 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
继承自 BaseRunnable,支持被作为节点嵌套在 Team 或 其他工作流中。 继承自 BaseRunnable,支持被作为节点嵌套在 Team 或 其他工作流中。
""" """
def __init__(self, name: str, steps: list["NodeSource"], description: str = ""): def __init__(
self,
name: str,
steps: list["NodeSource"],
description: str = "",
blackboard: type[BaseModel] | BaseModel | None = None,
):
""" """
静态图元工作流容器初始化。 静态图元工作流容器初始化。
@@ -45,7 +54,8 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
name: 工作流的名称标识。 name: 工作流的名称标识。
steps: 工作流的节点列表(按列表顺序构成串行或嵌套结构)。 steps: 工作流的节点列表(按列表顺序构成串行或嵌套结构)。
description: 工作流的说明描述,用于被 Agent 调用时理解其功能。 description: 工作流的说明描述,用于被 Agent 调用时理解其功能。
""" blackboard: (可选) 结构化黑板。可传入 Schema 类型类,或直接传入带有初始数据的 Schema 实例对象。
""" # noqa: E501
self.name = name self.name = name
self.description = description self.description = description
self.id = uuid.uuid4().hex self.id = uuid.uuid4().hex
@@ -54,6 +64,19 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
self.runtime_config = BaseRuntimeConfig(stateless=True) self.runtime_config = BaseRuntimeConfig(stateless=True)
self.persona = None self.persona = None
self.blackboard_schema = None
self.initial_blackboard_state = None
if blackboard is not None:
if isinstance(blackboard, type) and issubclass(blackboard, BaseModel):
self.blackboard_schema = blackboard
elif isinstance(blackboard, BaseModel):
self.blackboard_schema = type(blackboard)
self.initial_blackboard_state = blackboard
else:
raise ValueError(
"blackboard 参数必须是 Pydantic BaseModel 的子类(类型)或其实例"
)
def _build_result( def _build_result(
self, self,
initial_input: StepInput, initial_input: StepInput,
@@ -94,7 +117,12 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
return Depends(_dependency) return Depends(_dependency)
async def reply( async def reply(
self, prompt: PromptInput | None = None, reply_to: bool = False, **kwargs: Any self,
prompt: PromptInput | None = None,
reply_to: bool = False,
*,
context: RunContext | None = None,
**kwargs: Any,
) -> WorkflowRunResult: ) -> WorkflowRunResult:
""" """
工作流交互执行语法糖,隐式提取上下文并自动将最终流水线产出发送回复给用户。 工作流交互执行语法糖,隐式提取上下文并自动将最终流水线产出发送回复给用户。
@@ -102,12 +130,13 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
参数: 参数:
prompt: 传入工作流入口根节点的初始参数或指令。 prompt: 传入工作流入口根节点的初始参数或指令。
reply_to: 是否将结果作为回复消息发送 (at用户或引用原消息)。 reply_to: 是否将结果作为回复消息发送 (at用户或引用原消息)。
context: 显式传入的会话与运行上下文。
kwargs: 追加的工作流附带参数 (additional_data)。 kwargs: 追加的工作流附带参数 (additional_data)。
返回: 返回:
WorkflowRunResult: 包含执行状态、断点快照、各节点产出的全量工作流结果对象。 WorkflowRunResult: 包含执行状态、断点快照、各节点产出的全量工作流结果对象。
""" """
ctx = RunContext() ctx = context or RunContext()
bot = ctx.get_bot() bot = ctx.get_bot()
event = ctx.get_event() event = ctx.get_event()
@@ -152,6 +181,12 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
) )
safe_context = context or RunContext(session_id=session_id) safe_context = context or RunContext(session_id=session_id)
if self.blackboard_schema and not safe_context.session.blackboard:
safe_context.session.blackboard = BlackboardManager(
schema=self.blackboard_schema,
initial_state=self.initial_blackboard_state,
)
logger.debug(f"🏭 **工作流 [{self.name}] 启动**") logger.debug(f"🏭 **工作流 [{self.name}] 启动**")
initial_input = StepInput(input=prompt) initial_input = StepInput(input=prompt)
@@ -214,6 +249,12 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
) )
safe_context = context or RunContext(session_id=session_id) safe_context = context or RunContext(session_id=session_id)
if self.blackboard_schema and not safe_context.session.blackboard:
safe_context.session.blackboard = BlackboardManager(
schema=self.blackboard_schema,
initial_state=self.initial_blackboard_state,
)
logger.debug(f"🏭 **工作流 [{self.name}] 启动**") logger.debug(f"🏭 **工作流 [{self.name}] 启动**")
initial_input = StepInput(input=prompt) initial_input = StepInput(input=prompt)
+8 -7
View File
@@ -5,17 +5,18 @@ from typing import Any, cast
from zhenxun.services.ai.core.messages import PromptInput from zhenxun.services.ai.core.messages import PromptInput
from zhenxun.services.ai.flow.base import BaseRunnable from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.flow.workflow.base import BaseNode from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.flow.workflow.policies import BaseFailurePolicy from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.ai.flow.workflow.types import ( from zhenxun.services.ai.run.models import AgentRunEnd
from zhenxun.services.ai.utils.logger import log_flow as logger
from .base import BaseNode
from .policies import BaseFailurePolicy
from .types import (
StepInput, StepInput,
StepOutput, StepOutput,
StepType, StepType,
) )
from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.ai.run.models import AgentRunEnd
from zhenxun.services.log import logger
NodeSource = BaseNode | BaseRunnable | Callable NodeSource = BaseNode | BaseRunnable | Callable
"""工作流节点来源,可以是图元、可执行引擎或原生函数""" """工作流节点来源,可以是图元、可执行引擎或原生函数"""
@@ -4,9 +4,10 @@ from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from zhenxun.services.ai.flow.workflow.types import StepInput
from zhenxun.services.ai.llm.api import generate_structured from zhenxun.services.ai.llm.api import generate_structured
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_flow as logger
from .types import StepInput
class PolicyAction(str, Enum): class PolicyAction(str, Enum):
+2 -1
View File
@@ -13,7 +13,7 @@ from .glm import GLMAdapter
from .jina import JinaAdapter from .jina import JinaAdapter
from .mimo import MiMoAdapter from .mimo import MiMoAdapter
from .minimax import MiniMaxAdapter from .minimax import MiniMaxAdapter
from .openai import OpenAIAdapter, OpenAICompatAdapter from .openai import OpenAIAdapter, OpenAICompatAdapter, OpenAIResponsesAdapter
from .openrouter import OpenRouterAdapter from .openrouter import OpenRouterAdapter
LLMAdapterFactory.initialize() LLMAdapterFactory.initialize()
@@ -30,6 +30,7 @@ __all__ = [
"MiniMaxAdapter", "MiniMaxAdapter",
"OpenAIAdapter", "OpenAIAdapter",
"OpenAICompatAdapter", "OpenAICompatAdapter",
"OpenAIResponsesAdapter",
"OpenRouterAdapter", "OpenRouterAdapter",
"RequestData", "RequestData",
"ResponseData", "ResponseData",
+3 -4
View File
@@ -47,11 +47,11 @@ from zhenxun.services.ai.core.messages import (
ThoughtPart, ThoughtPart,
) )
from zhenxun.services.ai.core.models import ModelIdentity from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.log_sanitizer import sanitize_for_logging from zhenxun.utils.log_sanitizer import sanitize_for_logging
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.llm.adapters.handlers.base import ( from .handlers.base import (
BaseAudioHandler, BaseAudioHandler,
BaseEmbeddingHandler, BaseEmbeddingHandler,
BaseImageHandler, BaseImageHandler,
@@ -496,8 +496,7 @@ class BaseAdapter(ABC):
async def parse_speech_response( async def parse_speech_response(
self, identity: ModelIdentity, raw_response: httpx.Response self, identity: ModelIdentity, raw_response: httpx.Response
) -> AudioResponse: ) -> AudioResponse:
"""解析语音响应并委派给 `audio_handler`。 """解析语音响应并委派给 `audio_handler`"""
注意传入的是 httpx.Response 的 raw 对象"""
if self.audio_handler: if self.audio_handler:
return await self.audio_handler.parse_speech_response( return await self.audio_handler.parse_speech_response(
adapter=self, identity=identity, raw_response=raw_response adapter=self, identity=identity, raw_response=raw_response
+3 -17
View File
@@ -6,12 +6,13 @@ from zhenxun.services.ai.core.models import (
ModelIdentity, ModelIdentity,
) )
from zhenxun.services.ai.core.options import GenerationConfig from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .handlers.openai_handlers import (
OpenAIConfigMapper, OpenAIConfigMapper,
OpenAITextHandler, OpenAITextHandler,
OpenAIToolSerializer, OpenAIToolSerializer,
) )
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter from .openai import OpenAICompatAdapter
class DeepSeekToolSerializer(OpenAIToolSerializer): class DeepSeekToolSerializer(OpenAIToolSerializer):
@@ -84,21 +85,6 @@ class DeepSeekConfigMapper(OpenAIConfigMapper):
if isinstance(rf, dict) and rf.get("type") == "json_schema": if isinstance(rf, dict) and rf.get("type") == "json_schema":
params["response_format"] = {"type": "json_object"} params["response_format"] = {"type": "json_object"}
if config.common.reasoning_effort:
effort = str(config.common.reasoning_effort).lower()
if effort == "none":
params["thinking"] = {"type": "disabled"}
else:
params["thinking"] = {"type": "enabled"}
elif (
hasattr(config, "deepseek_options")
and config.deepseek_options.thinking is not None
):
if config.deepseek_options.thinking is True:
params["thinking"] = {"type": "enabled"}
elif config.deepseek_options.thinking is False:
params["thinking"] = {"type": "disabled"}
return params return params
+2 -1
View File
@@ -34,10 +34,11 @@ class LLMAdapterFactory:
from .jina import JinaAdapter from .jina import JinaAdapter
from .mimo import MiMoAdapter from .mimo import MiMoAdapter
from .minimax import MiniMaxAdapter from .minimax import MiniMaxAdapter
from .openai import OpenAIAdapter from .openai import OpenAIAdapter, OpenAIResponsesAdapter
from .openrouter import OpenRouterAdapter from .openrouter import OpenRouterAdapter
cls.register_adapter(OpenAIAdapter()) cls.register_adapter(OpenAIAdapter())
cls.register_adapter(OpenAIResponsesAdapter())
cls.register_adapter(OpenRouterAdapter()) cls.register_adapter(OpenRouterAdapter())
cls.register_adapter(DeepSeekAdapter()) cls.register_adapter(DeepSeekAdapter())
cls.register_adapter(JinaAdapter()) cls.register_adapter(JinaAdapter())
+4 -3
View File
@@ -1,13 +1,14 @@
from zhenxun.services.ai.core.messages import RerankRequest from zhenxun.services.ai.core.messages import RerankRequest
from zhenxun.services.ai.core.models import ModelIdentity from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import ( from .base import BaseAdapter, RequestData
from .handlers.openai_handlers import (
OpenAIConfigMapper, OpenAIConfigMapper,
OpenAIEmbeddingHandler, OpenAIEmbeddingHandler,
OpenAIRerankHandler, OpenAIRerankHandler,
OpenAITextHandler, OpenAITextHandler,
) )
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter from .openai import OpenAICompatAdapter
class GLMRerankHandler(OpenAIRerankHandler): class GLMRerankHandler(OpenAIRerankHandler):
@@ -56,7 +56,9 @@ from zhenxun.services.ai.llm.adapters.base import (
ResponseData, ResponseData,
process_image_data, process_image_data,
) )
from zhenxun.services.ai.llm.adapters.handlers.base import ( from zhenxun.services.ai.utils.logger import log_llm as logger
from .base import (
BaseAudioHandler, BaseAudioHandler,
BaseEmbeddingHandler, BaseEmbeddingHandler,
BaseImageHandler, BaseImageHandler,
@@ -66,7 +68,6 @@ from zhenxun.services.ai.llm.adapters.handlers.base import (
ResponseParser, ResponseParser,
ToolSerializer, ToolSerializer,
) )
from zhenxun.services.log import logger
class GeminiConfigMapper(ConfigMapper): class GeminiConfigMapper(ConfigMapper):
@@ -121,22 +122,19 @@ class GeminiConfigMapper(ConfigMapper):
fc_config["allowedFunctionNames"] = user_funcs fc_config["allowedFunctionNames"] = user_funcs
params["toolConfig"] = {"functionCallingConfig": fc_config} params["toolConfig"] = {"functionCallingConfig": fc_config}
has_effort = bool( if config.common.reasoning_effort and capabilities:
config.common.reasoning_effort
and str(config.common.reasoning_effort).lower() != "none"
)
if (
has_effort
and capabilities
and capabilities.reasoning_mode == ReasoningMode.LEVEL
):
thinking_config = params.setdefault("thinkingConfig", {})
effort = str(config.common.reasoning_effort).lower() effort = str(config.common.reasoning_effort).lower()
if capabilities.reasoning_effort_map: if capabilities.reasoning_effort_map:
effort = capabilities.reasoning_effort_map.get(effort, effort) effort = capabilities.reasoning_effort_map.get(effort, effort)
if capabilities.reasoning_mode == ReasoningMode.LEVEL and effort != "none":
thinking_config = params.setdefault("thinkingConfig", {})
thinking_config["thinkingLevel"] = effort thinking_config["thinkingLevel"] = effort
elif (
capabilities.reasoning_mode == ReasoningMode.BUDGET and effort == "none"
):
thinking_config = params.setdefault("thinkingConfig", {})
thinking_config["thinkingBudget"] = 0
if config.gemini_options.include_thoughts is not None: if config.gemini_options.include_thoughts is not None:
thinking_config = params.setdefault("thinkingConfig", {}) thinking_config = params.setdefault("thinkingConfig", {})
@@ -829,10 +827,6 @@ class GeminiEmbeddingHandler(BaseEmbeddingHandler):
url = f"{base_url}/v1beta/{api_model_name}:batchEmbedContents" url = f"{base_url}/v1beta/{api_model_name}:batchEmbedContents"
headers = adapter.get_base_headers(api_key) headers = adapter.get_base_headers(api_key)
from zhenxun.services.ai.llm.adapters.handlers.gemini_handlers import (
GeminiMessageConverter,
)
converter = GeminiMessageConverter() converter = GeminiMessageConverter()
requests_payload = [] requests_payload = []
@@ -16,13 +16,13 @@ from zhenxun.services.ai.core.messages import (
) )
from zhenxun.services.ai.core.models import ( from zhenxun.services.ai.core.models import (
ModelCapabilities, ModelCapabilities,
ModelDetail,
ModelIdentity, ModelIdentity,
) )
from zhenxun.services.ai.core.options import GenerationConfig, TTSConfig from zhenxun.services.ai.core.options import TTSConfig
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.base import BaseAudioHandler
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import ( from .base import BaseAudioHandler
from .openai_handlers import (
OpenAIConfigMapper, OpenAIConfigMapper,
OpenAIMessageConverter, OpenAIMessageConverter,
OpenAITextHandler, OpenAITextHandler,
@@ -53,35 +53,6 @@ class MiMoToolSerializer(OpenAIToolSerializer):
return res return res
class MiMoConfigMapper(OpenAIConfigMapper):
"""MiMo 配置映射器,处理深度思考参数差异"""
def map_config(
self,
config: GenerationConfig,
model_detail: ModelDetail | None = None,
capabilities: ModelCapabilities | None = None,
) -> dict[str, Any]:
params = super().map_config(config, model_detail, capabilities)
if config.common.reasoning_effort:
effort = str(config.common.reasoning_effort).lower()
if effort == "none":
params["thinking"] = {"type": "disabled"}
else:
params["thinking"] = {"type": "enabled"}
elif (
hasattr(config, "deepseek_options")
and config.deepseek_options.thinking is not None
):
if config.deepseek_options.thinking is True:
params["thinking"] = {"type": "enabled"}
elif config.deepseek_options.thinking is False:
params["thinking"] = {"type": "disabled"}
return params
class MiMoMessageConverter(OpenAIMessageConverter): class MiMoMessageConverter(OpenAIMessageConverter):
"""MiMo 消息转换器,拦截处理特有的音视频多模态结构""" """MiMo 消息转换器,拦截处理特有的音视频多模态结构"""
@@ -141,7 +112,7 @@ class MiMoTextHandler(OpenAITextHandler):
super().__init__(api_type=api_type) super().__init__(api_type=api_type)
self.converter = MiMoMessageConverter(api_type=api_type) self.converter = MiMoMessageConverter(api_type=api_type)
self.serializer = MiMoToolSerializer(api_type=api_type) self.serializer = MiMoToolSerializer(api_type=api_type)
self.mapper = MiMoConfigMapper(api_type=api_type) self.mapper = OpenAIConfigMapper(api_type=api_type)
class MiMoAudioHandler(BaseAudioHandler): class MiMoAudioHandler(BaseAudioHandler):
@@ -53,7 +53,9 @@ from zhenxun.services.ai.llm.adapters.base import (
ResponseData, ResponseData,
process_image_data, process_image_data,
) )
from zhenxun.services.ai.llm.adapters.handlers.base import ( from zhenxun.services.ai.utils.logger import log_llm as logger
from .base import (
BaseAudioHandler, BaseAudioHandler,
BaseEmbeddingHandler, BaseEmbeddingHandler,
BaseImageHandler, BaseImageHandler,
@@ -64,7 +66,6 @@ from zhenxun.services.ai.llm.adapters.handlers.base import (
ResponseParser, ResponseParser,
ToolSerializer, ToolSerializer,
) )
from zhenxun.services.log import logger
class OpenAIConfigMapper(ConfigMapper): class OpenAIConfigMapper(ConfigMapper):
@@ -114,7 +115,13 @@ class OpenAIConfigMapper(ConfigMapper):
if capabilities and capabilities.reasoning_effort_map: if capabilities and capabilities.reasoning_effort_map:
effort = capabilities.reasoning_effort_map.get(effort, effort) effort = capabilities.reasoning_effort_map.get(effort, effort)
if effort != "none": if (
effort == "none"
and capabilities
and capabilities.supports_thinking_toggle
):
params["thinking"] = {"type": "disabled"}
else:
params["reasoning_effort"] = effort params["reasoning_effort"] = effort
if isinstance(config.output.response_format, dict): if isinstance(config.output.response_format, dict):
@@ -1165,45 +1172,3 @@ class OpenAIAudioHandler(BaseAudioHandler):
usage=UsageInfo(), usage=UsageInfo(),
model_name=identity.model_name, model_name=identity.model_name,
) )
class CompositeOpenAITextHandler(BaseTextHandler):
"""
OpenAI 复合文本对话处理器 (Composite Pattern)。
内部包装标准协议与 responses 协议 Handler,根据模型配置在请求时动态路由
"""
def __init__(self, api_type: str = "openai"):
self.api_type = api_type
self._standard_handler = OpenAITextHandler(api_type=api_type)
self._responses_handler = OpenAIResponsesTextHandler(
api_type="openai_responses"
)
def _get_active_handler(self, identity: ModelIdentity) -> BaseTextHandler:
current_api_type = identity.api_type
if current_api_type == "openai_responses":
return self._responses_handler
return self._standard_handler
async def prepare_text_request(
self,
adapter: BaseAdapter,
identity: ModelIdentity,
api_key: str,
request: ChatRequest,
) -> RequestData:
handler = self._get_active_handler(identity)
return await handler.prepare_text_request(adapter, identity, api_key, request)
def parse_text_response(
self,
adapter: BaseAdapter,
identity: ModelIdentity,
response_json: dict[str, Any],
is_advanced: bool = False,
) -> ResponseData:
handler = self._get_active_handler(identity)
return handler.parse_text_response(
adapter, identity, response_json, is_advanced
)
+5 -4
View File
@@ -1,12 +1,14 @@
from zhenxun.services.ai.core.messages import EmbeddingRequest from zhenxun.services.ai.core.messages import EmbeddingRequest
from zhenxun.services.ai.core.models import ModelIdentity from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.core.options import LLMEmbeddingConfig from zhenxun.services.ai.core.options import LLMEmbeddingConfig
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .base import BaseAdapter, RequestData
from .handlers.openai_handlers import (
OpenAIEmbeddingHandler, OpenAIEmbeddingHandler,
OpenAIRerankHandler, OpenAIRerankHandler,
) )
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter from .openai import OpenAICompatAdapter
class JinaEmbeddingHandler(OpenAIEmbeddingHandler): class JinaEmbeddingHandler(OpenAIEmbeddingHandler):
@@ -36,7 +38,6 @@ class JinaEmbeddingHandler(OpenAIEmbeddingHandler):
TextPart, TextPart,
VideoPart, VideoPart,
) )
from zhenxun.services.log import logger
for payload in batch.payloads: for payload in batch.payloads:
jina_content = [] jina_content = []
+4 -3
View File
@@ -1,12 +1,13 @@
from zhenxun.services.ai.core.models import ModelIdentity from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.llm.adapters.handlers.mimo_handlers import (
from .handlers.mimo_handlers import (
MiMoAudioHandler, MiMoAudioHandler,
MiMoTextHandler, MiMoTextHandler,
) )
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import ( from .handlers.openai_handlers import (
OpenAIImageHandler, OpenAIImageHandler,
) )
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter from .openai import OpenAICompatAdapter
class MiMoAdapter(OpenAICompatAdapter): class MiMoAdapter(OpenAICompatAdapter):
+2 -2
View File
@@ -19,9 +19,9 @@ from zhenxun.services.ai.core.models import (
ModelIdentity, ModelIdentity,
) )
from zhenxun.services.ai.core.options import GenerationConfig, TTSConfig from zhenxun.services.ai.core.options import GenerationConfig, TTSConfig
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.base import BaseAudioHandler
from .base import BaseAdapter, RequestData
from .handlers.base import BaseAudioHandler
from .handlers.openai_handlers import ( from .handlers.openai_handlers import (
OpenAIConfigMapper, OpenAIConfigMapper,
OpenAIMessageConverter, OpenAIMessageConverter,
+29 -14
View File
@@ -15,11 +15,12 @@ from .base import (
RequestData, RequestData,
) )
from .handlers.openai_handlers import ( from .handlers.openai_handlers import (
CompositeOpenAITextHandler,
OpenAIAudioHandler, OpenAIAudioHandler,
OpenAIEmbeddingHandler, OpenAIEmbeddingHandler,
OpenAIImageHandler, OpenAIImageHandler,
OpenAIRerankHandler, OpenAIRerankHandler,
OpenAIResponsesTextHandler,
OpenAITextHandler,
) )
@@ -83,12 +84,12 @@ class OpenAICompatAdapter(BaseAdapter):
class OpenAIAdapter(OpenAICompatAdapter): class OpenAIAdapter(OpenAICompatAdapter):
"""OpenAI 系列适配器,统一装配文本/图像/嵌入/重排处理链。""" """标准 OpenAI 系列适配器,统一装配文本/图像/嵌入/重排处理链。"""
def __init__(self): def __init__(self):
"""初始化并挂载复合文本处理器与通用多模态处理器。""" """初始化并挂载标准文本处理器与通用多模态处理器。"""
super().__init__() super().__init__()
self.text_handler = CompositeOpenAITextHandler(api_type=self.api_type) self.text_handler = OpenAITextHandler(api_type=self.api_type)
self.image_handler = OpenAIImageHandler() self.image_handler = OpenAIImageHandler()
self.embedding_handler = OpenAIEmbeddingHandler() self.embedding_handler = OpenAIEmbeddingHandler()
self.rerank_handler = OpenAIRerankHandler() self.rerank_handler = OpenAIRerankHandler()
@@ -102,21 +103,35 @@ class OpenAIAdapter(OpenAICompatAdapter):
@property @property
def supported_api_types(self) -> list[str]: def supported_api_types(self) -> list[str]:
"""支持的 API 类型及别名。""" """支持的 API 类型及别名。"""
return [ return ["openai"]
"openai",
"openai_responses",
]
def get_chat_endpoint(self, identity: ModelIdentity) -> str: def get_chat_endpoint(self, identity: ModelIdentity) -> str:
"""返回聊天完成端点""" """返回聊天完成端点"""
current_api_type = identity.api_type
if current_api_type == "openai_responses":
return "/v1/responses"
if current_api_type == "doubao":
return "/api/v3/chat/completions"
return "/v1/chat/completions" return "/v1/chat/completions"
def get_embedding_endpoint(self, identity: ModelIdentity) -> str: def get_embedding_endpoint(self, identity: ModelIdentity) -> str:
"""返回嵌入端点。""" """返回嵌入端点。"""
return "/v1/embeddings" return "/v1/embeddings"
class OpenAIResponsesAdapter(OpenAICompatAdapter):
"""OpenAI v1/responses 协议专用适配器"""
def __init__(self):
super().__init__()
self.text_handler = OpenAIResponsesTextHandler(api_type=self.api_type)
self.image_handler = OpenAIImageHandler()
self.embedding_handler = OpenAIEmbeddingHandler()
self.rerank_handler = OpenAIRerankHandler()
self.audio_handler = OpenAIAudioHandler()
@property
def api_type(self) -> str:
return "openai_responses"
@property
def supported_api_types(self) -> list[str]:
return ["openai_responses"]
def get_chat_endpoint(self, identity: ModelIdentity) -> str:
return "/v1/responses"
@@ -10,18 +10,19 @@ from zhenxun.services.ai.core.messages import (
ThoughtPart, ThoughtPart,
) )
from zhenxun.services.ai.core.models import ModelIdentity from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.llm.adapters.base import (
from .base import (
BaseAdapter, BaseAdapter,
RequestData, RequestData,
ResponseData, ResponseData,
process_image_data, process_image_data,
) )
from zhenxun.services.ai.llm.adapters.handlers.base import BaseImageHandler from .handlers.base import BaseImageHandler
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import ( from .handlers.openai_handlers import (
CompositeOpenAITextHandler,
OpenAIMessageConverter, OpenAIMessageConverter,
OpenAITextHandler,
) )
from zhenxun.services.ai.llm.adapters.openai import OpenAIAdapter from .openai import OpenAIAdapter
class OpenRouterMessageConverter(OpenAIMessageConverter): class OpenRouterMessageConverter(OpenAIMessageConverter):
@@ -60,12 +61,12 @@ class OpenRouterMessageConverter(OpenAIMessageConverter):
return openai_messages return openai_messages
class OpenRouterTextHandler(CompositeOpenAITextHandler): class OpenRouterTextHandler(OpenAITextHandler):
"""OpenRouter 专有文本处理器,挂载专有 Converter""" """OpenRouter 专有文本处理器,挂载专有 Converter"""
def __init__(self, api_type: str = "openrouter"): def __init__(self, api_type: str = "openrouter"):
super().__init__(api_type=api_type) super().__init__(api_type=api_type)
self._standard_handler.converter = OpenRouterMessageConverter(api_type=api_type) self.converter = OpenRouterMessageConverter(api_type=api_type)
class OpenRouterImageHandler(BaseImageHandler): class OpenRouterImageHandler(BaseImageHandler):
+2 -2
View File
@@ -35,10 +35,10 @@ from zhenxun.services.ai.core.options import (
TTSConfig, TTSConfig,
) )
from zhenxun.services.ai.guardrails import GuardrailSource from zhenxun.services.ai.guardrails import GuardrailSource
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.services.log import logger
from .builder import IntentBuilder from .builder import IntentBuilder
from .engine.router import LLMOrchestrator
T = TypeVar("T", bound=BaseModel) T = TypeVar("T", bound=BaseModel)
+1 -20
View File
@@ -10,7 +10,7 @@ from zhenxun.services.ai.core.options import (
GenerationConfig, GenerationConfig,
ResponseFormat, ResponseFormat,
) )
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.pydantic_compat import model_json_schema, model_validate from zhenxun.utils.pydantic_compat import model_json_schema, model_validate
@@ -43,18 +43,6 @@ class OpenAIIntentNamespace:
return self._builder return self._builder
class DeepSeekIntentNamespace:
"""DeepSeek 专属高级参数构建域"""
def __init__(self, builder: "IntentBuilder"):
self._builder = builder
def disable_thinking(self) -> "IntentBuilder":
"""显式关闭 DeepSeek 的思维链"""
self._builder._config.deepseek_options.thinking = False
return self._builder
class IntentBuilder: class IntentBuilder:
""" """
基于能力意图声明的构建器 (Intent-Driven Builder)。 基于能力意图声明的构建器 (Intent-Driven Builder)。
@@ -72,10 +60,6 @@ class IntentBuilder:
def openai(self) -> OpenAIIntentNamespace: def openai(self) -> OpenAIIntentNamespace:
return OpenAIIntentNamespace(self) return OpenAIIntentNamespace(self)
@property
def deepseek(self) -> DeepSeekIntentNamespace:
return DeepSeekIntentNamespace(self)
def with_reasoning(self, level: str | None = None) -> Self: def with_reasoning(self, level: str | None = None) -> Self:
""" """
跨厂商统一的思考/推理等级意图声明。 跨厂商统一的思考/推理等级意图声明。
@@ -85,9 +69,6 @@ class IntentBuilder:
self._config.common.reasoning_effort = level self._config.common.reasoning_effort = level
if level.lower() != "none": if level.lower() != "none":
self._config.gemini_options.include_thoughts = True self._config.gemini_options.include_thoughts = True
self._config.deepseek_options.thinking = True
else:
self._config.deepseek_options.thinking = False
return self return self
def with_local_cache(self, ttl: int = 3600) -> Self: def with_local_cache(self, ttl: int = 3600) -> Self:
@@ -51,7 +51,7 @@ from zhenxun.services.ai.llm.system.network import (
HealthManager, HealthManager,
LLMHttpClient, LLMHttpClient,
) )
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.log_sanitizer import sanitize_for_logging from zhenxun.utils.log_sanitizer import sanitize_for_logging
from zhenxun.utils.pydantic_compat import ( from zhenxun.utils.pydantic_compat import (
@@ -136,7 +136,7 @@ class LLMCacheMiddleware:
if cached_data is not None: if cached_data is not None:
logger.debug( logger.debug(
f"⚡ [LLMCache] 命中本地极速缓存 - " f"命中本地极速缓存 - "
f"model: {self.model_name}, type: {type(context.request).__name__}" f"model: {self.model_name}, type: {type(context.request).__name__}"
) )
+7 -10
View File
@@ -16,7 +16,7 @@ from zhenxun.services.ai.llm.manager import (
) )
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities
from zhenxun.services.ai.llm.system.network import health_manager from zhenxun.services.ai.llm.system.network import health_manager
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_llm as logger
class BaseModelRouter(ABC): class BaseModelRouter(ABC):
@@ -57,7 +57,7 @@ class FallbackRouter(BaseModelRouter):
m_name = model_names[idx] m_name = model_names[idx]
if not health_manager.is_route_healthy(m_name, strict_mode=is_routed_call): if not health_manager.is_route_healthy(m_name, strict_mode=is_routed_call):
logger.debug(f"👉 [Orchestrator] 节点 '{m_name}' 熔断中,已跳过") logger.debug(f"👉 节点 '{m_name}' 熔断中,已跳过")
errors.append(f"{m_name}(熔断中)") errors.append(f"{m_name}(熔断中)")
continue continue
@@ -70,7 +70,7 @@ class FallbackRouter(BaseModelRouter):
try: try:
if len(model_names) > 1: if len(model_names) > 1:
if idx != start_idx: if idx != start_idx:
logger.debug(f"🔄 [Orchestrator] 切换至备用节点: '{m_name}'...") logger.debug(f"🔄 切换至备用节点: '{m_name}'...")
async with await get_model_instance( async with await get_model_instance(
m_name, override_config, task=task m_name, override_config, task=task
@@ -83,26 +83,23 @@ class FallbackRouter(BaseModelRouter):
except LLMException as e: except LLMException as e:
if not e.should_failover: if not e.should_failover:
logger.warning( logger.warning(
f"🚫 [Orchestrator] 节点 '{m_name}' " f"🚫 节点 '{m_name}' "
f"返回不可恢复错误 ({e.__class__.__name__}),停止故障转移。" f"返回不可恢复错误 ({e.__class__.__name__}),停止故障转移。"
) )
raise e raise e
logger.warning( logger.warning(
f"⚠️ [Orchestrator] 节点 '{m_name}' " f"⚠️ 节点 '{m_name}' "
f"错误 ({e.__class__.__name__}),触发故障转移..." f"错误 ({e.__class__.__name__}),触发故障转移..."
) )
errors.append(f"{m_name}({e.__class__.__name__})") errors.append(f"{m_name}({e.__class__.__name__})")
except Exception as e: except Exception as e:
logger.warning( logger.warning(f"⚠️ 节点 '{m_name}' 发生未知异常,触发故障转移: {e}")
f"⚠️ [Orchestrator] 节点 '{m_name}' 发生未知异常,触发故障转移: {e}"
)
errors.append(f"{m_name}(Error)") errors.append(f"{m_name}(Error)")
if all_nodes_bypassed and len(model_names) > 1: if all_nodes_bypassed and len(model_names) > 1:
fallback_model = health_manager.get_best_fallback_route(model_names) fallback_model = health_manager.get_best_fallback_route(model_names)
logger.warning( logger.warning(
f"⚠️ [Orchestrator] 路由组所有节点均已宕机!" f"⚠️ 路由组所有节点均已宕机!" f"强制放行 '{fallback_model}' 探活..."
f"强制放行 '{fallback_model}' 探活..."
) )
try: try:
async with await get_model_instance( async with await get_model_instance(
+15 -19
View File
@@ -43,9 +43,22 @@ from zhenxun.services.ai.core.protocols.llm import (
SupportsTextEmbedding, SupportsTextEmbedding,
) )
from zhenxun.services.ai.core.protocols.middleware import LLMMiddleware from zhenxun.services.ai.core.protocols.middleware import LLMMiddleware
from zhenxun.services.ai.llm.adapters.factory import get_adapter_for_api_type
from zhenxun.services.ai.llm.system.models import RetryConfig from zhenxun.services.ai.llm.system.models import RetryConfig
from zhenxun.services.ai.llm.system.network import HealthManager, LLMHttpClient from zhenxun.services.ai.llm.system.network import HealthManager, LLMHttpClient
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_llm as logger
from .middlewares import (
ConfigMergeMiddleware,
FailoverAndRetryMiddleware,
HttpExecutionMiddleware,
LLMCacheMiddleware,
LoggingMiddleware,
MiddlewarePipeline,
ModalityFilterMiddleware,
OutputValidationMiddleware,
ResponseRescueMiddleware,
)
T = TypeVar("T", bound=BaseModel) T = TypeVar("T", bound=BaseModel)
@@ -86,7 +99,7 @@ class LLMModel(
) )
self.model_name = model_detail.model_name self.model_name = model_detail.model_name
self.temperature = model_detail.temperature self.temperature = model_detail.temperature
self.generation_max_tokens = model_detail.generation_max_tokens self.max_output_tokens = model_detail.max_output_tokens
self._is_closed = False self._is_closed = False
self._ref_count = 0 self._ref_count = 0
@@ -100,8 +113,6 @@ class LLMModel(
generation_config=self._generation_config, generation_config=self._generation_config,
) )
from zhenxun.services.ai.llm.engine.middlewares import MiddlewarePipeline
self.pipeline = MiddlewarePipeline() self.pipeline = MiddlewarePipeline()
self._setup_default_pipeline() self._setup_default_pipeline()
@@ -110,17 +121,6 @@ class LLMModel(
self.pipeline.add_middleware(middleware) self.pipeline.add_middleware(middleware)
def _setup_default_pipeline(self) -> None: def _setup_default_pipeline(self) -> None:
from zhenxun.services.ai.llm.adapters.factory import get_adapter_for_api_type
from zhenxun.services.ai.llm.engine.middlewares import (
ConfigMergeMiddleware,
FailoverAndRetryMiddleware,
LLMCacheMiddleware,
LoggingMiddleware,
ModalityFilterMiddleware,
OutputValidationMiddleware,
ResponseRescueMiddleware,
)
client_settings = get_llm_config().client_settings client_settings = get_llm_config().client_settings
retry_config = RetryConfig( retry_config = RetryConfig(
max_retries=client_settings.max_retries, max_retries=client_settings.max_retries,
@@ -216,10 +216,6 @@ class LLMModel(
request=request, request=request,
cancellation_token=cancellation_token, cancellation_token=cancellation_token,
) )
from zhenxun.services.ai.llm.adapters.factory import get_adapter_for_api_type
from zhenxun.services.ai.llm.engine.middlewares import HttpExecutionMiddleware
adapter = get_adapter_for_api_type(self.api_type) adapter = get_adapter_for_api_type(self.api_type)
execution_middleware = HttpExecutionMiddleware( execution_middleware = HttpExecutionMiddleware(
http_client=self.http_client, http_client=self.http_client,
+8 -6
View File
@@ -13,12 +13,13 @@ from zhenxun.services.ai.config import (
from zhenxun.services.ai.core.exceptions import ConfigurationException from zhenxun.services.ai.core.exceptions import ConfigurationException
from zhenxun.services.ai.core.models import ModelDetail from zhenxun.services.ai.core.models import ModelDetail
from zhenxun.services.ai.core.options import GenerationConfig from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.services.ai.llm.system.network import health_manager
from zhenxun.services.log import logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.pydantic_compat import model_dump from zhenxun.utils.pydantic_compat import model_dump
from .system.capabilities import get_model_capabilities
from .system.network import health_manager
_RESOLVED_GROUP_CACHE: dict[str, list[str]] = {} _RESOLVED_GROUP_CACHE: dict[str, list[str]] = {}
"""路由组解析缓存,避免每次调用重复打印剔除警告并提升性能""" """路由组解析缓存,避免每次调用重复打印剔除警告并提升性能"""
@@ -276,7 +277,7 @@ async def get_model_instance(
provider_config_found, model_detail_found = config_tuple_found provider_config_found, model_detail_found = config_tuple_found
from zhenxun.services.ai.llm.system.cache import get_or_create_model from .system.cache import get_or_create_model
return await get_or_create_model( return await get_or_create_model(
provider_config_found, model_detail_found, override_config provider_config_found, model_detail_found, override_config
@@ -287,7 +288,7 @@ def clear_all_cache() -> None:
""" """
清空模型实例缓存与路由组解析缓存。 清空模型实例缓存与路由组解析缓存。
""" """
from zhenxun.services.ai.llm.system.cache import clear_model_cache from .system.cache import clear_model_cache
clear_model_cache() clear_model_cache()
clear_resolved_group_cache() clear_resolved_group_cache()
@@ -300,9 +301,10 @@ async def _init_llm_config_on_startup():
logger.info("正在初始化 LLM 配置并加载遥测状态...") logger.info("正在初始化 LLM 配置并加载遥测状态...")
try: try:
from zhenxun.services.ai.config import get_llm_config from zhenxun.services.ai.config import get_llm_config
from zhenxun.services.ai.llm.system.network import health_manager
from zhenxun.services.ai.tools.engine.registry import tool_provider_manager from zhenxun.services.ai.tools.engine.registry import tool_provider_manager
from .system.network import health_manager
get_llm_config() get_llm_config()
await health_manager.initialize() await health_manager.initialize()
await tool_provider_manager.initialize() await tool_provider_manager.initialize()
+28 -7
View File
@@ -8,11 +8,12 @@ from zhenxun.services.ai.core.models import ModelDetail, ModelModality
from zhenxun.services.ai.core.options import GenerationConfig from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.llm.builder import validate_override_params from zhenxun.services.ai.llm.builder import validate_override_params
from zhenxun.services.ai.llm.engine.service import LLMModel from zhenxun.services.ai.llm.engine.service import LLMModel
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.services.ai.llm.system.network import health_manager, http_client_manager
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import dump_json_safely, model_copy, model_dump from zhenxun.utils.pydantic_compat import dump_json_safely, model_copy, model_dump
from .capabilities import get_model_capabilities
from .network import health_manager, http_client_manager
_model_cache: dict[str, tuple[LLMModel, float]] = {} _model_cache: dict[str, tuple[LLMModel, float]] = {}
_cache_ttl = 3600 _cache_ttl = 3600
_max_cache_size = 10 _max_cache_size = 10
@@ -111,6 +112,22 @@ async def get_or_create_model(
capabilities = get_model_capabilities(model_detail_found.model_name) capabilities = get_model_capabilities(model_detail_found.model_name)
capabilities = model_copy(capabilities, deep=True) capabilities = model_copy(capabilities, deep=True)
if model_detail_found.max_input_tokens is not None:
capabilities.max_input_tokens = model_detail_found.max_input_tokens
base_gen_config = GenerationConfig()
if provider_config_found.temperature is not None:
base_gen_config.common.temperature = provider_config_found.temperature
if provider_config_found.max_output_tokens is not None:
base_gen_config.common.max_tokens = provider_config_found.max_output_tokens
if model_detail_found.temperature is not None:
base_gen_config.common.temperature = model_detail_found.temperature
if model_detail_found.max_output_tokens is not None:
base_gen_config.common.max_tokens = model_detail_found.max_output_tokens
if model_detail_found.reasoning_effort is not None:
base_gen_config.common.reasoning_effort = model_detail_found.reasoning_effort
if model_detail_found.task_type == "image_generation": if model_detail_found.task_type == "image_generation":
capabilities.output_modalities.add(ModelModality.IMAGE) capabilities.output_modalities.add(ModelModality.IMAGE)
capabilities.supports_tool_calling = False capabilities.supports_tool_calling = False
@@ -130,9 +147,8 @@ async def get_or_create_model(
timeout=default_timeout, timeout=default_timeout,
api_base=provider_config_found.api_base, api_base=provider_config_found.api_base,
api_type=provider_config_found.api_type, api_type=provider_config_found.api_type,
openai_compat=provider_config_found.openai_compat,
temperature=provider_config_found.temperature, temperature=provider_config_found.temperature,
generation_max_tokens=provider_config_found.generation_max_tokens, max_output_tokens=provider_config_found.max_output_tokens,
) )
shared_http_client = await http_client_manager.get_client(config_for_http_client) shared_http_client = await http_client_manager.get_client(config_for_http_client)
@@ -144,16 +160,21 @@ async def get_or_create_model(
health_manager=health_manager, health_manager=health_manager,
http_client=shared_http_client, http_client=shared_http_client,
capabilities=capabilities, capabilities=capabilities,
config_override=base_gen_config,
) )
if override_config: if override_config:
validated_override_params = validate_override_params(override_config) validated_override_params = validate_override_params(override_config)
model_instance._generation_config = validated_override_params final_config = base_gen_config.merge_with(validated_override_params)
model_instance.identity.generation_config = validated_override_params model_instance._generation_config = final_config
model_instance.identity.generation_config = final_config
logger.debug( logger.debug(
f"为新模型 {prov_name_str}/{mod_name_str} 应用配置覆盖: " f"为新模型 {prov_name_str}/{mod_name_str} 应用配置覆盖: "
f"{_get_clean_log_config(validated_override_params)}" f"{_get_clean_log_config(validated_override_params)}"
) )
else:
model_instance._generation_config = base_gen_config
model_instance.identity.generation_config = base_gen_config
_cache_model(cache_key, model_instance) _cache_model(cache_key, model_instance)
logger.debug( logger.debug(
+14 -1
View File
@@ -125,6 +125,7 @@ CAP_DEEPSEEK_V4 = ModelCapabilities(
input_modalities={ModelModality.TEXT}, input_modalities={ModelModality.TEXT},
output_modalities={ModelModality.TEXT}, output_modalities={ModelModality.TEXT},
supports_tool_calling=True, supports_tool_calling=True,
supports_thinking_toggle=True,
reasoning_mode=ReasoningMode.EFFORT, reasoning_mode=ReasoningMode.EFFORT,
reasoning_visibility="visible", reasoning_visibility="visible",
reasoning_effort_map={"minimal": "low"}, reasoning_effort_map={"minimal": "low"},
@@ -133,6 +134,7 @@ CAP_MINIMAX_REASONING = ModelCapabilities(
input_modalities={ModelModality.TEXT}, input_modalities={ModelModality.TEXT},
output_modalities={ModelModality.TEXT}, output_modalities={ModelModality.TEXT},
supports_tool_calling=True, supports_tool_calling=True,
supports_thinking_toggle=True,
reasoning_mode=ReasoningMode.EFFORT, reasoning_mode=ReasoningMode.EFFORT,
reasoning_visibility="visible", reasoning_visibility="visible",
) )
@@ -146,6 +148,15 @@ CAP_GLM_MULTIMODAL = ModelCapabilities(
}, },
output_modalities={ModelModality.TEXT}, output_modalities={ModelModality.TEXT},
supports_tool_calling=True, supports_tool_calling=True,
supports_thinking_toggle=True,
)
CAP_GLM_REASONING = ModelCapabilities(
input_modalities={ModelModality.TEXT},
output_modalities={ModelModality.TEXT},
supports_tool_calling=True,
supports_thinking_toggle=True,
reasoning_mode=ReasoningMode.EFFORT,
) )
CAP_MINIMAX_MULTIMODAL = ModelCapabilities( CAP_MINIMAX_MULTIMODAL = ModelCapabilities(
@@ -158,6 +169,7 @@ CAP_MIMO_TEXT = ModelCapabilities(
input_modalities={ModelModality.TEXT}, input_modalities={ModelModality.TEXT},
output_modalities={ModelModality.TEXT}, output_modalities={ModelModality.TEXT},
supports_tool_calling=True, supports_tool_calling=True,
supports_thinking_toggle=True,
supported_native_tools={"web_search"}, supported_native_tools={"web_search"},
reasoning_effort_map={"max": "high", "xhigh": "high", "minimal": "low"}, reasoning_effort_map={"max": "high", "xhigh": "high", "minimal": "low"},
) )
@@ -171,6 +183,7 @@ CAP_MIMO_MULTIMODAL = ModelCapabilities(
}, },
output_modalities={ModelModality.TEXT, ModelModality.AUDIO}, output_modalities={ModelModality.TEXT, ModelModality.AUDIO},
supports_tool_calling=True, supports_tool_calling=True,
supports_thinking_toggle=True,
supported_native_tools={"web_search"}, supported_native_tools={"web_search"},
reasoning_effort_map={"max": "high", "xhigh": "high", "minimal": "low"}, reasoning_effort_map={"max": "high", "xhigh": "high", "minimal": "low"},
) )
@@ -289,7 +302,7 @@ _ROUTING_TABLE: list[tuple[list[str], ModelCapabilities, int]] = [
CTX_256K, CTX_256K,
), ),
(["glm-5v*"], CAP_GLM_MULTIMODAL, CTX_200K), (["glm-5v*"], CAP_GLM_MULTIMODAL, CTX_200K),
(["glm-5*", "glm-4.7*", "glm-4.6*"], STANDARD_TEXT_TOOL_CAPABILITIES, CTX_200K), (["glm-5*", "glm-4.7*", "glm-4.6*"], CAP_GLM_REASONING, CTX_200K),
(["*MiniMax-M2*", "*minimax-m2*"], CAP_MINIMAX_REASONING, CTX_200K), (["*MiniMax-M2*", "*minimax-m2*"], CAP_MINIMAX_REASONING, CTX_200K),
(["gpt-4*", "gpt-3.5*", "gpt-*"], CAP_OPENAI_MULTIMODAL, CTX_128K), (["gpt-4*", "gpt-3.5*", "gpt-*"], CAP_OPENAI_MULTIMODAL, CTX_128K),
(["o1-*", "o3-*"], CAP_OPENAI_REASONING, CTX_128K), (["o1-*", "o3-*"], CAP_OPENAI_REASONING, CTX_128K),
+1 -1
View File
@@ -25,7 +25,7 @@ from zhenxun.services.ai.core.exceptions import (
QuotaExceededException, QuotaExceededException,
RateLimitException, RateLimitException,
) )
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.pydantic_compat import model_dump, parse_as from zhenxun.utils.pydantic_compat import model_dump, parse_as
from zhenxun.utils.user_agent import get_user_agent from zhenxun.utils.user_agent import get_user_agent
+13 -52
View File
@@ -1,23 +1,31 @@
from collections import defaultdict from collections import defaultdict
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import inspect
from io import BytesIO from io import BytesIO
import mimetypes import mimetypes
from pathlib import Path from pathlib import Path
from typing import Any, ClassVar, TypeVar, cast from typing import Any, ClassVar, TypeVar, cast
import anyio
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.adapters import Message as PlatformMessage from nonebot.adapters import Message as PlatformMessage
from nonebot.matcher import current_bot, current_event, current_matcher
from nonebot_plugin_alconna import UniMessage
from nonebot_plugin_alconna.uniseg import ( from nonebot_plugin_alconna.uniseg import (
At,
AtAll,
Audio, Audio,
Image, Image,
Reply,
Segment, Segment,
Text, Text,
UniMessage,
Video, Video,
Voice, Voice,
) )
from nonebot_plugin_alconna.uniseg.tools import image_fetch, reply_fetch
from PIL.Image import Image as PILImageType from PIL.Image import Image as PILImageType
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
from zhenxun.services.ai.core.messages import ( from zhenxun.services.ai.core.messages import (
AgentEvent, AgentEvent,
AudioPart, AudioPart,
@@ -35,7 +43,9 @@ from zhenxun.services.ai.core.messages import (
VideoPart, VideoPart,
) )
from zhenxun.services.ai.core.options import LLMEmbeddingConfig from zhenxun.services.ai.core.options import LLMEmbeddingConfig
from zhenxun.services.log import logger from zhenxun.services.ai.run import get_current_run_context
from zhenxun.services.ai.utils.logger import log_core as logger
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.pydantic_compat import TypeAdapter, model_copy, model_dump from zhenxun.utils.pydantic_compat import TypeAdapter, model_copy, model_dump
from zhenxun.utils.utils import infer_plugin_namespace from zhenxun.utils.utils import infer_plugin_namespace
@@ -96,16 +106,12 @@ class MessageBuilder:
) -> LLMContentPart | None: ) -> LLMContentPart | None:
"""将本地路径读取为多态消息部件""" """将本地路径读取为多态消息部件"""
try: try:
import anyio
aio_path = anyio.Path(path_like) aio_path = anyio.Path(path_like)
if not await aio_path.exists() or not await aio_path.is_file(): if not await aio_path.exists() or not await aio_path.is_file():
logger.warning(f"文件不存在或不是一个文件: {path_like}") logger.warning(f"文件不存在或不是一个文件: {path_like}")
return None return None
from pathlib import Path as StdPath std_path = Path(path_like)
std_path = StdPath(path_like)
resolved_aio_path = await aio_path.absolute() resolved_aio_path = await aio_path.absolute()
_ = resolved_aio_path _ = resolved_aio_path
@@ -233,11 +239,6 @@ class MessageBuilder:
"""获取并解析引用消息的内容片段""" """获取并解析引用消息的内容片段"""
namespace = namespace or infer_plugin_namespace(default="global") namespace = namespace or infer_plugin_namespace(default="global")
try: try:
from nonebot.adapters import Message as PlatformMessage
from nonebot_plugin_alconna import UniMessage
from nonebot_plugin_alconna.uniseg import Reply
from nonebot_plugin_alconna.uniseg.tools import reply_fetch
orig_msg = await reply_fetch(event, bot) orig_msg = await reply_fetch(event, bot)
if not orig_msg or not orig_msg.msg: if not orig_msg or not orig_msg.msg:
return None return None
@@ -283,17 +284,11 @@ class MessageBuilder:
reply_parts = [] reply_parts = []
try: try:
from nonebot.matcher import current_bot, current_event
from nonebot_plugin_alconna import UniMessage
from nonebot_plugin_alconna.uniseg import Reply
bot_inst = bot or current_bot.get(None) bot_inst = bot or current_bot.get(None)
event_inst = event or current_event.get(None) event_inst = event or current_event.get(None)
if not bot_inst or not event_inst: if not bot_inst or not event_inst:
try: try:
from zhenxun.services.ai.run import get_current_run_context
ctx = get_current_run_context() ctx = get_current_run_context()
if ctx: if ctx:
bot_inst = bot_inst or ctx.get_bot() bot_inst = bot_inst or ctx.get_bot()
@@ -319,7 +314,6 @@ class MessageBuilder:
converted_msgs: list[LLMMessage] = [] converted_msgs: list[LLMMessage] = []
converted = False converted = False
import inspect
for msg_type, converter_dict in cls._MESSAGE_CONVERTERS.items(): for msg_type, converter_dict in cls._MESSAGE_CONVERTERS.items():
if isinstance(message, msg_type): if isinstance(message, msg_type):
@@ -348,10 +342,6 @@ class MessageBuilder:
elif isinstance(message, str): elif isinstance(message, str):
converted_msgs = [LLMMessage.user(message)] converted_msgs = [LLMMessage.user(message)]
elif isinstance(message, AgentEvent): elif isinstance(message, AgentEvent):
from zhenxun.services.ai.core.engine.context_renderer import (
ContextConverter,
)
converted_msgs = ContextConverter.flatten_to_llm_messages([message]) converted_msgs = ContextConverter.flatten_to_llm_messages([message])
elif isinstance(message, list): elif isinstance(message, list):
parts = [] parts = []
@@ -402,14 +392,6 @@ class MessageBuilder:
elif config.multimodal is False: elif config.multimodal is False:
allowed_modalities = {"text"} allowed_modalities = {"text"}
from zhenxun.services.ai.core.messages import (
AudioPart,
FilePart,
ImagePart,
TextPart,
VideoPart,
)
messages = await cls.normalize_to_llm_messages( messages = await cls.normalize_to_llm_messages(
item, item,
bot=bot, bot=bot,
@@ -437,9 +419,6 @@ class MessageBuilder:
) -> "EmbedBatch": ) -> "EmbedBatch":
"""将任意输入标准化为嵌入向量批处理对象""" """将任意输入标准化为嵌入向量批处理对象"""
namespace = namespace or infer_plugin_namespace(default="global") namespace = namespace or infer_plugin_namespace(default="global")
from nonebot_plugin_alconna import UniMessage
from zhenxun.services.ai.core.messages import BaseContentPart, TextPart
if isinstance(inputs, list) and not isinstance(inputs, UniMessage): if isinstance(inputs, list) and not isinstance(inputs, UniMessage):
if not inputs: if not inputs:
@@ -510,9 +489,6 @@ def _extract_media_kwargs(seg: Segment, default_mime: str) -> dict | None:
async def _handle_image(seg: Image) -> ImagePart | None: async def _handle_image(seg: Image) -> ImagePart | None:
if not seg.raw and not getattr(seg, "path", None): if not seg.raw and not getattr(seg, "path", None):
try: try:
from nonebot.matcher import current_bot, current_event, current_matcher
from nonebot_plugin_alconna.uniseg.tools import image_fetch
bot = current_bot.get(None) bot = current_bot.get(None)
event = current_event.get(None) event = current_event.get(None)
matcher = current_matcher.get(None) matcher = current_matcher.get(None)
@@ -526,8 +502,6 @@ async def _handle_image(seg: Image) -> ImagePart | None:
logger.debug(f"底层静默水合下载图片失败: {e}") logger.debug(f"底层静默水合下载图片失败: {e}")
if not seg.raw and seg.url: if not seg.raw and seg.url:
from zhenxun.utils.http_utils import AsyncHttpx
try: try:
logger.debug(f"正在从临时 URL 物理固化图片: {seg.url[:50]}...") logger.debug(f"正在从临时 URL 物理固化图片: {seg.url[:50]}...")
raw_bytes = await AsyncHttpx.get_content(seg.url) raw_bytes = await AsyncHttpx.get_content(seg.url)
@@ -543,8 +517,6 @@ async def _handle_image(seg: Image) -> ImagePart | None:
async def _process_audio_seg(seg: Audio | Voice) -> AudioPart | None: async def _process_audio_seg(seg: Audio | Voice) -> AudioPart | None:
if not seg.raw and not getattr(seg, "path", None) and seg.url: if not seg.raw and not getattr(seg, "path", None) and seg.url:
from zhenxun.utils.http_utils import AsyncHttpx
try: try:
raw_bytes = await AsyncHttpx.get_content(seg.url) raw_bytes = await AsyncHttpx.get_content(seg.url)
if raw_bytes: if raw_bytes:
@@ -569,8 +541,6 @@ async def _handle_voice(seg: Voice) -> AudioPart | None:
@MessageBuilder.register_segment_handler(Video, scope="global") @MessageBuilder.register_segment_handler(Video, scope="global")
async def _handle_video(seg: Video) -> VideoPart | None: async def _handle_video(seg: Video) -> VideoPart | None:
if not seg.raw and not getattr(seg, "path", None) and seg.url: if not seg.raw and not getattr(seg, "path", None) and seg.url:
from zhenxun.utils.http_utils import AsyncHttpx
try: try:
raw_bytes = await AsyncHttpx.get_content(seg.url) raw_bytes = await AsyncHttpx.get_content(seg.url)
if raw_bytes: if raw_bytes:
@@ -582,21 +552,14 @@ async def _handle_video(seg: Video) -> VideoPart | None:
return VideoPart(**kwargs) if kwargs else None return VideoPart(**kwargs) if kwargs else None
from nonebot_plugin_alconna.uniseg import At, AtAll, Reply
@MessageBuilder.register_segment_handler(Reply, scope="global") @MessageBuilder.register_segment_handler(Reply, scope="global")
async def _handle_reply(seg: Reply) -> list[LLMContentPart] | LLMContentPart | None: async def _handle_reply(seg: Reply) -> list[LLMContentPart] | LLMContentPart | None:
try: try:
from nonebot.matcher import current_bot, current_event
bot = current_bot.get(None) bot = current_bot.get(None)
event = current_event.get(None) event = current_event.get(None)
if not bot or not event: if not bot or not event:
return None return None
from zhenxun.services.ai.run import get_current_run_context
ctx = get_current_run_context() ctx = get_current_run_context()
ns = getattr(ctx.session, "namespace", "global") if ctx else "global" ns = getattr(ctx.session, "namespace", "global") if ctx else "global"
@@ -618,8 +581,6 @@ async def _handle_at(seg: At) -> TextPart:
target_id = seg.target target_id = seg.target
try: try:
from nonebot.matcher import current_bot, current_event
bot = current_bot.get(None) bot = current_bot.get(None)
event = current_event.get(None) event = current_event.get(None)
-3
View File
@@ -1,7 +1,6 @@
from zhenxun.services.ai.core.models import CancellationToken from zhenxun.services.ai.core.models import CancellationToken
from .blackboard import BlackboardManager from .blackboard import BlackboardManager
from .capabilities import GLOBAL_CAPABILITIES, register_global_capability
from .context import ( from .context import (
NoneBotDeps, NoneBotDeps,
RunContext, RunContext,
@@ -19,7 +18,6 @@ from .session import session_manager
from .ui import UIController from .ui import UIController
__all__ = [ __all__ = [
"GLOBAL_CAPABILITIES",
"AgentRunResult", "AgentRunResult",
"AgentTask", "AgentTask",
"BlackboardManager", "BlackboardManager",
@@ -33,6 +31,5 @@ __all__ = [
"StreamedRunResult", "StreamedRunResult",
"UIController", "UIController",
"get_current_run_context", "get_current_run_context",
"register_global_capability",
"session_manager", "session_manager",
] ]
-40
View File
@@ -1,40 +0,0 @@
from __future__ import annotations
from collections import defaultdict
from zhenxun.services.ai.capabilities import (
AbstractCapability,
)
from zhenxun.services.log import logger
from zhenxun.utils.utils import infer_plugin_namespace
GLOBAL_CAPABILITIES: dict[str, list[AbstractCapability]] = defaultdict(list)
from zhenxun.services.ai.capabilities.builtin import (
BillingCapability,
GlobalCycleLimitCapability,
PermissionCapability,
ReflexionCapability,
StuckDetectionCapability,
ToolRetryAndReflectionCapability,
)
for _cap in [
GlobalCycleLimitCapability(),
StuckDetectionCapability(),
PermissionCapability(),
BillingCapability(),
ToolRetryAndReflectionCapability(),
ReflexionCapability(),
]:
GLOBAL_CAPABILITIES["global"].append(_cap)
def register_global_capability(
capability: AbstractCapability, scope: str | None = None
) -> None:
ns = scope if scope is not None else infer_plugin_namespace()
GLOBAL_CAPABILITIES[ns].append(capability)
logger.debug(
f"已注册全局 Capability: {capability.__class__.__name__} -> Namespace: {ns}"
)
+74 -32
View File
@@ -4,17 +4,31 @@ from contextlib import contextmanager
from contextvars import ContextVar from contextvars import ContextVar
import dataclasses import dataclasses
import inspect import inspect
from typing import Any, Generic, cast, get_origin from typing import TYPE_CHECKING, Any, Generic, cast, get_origin
from typing_extensions import TypeVar from typing_extensions import TypeVar
import uuid
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher, current_bot, current_event, current_matcher
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.capabilities.base import AbstractCapability
from zhenxun.services.ai.core.engine.append_only import AppendOnlyContextManager
from zhenxun.services.ai.core.messages import AgentEvent, AgentMessage from zhenxun.services.ai.core.messages import AgentEvent, AgentMessage
from zhenxun.services.ai.core.models import CancellationToken
from zhenxun.services.ai.core.protocols.tool import ToolExecutable
from zhenxun.services.ai.core.stream_events import AgentStreamEvent, EventBus
from zhenxun.services.ai.utils import ContextUtils from zhenxun.services.ai.utils import ContextUtils
from zhenxun.services.ai.utils.scope import ScopeSelector
from zhenxun.services.scheduler.types import ScheduleContext
from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.utils import infer_plugin_namespace from zhenxun.utils.utils import infer_plugin_namespace
from .blackboard import BlackboardManager
if TYPE_CHECKING:
from zhenxun.services.ai.context.memory.facades import AgentSessionFacade
AgentDepsT = TypeVar("AgentDepsT", default=Any) AgentDepsT = TypeVar("AgentDepsT", default=Any)
"""泛型类型变量:外部环境依赖对象 (Agent Dependencies)。""" """泛型类型变量:外部环境依赖对象 (Agent Dependencies)。"""
ProviderFunc = Callable[["RunContext"], Any | Awaitable[Any]] ProviderFunc = Callable[["RunContext"], Any | Awaitable[Any]]
@@ -40,19 +54,47 @@ class NoneBotDeps(BaseModel):
@classmethod @classmethod
def get_current(cls) -> "NoneBotDeps | None": def get_current(cls) -> "NoneBotDeps | None":
"""利用 NoneBot 原生魔法,基于 ContextVars 隐式提取当前执行上下文""" """利用 NoneBot 原生魔法,基于 ContextVars 隐式提取当前执行上下文"""
try:
from nonebot.matcher import current_bot, current_event, current_matcher
bot = current_bot.get(None) bot = current_bot.get(None)
event = current_event.get(None) event = current_event.get(None)
matcher = current_matcher.get(None) matcher = current_matcher.get(None)
if bot or event: if bot or event:
return cls(bot=bot, event=event, matcher=matcher) return cls(bot=bot, event=event, matcher=matcher)
except Exception:
pass
return None return None
class ScheduledDeps(BaseModel):
"""后台/定时任务环境下的标准依赖容器。"""
model_config = ConfigDict(arbitrary_types_allowed=True)
bot: Bot | None = Field(default=None)
"""机器人实例"""
group_id: str | None = Field(default=None)
"""群组/频道 ID"""
user_id: str | None = Field(default=None)
"""用户 ID"""
schedule_id: int | str | None = Field(default=None)
"""定时任务/调度任务 ID"""
platform: str | None = Field(default=None)
"""适配器平台标识"""
@classmethod
def from_schedule_context(
cls, bot: Bot, context: ScheduleContext
) -> "ScheduledDeps":
"""从调度器上下文中快速构造并提取依赖"""
return cls(
bot=bot,
group_id=getattr(context, "group_id", None),
user_id=getattr(context, "user_id", None),
schedule_id=getattr(context, "schedule_id", None),
platform=getattr(context, "platform_scope", None)
or PlatformUtils.get_platform_scope(bot),
)
@dataclasses.dataclass @dataclasses.dataclass
class SessionContext(Generic[AgentDepsT]): class SessionContext(Generic[AgentDepsT]):
""" """
@@ -66,9 +108,7 @@ class SessionContext(Generic[AgentDepsT]):
"""强类型的外部依赖注入对象(如 Bot, Event),供跨工具共享。""" """强类型的外部依赖注入对象(如 Bot, Event),供跨工具共享。"""
shared_state: dict[str, Any] = dataclasses.field(default_factory=dict) shared_state: dict[str, Any] = dataclasses.field(default_factory=dict)
"""共享状态字典:全局引用穿透,用于主智能体与嵌套子智能体之间的数据通信。""" """共享状态字典:全局引用穿透,用于主智能体与嵌套子智能体之间的数据通信。"""
auth_tokens: dict[str, str] = dataclasses.field(default_factory=dict) blackboard: BlackboardManager | None = None
"""授权凭证字典:保存用户针对各 Provider 的 OAuth 或 API Token。"""
blackboard: Any | None = None
"""结构化黑板管理器,作为共享状态的高级替代方案,提供并发锁和强类型校验。""" """结构化黑板管理器,作为共享状态的高级替代方案,提供并发锁和强类型校验。"""
namespace: str = "global" namespace: str = "global"
"""触发事件的插件命名空间""" """触发事件的插件命名空间"""
@@ -76,7 +116,7 @@ class SessionContext(Generic[AgentDepsT]):
"""用于大模型前缀缓存命中优化的追加写入管理器。""" """用于大模型前缀缓存命中优化的追加写入管理器。"""
@property @property
def memory(self) -> Any: def memory(self) -> "AgentSessionFacade":
""" """
获取当前会话的持久化记忆访问门面 (AgentSessionFacade)。 获取当前会话的持久化记忆访问门面 (AgentSessionFacade)。
提供极简的 history 和 slots 操作 API。 提供极简的 history 和 slots 操作 API。
@@ -84,7 +124,6 @@ class SessionContext(Generic[AgentDepsT]):
from zhenxun.services.ai.context.memory.facades import AgentSessionFacade from zhenxun.services.ai.context.memory.facades import AgentSessionFacade
from zhenxun.services.ai.context.memory.manager import memory_manager from zhenxun.services.ai.context.memory.manager import memory_manager
from zhenxun.services.ai.context.memory.types import SessionMetadata from zhenxun.services.ai.context.memory.types import SessionMetadata
from zhenxun.services.ai.utils.scope import ScopeSelector
user_id = ContextUtils.extract_user_id(self.deps) user_id = ContextUtils.extract_user_id(self.deps)
group_id = ContextUtils.extract_group_id(self.deps) group_id = ContextUtils.extract_group_id(self.deps)
@@ -127,9 +166,9 @@ class AgentRunContext(Generic[AgentDepsT]):
"""子智能体委派深度标记,用于防范无限递归嵌套。""" """子智能体委派深度标记,用于防范无限递归嵌套。"""
tool_retries: dict[str, int] = dataclasses.field(default_factory=dict) tool_retries: dict[str, int] = dataclasses.field(default_factory=dict)
"""记录当前轮次内各个工具的累积失败重试次数,用于系统熔断。""" """记录当前轮次内各个工具的累积失败重试次数,用于系统熔断。"""
cancellation_token: Any | None = None cancellation_token: CancellationToken | None = None
"""全局级联取消令牌,用于跨 Agent 的协程挂起中断。""" """全局级联取消令牌,用于跨 Agent 的协程挂起中断。"""
event_bus: Any | None = None event_bus: EventBus | None = None
"""底层的事件流发射器 (EventBus),由执行引擎在运行时挂载。""" """底层的事件流发射器 (EventBus),由执行引擎在运行时挂载。"""
dynamic_prompts: dict[str, str] = dataclasses.field(default_factory=dict) dynamic_prompts: dict[str, str] = dataclasses.field(default_factory=dict)
@@ -146,6 +185,11 @@ class AgentRunContext(Generic[AgentDepsT]):
"""向当前运行上下文中安全追加业务事件""" """向当前运行上下文中安全追加业务事件"""
self.messages.append(event) self.messages.append(event)
async def emit(self, event: AgentStreamEvent) -> None:
"""安全地向事件总线发射流式事件(内置判空逻辑,消解业务层的样板代码)"""
if self.event_bus:
await self.event_bus.emit(event)
@dataclasses.dataclass @dataclasses.dataclass
class ToolCallContext(Generic[AgentDepsT]): class ToolCallContext(Generic[AgentDepsT]):
@@ -162,7 +206,7 @@ class ToolCallContext(Generic[AgentDepsT]):
"""本次调用的工具名称。""" """本次调用的工具名称。"""
retry_count: int = 0 retry_count: int = 0
"""当前工具调用的重试序号 (第几次重试)。""" """当前工具调用的重试序号 (第几次重试)。"""
current_tool: Any | None = None current_tool: ToolExecutable | None = None
"""当前工具的可执行实例 (ToolExecutable)。""" """当前工具的可执行实例 (ToolExecutable)。"""
@@ -188,7 +232,7 @@ class RunContext(Generic[AgentDepsT]):
upstream_results: dict[str, Any] = dataclasses.field(default_factory=dict) upstream_results: dict[str, Any] = dataclasses.field(default_factory=dict)
"""前置节点产出字典:标准化的数据流载荷契约,键为 Agent Name,值为输出内容""" """前置节点产出字典:标准化的数据流载荷契约,键为 Agent Name,值为输出内容"""
capabilities: list[Any] = dataclasses.field(default_factory=list) capabilities: list[AbstractCapability] = dataclasses.field(default_factory=list)
"""当前上下文绑定的拦截器 (Capabilities) 链,用于生命周期拦截""" """当前上下文绑定的拦截器 (Capabilities) 链,用于生命周期拦截"""
deps: AgentDepsT = dataclasses.field(default=cast(AgentDepsT, None)) deps: AgentDepsT = dataclasses.field(default=cast(AgentDepsT, None))
@@ -211,6 +255,14 @@ class RunContext(Generic[AgentDepsT]):
) )
"""标记 session_id 是否为框架隐式生成的。""" """标记 session_id 是否为框架隐式生成的。"""
@classmethod
def from_schedule(
cls, bot: Bot, context: ScheduleContext, **kwargs
) -> "RunContext[ScheduledDeps]":
"""极简构造语法糖:从定时调度任务上下文中直接生成 RunContext"""
deps = ScheduledDeps.from_schedule_context(bot, context)
return cast("RunContext[ScheduledDeps]", cls(deps=cast(Any, deps), **kwargs))
def get_bot(self) -> Bot | None: def get_bot(self) -> Bot | None:
"""强类型安全地提取 Bot 实例""" """强类型安全地提取 Bot 实例"""
if not self.deps: if not self.deps:
@@ -256,21 +308,15 @@ class RunContext(Generic[AgentDepsT]):
bot = self.get_bot() bot = self.get_bot()
event = self.get_event() event = self.get_event()
if bot and event: if bot:
meta = ContextUtils.generate_session_meta( meta = ContextUtils.generate_session_meta(
bot, event, scope_builder=Isolation.AGENT_USER() bot=bot,
event=event,
deps=self.deps,
scope_builder=Isolation.AGENT_USER(),
) )
self.session_id = meta.session_id self.session_id = meta.session_id
self._is_auto_session_id = True self._is_auto_session_id = True
else:
uid = self.get_user_id()
gid = self.get_group_id()
if uid and gid:
self.session_id = f"auto_{gid}_{uid}"
self._is_auto_session_id = True
elif uid:
self.session_id = f"auto_private_{uid}"
self._is_auto_session_id = True
ns = "global" ns = "global"
if self.deps: if self.deps:
@@ -286,9 +332,6 @@ class RunContext(Generic[AgentDepsT]):
shared_state=self.shared_state, shared_state=self.shared_state,
namespace=ns, namespace=ns,
) )
from zhenxun.services.ai.core.engine.append_only import (
AppendOnlyContextManager,
)
self.session.append_only_manager = AppendOnlyContextManager() self.session.append_only_manager = AppendOnlyContextManager()
@@ -351,8 +394,6 @@ class RunContext(Generic[AgentDepsT]):
new_ctx.run.messages = [] new_ctx.run.messages = []
new_ctx.capabilities = [] new_ctx.capabilities = []
import uuid
base_sid = self.session_id or "default" base_sid = self.session_id or "default"
new_ctx.session_id = f"{base_sid}/sub_{member_name}_{uuid.uuid4().hex[:6]}" new_ctx.session_id = f"{base_sid}/sub_{member_name}_{uuid.uuid4().hex[:6]}"
new_ctx.session.session_id = new_ctx.session_id new_ctx.session.session_id = new_ctx.session_id
@@ -403,6 +444,7 @@ __all__ = [
"AgentRunContext", "AgentRunContext",
"NoneBotDeps", "NoneBotDeps",
"RunContext", "RunContext",
"ScheduledDeps",
"SessionContext", "SessionContext",
"ToolCallContext", "ToolCallContext",
"ToolsPrepareFunc", "ToolsPrepareFunc",
+18 -1
View File
@@ -9,9 +9,9 @@ from nonebot.utils import is_coroutine_callable
from nonebot_plugin_session import EventSession, extract_session from nonebot_plugin_session import EventSession, extract_session
from zhenxun.services.ai.context.memory.facades import AgentSessionFacade from zhenxun.services.ai.context.memory.facades import AgentSessionFacade
from zhenxun.services.ai.run.blackboard import BlackboardManager
from zhenxun.utils.utils import infer_plugin_namespace from zhenxun.utils.utils import infer_plugin_namespace
from .blackboard import BlackboardManager
from .context import ProviderFunc, RunContext, _is_run_context_type from .context import ProviderFunc, RunContext, _is_run_context_type
from .hitl import HITLController from .hitl import HITLController
from .ui import UIController from .ui import UIController
@@ -105,6 +105,10 @@ class Inject:
""" """
return _UpstreamResultMarker(step_name) return _UpstreamResultMarker(step_name)
CurrentBlackboardState = Annotated[Any, Hidden(), _InjectMarker("blackboard_state")]
BlackboardState = CurrentBlackboardState
"""自动注入:当前挂载的强类型黑板底层 Pydantic 实例(需配合 Annotated 使用)"""
UserId = CurrentUserId UserId = CurrentUserId
"""自动注入:触发当前任务的用户 ID""" """自动注入:触发当前任务的用户 ID"""
@@ -366,6 +370,19 @@ def _resolve_blackboard(ctx: RunContext):
Inject.register_provider("blackboard", _resolve_blackboard, scope="global") 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): def _resolve_session(ctx):
bot = ctx.get_bot() bot = ctx.get_bot()
event = ctx.get_event() event = ctx.get_event()
+8 -6
View File
@@ -1,17 +1,17 @@
from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable from collections.abc import Callable
import time import time
from typing import TYPE_CHECKING
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from nonebot_plugin_waiter import waiter from nonebot_plugin_waiter import waiter
from zhenxun.services.ai.core.exceptions import AbortException, ToolFatalError from zhenxun.services.ai.core.exceptions import AbortException, ToolFatalError
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_agent as logger
if TYPE_CHECKING: from .context import RunContext
from zhenxun.services.ai.run.context import RunContext
CANCEL_WORDS = {"取消", "cancel", "0", "退出", "quit"} CANCEL_WORDS = {"取消", "cancel", "0", "退出", "quit"}
CONFIRM_WORDS = {"y", "yes", "是", "1", "ok", "确认"} CONFIRM_WORDS = {"y", "yes", "是", "1", "ok", "确认"}
@@ -24,7 +24,7 @@ class HITLController:
封装底层的物理环境隔离等待逻辑,供上层工具和中间件发起提问或审批。 封装底层的物理环境隔离等待逻辑,供上层工具和中间件发起提问或审批。
""" """
def __init__(self, context: "RunContext"): def __init__(self, context: RunContext):
self.context = context self.context = context
async def wait_event( async def wait_event(
@@ -42,7 +42,9 @@ class HITLController:
if not bot or not event: if not bot or not event:
logger.warning("HITLController: 当前环境无 Bot/Event 实例,无法发起交互。") logger.warning("HITLController: 当前环境无 Bot/Event 实例,无法发起交互。")
return None raise ToolFatalError(
"当前处于自动化后台调度环境,无法发起人工交互。任务已强行中止。"
)
if prompt_msg: if prompt_msg:
await bot.send(event, prompt_msg) await bot.send(event, prompt_msg)
+16 -28
View File
@@ -1,8 +1,10 @@
from __future__ import annotations
""" """
运行时(Run)相关核心类型定义 运行时(Run)相关核心类型定义
""" """
from collections.abc import AsyncIterator from collections.abc import AsyncIterator, Callable
import json import json
from typing import Any, Generic, cast from typing import Any, Generic, cast
from typing_extensions import TypeVar from typing_extensions import TypeVar
@@ -10,12 +12,16 @@ from typing_extensions import TypeVar
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
from zhenxun.services.ai.core.messages import AgentMessage, LLMMessage, UsageInfo from zhenxun.services.ai.core.messages import AgentMessage, LLMMessage, UsageInfo
from zhenxun.services.ai.core.options import BaseOutputDefinition
from zhenxun.services.ai.core.protocols.tool import ToolResolvable
from zhenxun.services.ai.core.stream_events import ( from zhenxun.services.ai.core.stream_events import (
AgentStreamEvent, AgentStreamEvent,
EventBus, EventBus,
ToolCallStartEvent, ToolCallStartEvent,
ToolStreamChunkEvent, ToolStreamChunkEvent,
) )
from zhenxun.services.ai.guardrails import BaseGuardrail, GuardrailSource
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.utils.pydantic_compat import model_dump, model_validator from zhenxun.utils.pydantic_compat import model_dump, model_validator
@@ -37,7 +43,7 @@ class ToolSummary(BaseModel):
"""工具执行失败次数""" """工具执行失败次数"""
total_latency_ms: float = 0.0 total_latency_ms: float = 0.0
"""工具执行总耗时(毫秒)""" """工具执行总耗时(毫秒)"""
by_name: dict[str, dict[str, Any]] = Field(default_factory=dict) by_name: dict[str, dict[str, float | int]] = Field(default_factory=dict)
"""按工具名称细分的执行状态统计""" """按工具名称细分的执行状态统计"""
@@ -126,9 +132,8 @@ class StreamedRunResult(Generic[OutputDataT]):
self.is_complete: bool = False self.is_complete: bool = False
self._result: AgentRunResult[OutputDataT] | None = None self._result: AgentRunResult[OutputDataT] | None = None
async def stream_events(self) -> AsyncIterator[Any]: async def stream_events(self) -> "AsyncIterator[AgentStreamEvent]":
"""获取底层的所有原始事件(包含工具调用过程等)""" """获取底层的所有原始事件(包含工具调用过程等)"""
from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunError
async for event in self._event_bus: async for event in self._event_bus:
if isinstance(event, AgentRunEnd): if isinstance(event, AgentRunEnd):
@@ -214,24 +219,6 @@ class StreamedRunResult(Generic[OutputDataT]):
return await self.get_run_result() return await self.get_run_result()
class TaskResult(BaseModel):
"""单个数据契约任务的执行结果"""
task_id: str
"""关联的任务唯一 ID"""
output: Any
"""任务的实际产出(如果是结构化任务,则为解析后的 Pydantic 实例;否则为纯文本)"""
raw_response: str | None = None
"""大模型返回的原始纯文本内容"""
usage: UsageInfo = Field(default_factory=UsageInfo)
"""该任务执行期间的 Token 消耗统计"""
model_config = ConfigDict(arbitrary_types_allowed=True)
class AgentTask(BaseModel): class AgentTask(BaseModel):
"""标准化数据契约(意图载体 Payload),定义大模型需要做什么及产出什么格式""" """标准化数据契约(意图载体 Payload),定义大模型需要做什么及产出什么格式"""
@@ -247,23 +234,25 @@ class AgentTask(BaseModel):
expected_output: str expected_output: str
"""预期输出的自然语言描述(指导大模型如何组织最终答案)""" """预期输出的自然语言描述(指导大模型如何组织最终答案)"""
response_model: Any | None = None response_model: type[BaseModel] | BaseOutputDefinition | None = None
"""强制要求返回的强类型结构 (Pydantic Model) 或 """强制要求返回的强类型结构 (Pydantic Model) 或
OutputDefinition,为空则返回普通文本""" OutputDefinition,为空则返回普通文本"""
tools: list[str | Any] | None = None tools: list[str | Callable | dict[str, Any] | BaseTool | ToolResolvable] | None = (
None
)
"""针对此特定任务动态追加或覆盖的工具列表""" """针对此特定任务动态追加或覆盖的工具列表"""
guardrails: list[Any] | None = None guardrails: list[GuardrailSource] | None = None
"""护栏验证列表。支持传入函数、BaseGuardrail 实例, """护栏验证列表。支持传入函数、BaseGuardrail 实例,
或直接传入自然语言字符串规则(自动转为 LLM 裁判)""" 或直接传入自然语言字符串规则(自动转为 LLM 裁判)"""
_parsed_guardrails: list[Any] = PrivateAttr(default_factory=list) _parsed_guardrails: list[BaseGuardrail] = PrivateAttr(default_factory=list)
model_config = ConfigDict(arbitrary_types_allowed=True) model_config = ConfigDict(arbitrary_types_allowed=True)
@model_validator(mode="after") @model_validator(mode="after")
def _parse_and_set_guardrails(self) -> "AgentTask": def _parse_and_set_guardrails(self) -> AgentTask:
from zhenxun.services.ai.guardrails import parse_guardrails from zhenxun.services.ai.guardrails import parse_guardrails
self._parsed_guardrails = parse_guardrails(self.guardrails) self._parsed_guardrails = parse_guardrails(self.guardrails)
@@ -275,5 +264,4 @@ __all__ = [
"AgentTask", "AgentTask",
"OutputDataT", "OutputDataT",
"StreamedRunResult", "StreamedRunResult",
"TaskResult",
] ]
+1 -1
View File
@@ -108,7 +108,7 @@ class AgentSessionManager:
count += 1 count += 1
if task and not task.done(): if task and not task.done():
task.cancel() task.cancel()
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_agent as logger
if count > 0: if count > 0:
logger.info( logger.info(
+25 -4
View File
@@ -13,9 +13,12 @@ from zhenxun.services.ai.core.stream_events import (
ToolStreamChunkEvent, ToolStreamChunkEvent,
UserCustomEvent, UserCustomEvent,
) )
from zhenxun.services.ai.run.context import RunContext from zhenxun.services.ai.utils.logger import log_agent as logger
from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunStart, AgentRunSummary from zhenxun.utils.message import MessageUtils
from zhenxun.services.log import logger from zhenxun.utils.platform import PlatformUtils
from .context import RunContext
from .models import AgentRunEnd, AgentRunStart, AgentRunSummary
class TelemetrySubscriber: class TelemetrySubscriber:
@@ -122,7 +125,7 @@ class DefaultUISubscriber:
async def _send_to_platform(self, display: Any): async def _send_to_platform(self, display: Any):
"""将通用的显示内容或富文本消息部件渲染并发送至具体的聊天平台""" """将通用的显示内容或富文本消息部件渲染并发送至具体的聊天平台"""
if not self.bot or not self.event or not display: if not self.bot or not display:
return return
if ( if (
@@ -144,9 +147,27 @@ class DefaultUISubscriber:
display = msg display = msg
if isinstance(display, UniMessage): if isinstance(display, UniMessage):
if self.event:
await display.send(self.event, bot=self.bot, reply_to=self.reply_to) await display.send(self.event, bot=self.bot, reply_to=self.reply_to)
else: else:
target = PlatformUtils.get_target(
user_id=self.context.get_user_id(),
group_id=self.context.get_group_id(),
)
if target:
await display.send(target=target, bot=self.bot)
else:
if self.event:
await self.bot.send(self.event, str(display)) await self.bot.send(self.event, str(display))
else:
target = PlatformUtils.get_target(
user_id=self.context.get_user_id(),
group_id=self.context.get_group_id(),
)
if target:
await MessageUtils.build_message(str(display)).send(
target=target, bot=self.bot
)
async def on_tool_stream(self, event: ToolStreamChunkEvent): async def on_tool_stream(self, event: ToolStreamChunkEvent):
"""处理工具流式数据块事件,仅在 verbose 开启时发送给平台""" """处理工具流式数据块事件,仅在 verbose 开启时发送给平台"""
+26 -25
View File
@@ -1,17 +1,20 @@
from typing import TYPE_CHECKING, Any from __future__ import annotations
from typing import Any
from nonebot_plugin_alconna import UniMessage from nonebot_plugin_alconna import UniMessage
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent, UserCustomEvent from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent, UserCustomEvent
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
if TYPE_CHECKING: from .context import RunContext
from zhenxun.services.ai.run.context import RunContext
class UIController: class UIController:
"""前端 UI 富交互流式控制器 (按需生成模式)""" """前端 UI 富交互流式控制器 (按需生成模式)"""
def __init__(self, context: "RunContext"): def __init__(self, context: RunContext):
self.context = context self.context = context
@property @property
@@ -19,15 +22,9 @@ class UIController:
"""从上下文中动态获取当前调用的工具名""" """从上下文中动态获取当前调用的工具名"""
return getattr(self.context.call, "tool_name", "UnknownTool") return getattr(self.context.call, "tool_name", "UnknownTool")
@property
def _event_bus(self) -> Any | None:
"""从运行时上下文中获取底层的事件发射器"""
return getattr(self.context.run, "event_bus", None)
async def send_text(self, text: str, status: str = "running") -> None: async def send_text(self, text: str, status: str = "running") -> None:
"""向前端流式反馈执行进度文本""" """向前端流式反馈执行进度文本"""
if self._event_bus: await self.context.run.emit(
await self._event_bus.emit(
ToolStreamChunkEvent( ToolStreamChunkEvent(
tool_name=self.tool_name, content=text, metadata={"status": status} tool_name=self.tool_name, content=text, metadata={"status": status}
) )
@@ -35,22 +32,21 @@ class UIController:
async def send_image(self, image: bytes | str) -> None: async def send_image(self, image: bytes | str) -> None:
"""向前端发送富文本图片气泡(字节流或 URL)""" """向前端发送富文本图片气泡(字节流或 URL)"""
if self._event_bus:
display_msg = UniMessage() display_msg = UniMessage()
if isinstance(image, bytes): if isinstance(image, bytes):
display_msg = display_msg.image(raw=image) display_msg = display_msg.image(raw=image)
else: else:
display_msg = display_msg.image(url=image) display_msg = display_msg.image(url=image)
await self._event_bus.emit(UserCustomEvent(display=display_msg)) await self.context.run.emit(UserCustomEvent(display=display_msg))
async def send_display(self, display: Any) -> None: async def send_display(self, display: Any) -> None:
"""向前端发送任意展示载体""" """向前端发送任意展示载体"""
if self._event_bus and display is not None: if display is not None:
await self._event_bus.emit(UserCustomEvent(display=display)) await self.context.run.emit(UserCustomEvent(display=display))
@staticmethod @staticmethod
async def handle_control_flow_exit_display( async def handle_control_flow_exit_display(
e: BaseException, context: "RunContext | None", reply_to: bool = False e: BaseException, context: RunContext | None, reply_to: bool = False
) -> None: ) -> None:
"""统一处理 ControlFlowExit 异常带来的 UI 反馈逻辑""" """统一处理 ControlFlowExit 异常带来的 UI 反馈逻辑"""
from zhenxun.services.ai.core.exceptions import ControlFlowExit from zhenxun.services.ai.core.exceptions import ControlFlowExit
@@ -63,19 +59,24 @@ class UIController:
return return
try: try:
from nonebot_plugin_alconna import UniMessage
bot = context.get_bot() if context else None bot = context.get_bot() if context else None
event = context.get_event() if context else None event = context.get_event() if context else None
if bot and event: if bot:
if isinstance(display_msg, UniMessage): msg_to_send = (
await display_msg.send(event, bot=bot, reply_to=reply_to) display_msg
if isinstance(display_msg, UniMessage)
else MessageUtils.build_message(str(display_msg))
)
if event:
await msg_to_send.send(event, bot=bot, reply_to=reply_to)
elif context:
target = PlatformUtils.get_target(
user_id=context.get_user_id(), group_id=context.get_group_id()
)
if target:
await msg_to_send.send(target=target, bot=bot)
else: else:
await bot.send(event, str(display_msg))
else:
from zhenxun.utils.message import MessageUtils
await MessageUtils.build_message(str(display_msg)).send( await MessageUtils.build_message(str(display_msg)).send(
reply_to=reply_to reply_to=reply_to
) )
+1 -1
View File
@@ -3,7 +3,7 @@ from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_sandbox as logger
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.sandbox.drivers.base import BaseSandboxSession from zhenxun.services.ai.sandbox.drivers.base import BaseSandboxSession
@@ -7,12 +7,13 @@ from anyio import create_memory_object_stream, create_task_group
from mcp.shared.message import SessionMessage from mcp.shared.message import SessionMessage
from mcp.types import JSONRPCMessage from mcp.types import JSONRPCMessage
from zhenxun.services.ai.sandbox.addons.base import BaseMcpProxyExtension
from zhenxun.services.ai.sandbox.protocols import SupportsStreamExecution from zhenxun.services.ai.sandbox.protocols import SupportsStreamExecution
from zhenxun.services.ai.sandbox.registry import SandboxRegistry from zhenxun.services.ai.sandbox.registry import SandboxRegistry
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_sandbox as logger
from zhenxun.utils.pydantic_compat import model_dump_json, model_validate from zhenxun.utils.pydantic_compat import model_dump_json, model_validate
from .base import BaseMcpProxyExtension
class UniversalMcpExtension(BaseMcpProxyExtension): class UniversalMcpExtension(BaseMcpProxyExtension):
"""通用 MCP 代理扩展类,用于在沙箱内连接 MCP 服务""" """通用 MCP 代理扩展类,用于在沙箱内连接 MCP 服务"""
+7 -21
View File
@@ -24,7 +24,7 @@ from zhenxun.services.ai.sandbox.protocols import (
SandboxProcessStream, SandboxProcessStream,
) )
from zhenxun.services.ai.sandbox.storage import RESOLVE_PATH_HELPER, coerce_posix_path from zhenxun.services.ai.sandbox.storage import RESOLVE_PATH_HELPER, coerce_posix_path
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_sandbox as logger
from .base import BaseSandboxClient, BaseSandboxSession from .base import BaseSandboxClient, BaseSandboxSession
@@ -388,8 +388,6 @@ class DockerSandboxSession(BaseSandboxSession):
await self.container.put_archive(sandbox_target_path, tar_bytes) await self.container.put_archive(sandbox_target_path, tar_bytes)
return True return True
except Exception as e: except Exception as e:
from zhenxun.services.log import logger
logger.error(f"[Docker I/O] 上传目录失败: {e}") logger.error(f"[Docker I/O] 上传目录失败: {e}")
return False return False
@@ -552,10 +550,7 @@ class DockerSandboxClient(BaseSandboxClient):
DockerSandboxClient._containers[eff_cname] = container DockerSandboxClient._containers[eff_cname] = container
DockerSandboxClient._jupyter_ports[eff_cname] = jupyter_port DockerSandboxClient._jupyter_ports[eff_cname] = jupyter_port
logger.info( logger.info(f"已启动物理隔离容器: {eff_cname} (镜像: {eff_image})")
f"[DockerSandbox] 已启动物理隔离容器: {eff_cname} "
f"(镜像: {eff_image})"
)
state = SandboxSessionState( state = SandboxSessionState(
session_id=session_id, session_id=session_id,
@@ -575,10 +570,7 @@ class DockerSandboxClient(BaseSandboxClient):
"test -x /global_env/python_venv/bin/pip" "test -x /global_env/python_venv/bin/pip"
) )
if check_venv.exit_code != 0: if check_venv.exit_code != 0:
logger.info( logger.info(f"正在初始化/修复容器 [{eff_cname}] 的共享 Python 虚拟环境...")
f"正在初始化/修复容器 [{eff_cname}] 的共享 Python 虚拟环境...",
command="SandboxManager",
)
init_res = await session.run_process( init_res = await session.run_process(
"rm -rf /global_env/python_venv && " "rm -rf /global_env/python_venv && "
"uv venv --seed --system-site-packages /global_env/python_venv || " "uv venv --seed --system-site-packages /global_env/python_venv || "
@@ -586,8 +578,7 @@ class DockerSandboxClient(BaseSandboxClient):
) )
if init_res.exit_code != 0: if init_res.exit_code != 0:
logger.error( logger.error(
f"初始化虚拟环境失败: {init_res.stderr or init_res.stdout}", f"初始化虚拟环境失败: {init_res.stderr or init_res.stdout}"
command="SandboxManager",
) )
return session return session
@@ -615,17 +606,14 @@ class DockerSandboxClient(BaseSandboxClient):
await self._containers[cname].delete(force=True) await self._containers[cname].delete(force=True)
self._containers.pop(cname, None) self._containers.pop(cname, None)
self._jupyter_ports.pop(cname, None) self._jupyter_ports.pop(cname, None)
logger.info( logger.info(f"物理容器 {cname} 已长时间闲置,已触发彻底销毁释放内存。")
f"[DockerSandbox] 物理容器 {cname} "
"已长时间闲置,已触发彻底销毁释放内存。"
)
except Exception as e: except Exception as e:
self._containers.pop(cname, None) self._containers.pop(cname, None)
self._jupyter_ports.pop(cname, None) self._jupyter_ports.pop(cname, None)
if getattr(e, "status", None) == 404 or "No such container" in str(e): if getattr(e, "status", None) == 404 or "No such container" in str(e):
logger.debug(f"[DockerSandbox] 物理容器 {cname} 已不存在。") logger.debug(f"物理容器 {cname} 已不存在。")
else: else:
logger.error(f"[DockerSandbox] 闲置销毁物理容器 {cname} 失败: {e}") logger.error(f"闲置销毁物理容器 {cname} 失败: {e}")
@classmethod @classmethod
async def close_env(cls): async def close_env(cls):
@@ -663,8 +651,6 @@ class DockerSandboxClient(BaseSandboxClient):
except Exception: except Exception:
pass pass
if count > 0: if count > 0:
from zhenxun.services.log import logger
logger.info( logger.info(
f"静默清理:已成功回收 {count} 个上次异常退出遗留的沙箱容器。", f"静默清理:已成功回收 {count} 个上次异常退出遗留的沙箱容器。",
command="SandboxManager", command="SandboxManager",
+3 -3
View File
@@ -1,11 +1,11 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, ClassVar from typing import TYPE_CHECKING, Any, ClassVar
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_sandbox as logger
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.sandbox.drivers.base import BaseSandboxSession from .drivers.base import BaseSandboxSession
from zhenxun.services.ai.sandbox.models import SandboxBlueprint from .models import SandboxBlueprint
class BaseProvisioner(ABC): class BaseProvisioner(ABC):
+8 -22
View File
@@ -4,11 +4,11 @@ from typing import Any, cast
import nonebot import nonebot
from zhenxun.services.ai.config import get_llm_config from zhenxun.services.ai.config import get_llm_config
from zhenxun.services.ai.sandbox.models import SandboxBlueprint from zhenxun.services.ai.utils.logger import log_sandbox as logger
from zhenxun.services.log import logger
from zhenxun.utils.lifespan import LifespanManager from zhenxun.utils.lifespan import LifespanManager
from .drivers.base import BaseSandboxClient, BaseSandboxSession from .drivers.base import BaseSandboxClient, BaseSandboxSession
from .models import SandboxBlueprint
from .registry import SandboxRegistry from .registry import SandboxRegistry
_startup_tasks = set() _startup_tasks = set()
@@ -84,10 +84,7 @@ class SandboxManager:
session = self._active_sessions[session_id] session = self._active_sessions[session_id]
is_alive = await session.is_alive() is_alive = await session.is_alive()
if not is_alive: if not is_alive:
logger.warning( logger.warning(f"⚠️ Session '{session_id}' 容器已死亡,重建中。")
f"⚠️ Session '{session_id}' 容器已死亡,重建中。",
command="SandboxManager",
)
await self.release_resource(session_id) await self.release_resource(session_id)
else: else:
session.touch() session.touch()
@@ -98,9 +95,7 @@ class SandboxManager:
await session.mount_extension(p) await session.mount_extension(p)
return session return session
logger.info( logger.info(f"为 Session '{session_id}' 创建沙箱环境...")
f"为 Session '{session_id}' 创建沙箱环境...", command="SandboxManager"
)
client = self._get_client(blueprint) client = self._get_client(blueprint)
try: try:
session = await client.create(session_id, blueprint) session = await client.create(session_id, blueprint)
@@ -121,10 +116,7 @@ class SandboxManager:
from zhenxun.services.ai.core.exceptions import SandboxFatalError from zhenxun.services.ai.core.exceptions import SandboxFatalError
err_msg = str(e) or type(e).__name__ err_msg = str(e) or type(e).__name__
logger.error( logger.error(f"创建 Session 失败: {err_msg}\n{traceback.format_exc()}")
f"创建 Session 失败: {err_msg}\n{traceback.format_exc()}",
command="SandboxManager",
)
if not isinstance(e, SandboxFatalError): if not isinstance(e, SandboxFatalError):
raise SandboxFatalError(f"沙箱初始化异常: {err_msg}") from e raise SandboxFatalError(f"沙箱初始化异常: {err_msg}") from e
raise e raise e
@@ -134,15 +126,12 @@ class SandboxManager:
) -> bool: ) -> bool:
"""统一扫描指定工作区,通过 Provisioner 体系完成环境装配""" """统一扫描指定工作区,通过 Provisioner 体系完成环境装配"""
if session_id not in self._active_sessions: if session_id not in self._active_sessions:
logger.warning( logger.warning(f"找不到活跃的 Session '{session_id}',无法配置环境。")
f"找不到活跃的 Session '{session_id}',无法配置环境。",
command="SandboxManager",
)
return False return False
session = self._active_sessions[session_id] session = self._active_sessions[session_id]
from zhenxun.services.ai.sandbox.environments import ProvisionerRegistry from .environments import ProvisionerRegistry
for prov in ProvisionerRegistry.get_all().values(): for prov in ProvisionerRegistry.get_all().values():
await prov.scan_and_setup_workspace(cast(Any, session), workspace_dir) await prov.scan_and_setup_workspace(cast(Any, session), workspace_dir)
@@ -157,10 +146,7 @@ class SandboxManager:
client = client_cls() client = client_cls()
await client.delete(cast(Any, session)) await client.delete(cast(Any, session))
except Exception as e: except Exception as e:
logger.error( logger.error(f"销毁沙箱环境失败 (Session: {resource_id}): {e}")
f"销毁沙箱环境失败 (Session: {resource_id}): {e}",
command="SandboxManager",
)
async def close_session(self, session_id: str) -> None: async def close_session(self, session_id: str) -> None:
"""从生存期管理器中注销并销毁指定会话的沙箱环境""" """从生存期管理器中注销并销毁指定会话的沙箱环境"""
+2 -5
View File
@@ -12,11 +12,11 @@ from typing import TYPE_CHECKING, Annotated, Any, Literal
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_sandbox as logger
from zhenxun.utils.pydantic_compat import model_dump from zhenxun.utils.pydantic_compat import model_dump
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.sandbox.drivers.base import BaseSandboxSession from .drivers.base import BaseSandboxSession
class LanguageProfile(BaseModel): class LanguageProfile(BaseModel):
@@ -337,7 +337,6 @@ class SandboxBlueprint(BaseModel):
def with_local_dir(self, path: str, local_dir: str) -> "SandboxBlueprint": def with_local_dir(self, path: str, local_dir: str) -> "SandboxBlueprint":
"""声明预置本地宿主机完整目录""" """声明预置本地宿主机完整目录"""
from zhenxun.services.log import logger
for bm in self.bind_mounts: for bm in self.bind_mounts:
if path.startswith(bm.sandbox_path) or bm.sandbox_path.startswith(path): if path.startswith(bm.sandbox_path) or bm.sandbox_path.startswith(path):
@@ -370,8 +369,6 @@ class SandboxBlueprint(BaseModel):
"""声明宿主机物理目录双向挂载 (Bind Mount)""" """声明宿主机物理目录双向挂载 (Bind Mount)"""
from pathlib import Path from pathlib import Path
from zhenxun.services.log import logger
if remote_path in self.entries: if remote_path in self.entries:
logger.warning( logger.warning(
f"⚠️ [Sandbox] 目标沙箱路径 '{remote_path}' " f"⚠️ [Sandbox] 目标沙箱路径 '{remote_path}' "
+1 -1
View File
@@ -5,7 +5,7 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Protocol, runtime_checkable from typing import Any, Protocol, runtime_checkable
from zhenxun.services.ai.sandbox.models import SandboxExecutionResult from .models import SandboxExecutionResult
class InteractiveTerminalSession(Protocol): class InteractiveTerminalSession(Protocol):
+3 -3
View File
@@ -1,10 +1,10 @@
from typing import TYPE_CHECKING, ClassVar from typing import TYPE_CHECKING, ClassVar
from zhenxun.services.log import logger from zhenxun.services.ai.utils.logger import log_sandbox as logger
if TYPE_CHECKING: if TYPE_CHECKING:
from zhenxun.services.ai.sandbox.addons.base import BaseSandboxExtension from .addons.base import BaseSandboxExtension
from zhenxun.services.ai.sandbox.drivers.base import BaseSandboxClient from .drivers.base import BaseSandboxClient
class SandboxRegistry: class SandboxRegistry:
+5 -4
View File
@@ -7,13 +7,14 @@ import uuid
import aiohttp import aiohttp
from zhenxun.services.ai.sandbox.drivers.base import BaseSandboxSession from zhenxun.services.ai.utils.logger import log_sandbox as logger
from zhenxun.services.ai.sandbox.models import ( from zhenxun.utils.utils import infer_plugin_namespace
from .drivers.base import BaseSandboxSession
from .models import (
LanguageProfile, LanguageProfile,
SandboxExecutionResult, SandboxExecutionResult,
) )
from zhenxun.services.log import logger
from zhenxun.utils.utils import infer_plugin_namespace
def parse_shebang(script_path: str | Path) -> str | None: def parse_shebang(script_path: str | Path) -> str | None:

Some files were not shown because too many files have changed in this diff Show More