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