♻️ refactor(core): 重构 AI 能力与定时任务调度系统 (#2148)

* ♻️ refactor(core): 重构 AI 能力与定时任务调度系统

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

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

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

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

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

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rumio
2026-07-10 09:14:06 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent 0b32d69c9c
commit 922d092650
132 changed files with 2372 additions and 1972 deletions
@@ -6,7 +6,14 @@ from zhenxun.models.level_user import LevelUser
from zhenxun.models.scheduled_job import ScheduledJob
from zhenxun.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",
]
+6 -40
View File
@@ -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)
+14 -6
View File
@@ -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)。
统一处理结构化解析失败和语义护栏拦截。"""
+206
View File
@@ -0,0 +1,206 @@
from collections.abc import Callable
from dataclasses import dataclass, field
import fnmatch
from typing import Any, cast
from typing_extensions import Self
from pydantic import BaseModel, Field
from zhenxun.services.ai.utils.logger import log_capability as logger
from zhenxun.services.ai.utils.utils import parse_routing_string
from zhenxun.utils.utils import infer_plugin_namespace
from .base import AbstractCapability
from .wrappers import DynamicCapability
class CapabilityQuery(BaseModel):
"""
拦截器/能力组件的声明式查询对象。
用于在 Agent 中精确或批量筛选加载特定命名空间、特定标签的能力。
"""
name: str | list[str] | None = Field(default=None)
"""如果提供,则能力的名称必须等于该字符串或在列表中。支持 * / ? 通配符。"""
tags: list[str] | None = Field(default=None)
"""如果提供,则能力必须包含这里列出的所有标签 (交集/AND匹配)。"""
exclude_tags: list[str] | None = Field(default=None)
"""如果提供,则能力不能包含这里列出的任何标签 (排斥过滤)。"""
namespace: str | None = Field(default=None)
"""限制搜索的插件命名空间。如果不指定,将自动推导为调用者所在的插件;
'global' 将跨全插件搜索,'*' 代表所有插件。"""
CapabilitySource = (
str | Callable | AbstractCapability | type[AbstractCapability] | CapabilityQuery
)
"""能力/拦截器来源
支持字符串别名/标签、普通函数、Capability类或实例,以及声明式 Query 对象。
"""
@dataclass
class CapabilityEntry:
"""能力组件元数据载体"""
cls: type[AbstractCapability]
"""能力类"""
name: str
"""能力名称"""
namespace: str
"""能力所在的命名空间"""
tags: list[str] = field(default_factory=list)
"""能力标签列表"""
auto_apply: bool = False
"""是否自动挂载该能力"""
class CapabilityManager:
"""能力组件全局注册与发现中心 (单例)"""
_instance: "CapabilityManager | None" = None
_entries: list[CapabilityEntry]
def __new__(cls) -> Self:
"""单例模式:获取或创建全局唯一的能力管理器实例"""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._entries = []
return cast(Self, cls._instance)
def register(
self,
cls: type[AbstractCapability],
name: str,
namespace: str,
tags: list[str],
auto_apply: bool,
) -> None:
"""注册一个能力组件到管理器中"""
self._entries.append(
CapabilityEntry(
cls=cls,
name=name,
namespace=namespace,
tags=tags,
auto_apply=auto_apply,
)
)
tag_str = f" | Tags: {tags}" if tags else ""
logger.debug(
f"已注册 Capability: '{name}' -> Namespace: '{namespace}'{tag_str})"
)
def get_auto_apply_capabilities(self, namespace: str) -> list[AbstractCapability]:
"""获取指定命名空间及其它全局命名空间下自动挂载的能力实例"""
instances = []
for entry in self._entries:
if entry.auto_apply and entry.namespace in ("global", namespace):
try:
instances.append(entry.cls())
except Exception as e:
logger.error(f"实例化自动装配能力 {entry.name} 失败: {e}")
return instances
def query_capabilities(
self, query: CapabilityQuery, default_namespace: str
) -> list[AbstractCapability]:
"""根据声明式查询条件筛选并实例化匹配的能力组件"""
matched = []
ns = query.namespace or default_namespace
for entry in self._entries:
if ns != "*" and entry.namespace != ns:
continue
if query.name:
names = [query.name] if isinstance(query.name, str) else query.name
name_matched = False
for pattern in names:
if fnmatch.fnmatch(entry.name, pattern):
name_matched = True
break
if not name_matched:
continue
if query.tags:
if not all(tag in entry.tags for tag in query.tags):
continue
if query.exclude_tags:
if any(tag in entry.tags for tag in query.exclude_tags):
continue
try:
matched.append(entry.cls())
except Exception as e:
logger.error(f"实例化能力 {entry.name} 失败: {e}")
return matched
def resolve_capabilities(
self, sources: list[Any], default_namespace: str
) -> list[AbstractCapability]:
"""解析多种类型的能力来源并实例化为能力组件列表"""
resolved = []
for source in sources:
if isinstance(source, AbstractCapability):
resolved.append(source)
elif callable(source) and not isinstance(source, type):
resolved.append(DynamicCapability(source))
elif isinstance(source, CapabilityQuery):
resolved.extend(self.query_capabilities(source, default_namespace))
elif isinstance(source, str):
s = cast(str, source)
parsed_args = parse_routing_string(s, default_namespace)
q = CapabilityQuery(**parsed_args)
resolved.extend(self.query_capabilities(q, default_namespace))
elif isinstance(source, type) and issubclass(source, AbstractCapability):
try:
resolved.append(source())
except Exception as e:
logger.error(f"实例化能力 {source.__name__} 失败: {e}")
else:
raise TypeError(f"不支持的 Capability 来源: {type(source)}")
return resolved
capability_manager = CapabilityManager()
def capability(
name: str | None = None,
tags: list[str] | None = None,
auto_apply: bool = False,
namespace: str | None = None,
) -> Callable:
"""
类装饰器:声明式地注册一个 Capability 到全局能力池中。
允许第三方插件通过字符串别名或标签进行引用,彻底解耦模块依赖。
参数:
name: 能力的名称,如果为None则默认使用类名。
tags: 能力的标签列表,用于分类或批量筛选。
auto_apply: 是否自动应用挂载该能力。
namespace: 能力的命名空间,如果为None则自动推导为调用者所在的插件。
返回:
Callable: 装饰器函数,用于包装 AbstractCapability 类。
"""
def decorator(cls: type[AbstractCapability]):
"""装饰器内部函数,实现类注册"""
final_name = name or cls.__name__
final_tags = tags or []
ns = (
namespace
if namespace is not None
else infer_plugin_namespace(default="global")
)
capability_manager.register(
cls, name=final_name, namespace=ns, tags=final_tags, auto_apply=auto_apply
)
return cls
return decorator
+1 -11
View File
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
from nonebot.utils import is_coroutine_callable
from 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)
+1
View File
@@ -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",
+2 -4
View File
@@ -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)
+18 -20
View File
@@ -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
+8 -7
View File
@@ -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:
"""
+17 -16
View File
@@ -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)
+10 -11
View File
@@ -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 -5
View File
@@ -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:
+9 -7
View File
@@ -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。")
+3 -3
View File
@@ -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,
+7 -6
View File
@@ -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:
+4 -3
View File
@@ -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):
+4 -3
View File
@@ -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:
+14 -1
View File
@@ -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 = (
+8 -15
View File
@@ -2,7 +2,7 @@
消息与响应域类型定义 - 统一导出门面
"""
from nonebot.compat import PYDANTIC_V2
from zhenxun.utils.pydantic_compat import model_rebuild
from .context_events import (
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",
+1 -1
View File
@@ -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",
]
+8 -2
View File
@@ -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__ = [
-9
View File
@@ -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]:
"""
异步发现此提供者提供的所有工具。
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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:
+10 -22
View File
@@ -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,
)
+20 -11
View File
@@ -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"
+19 -16
View File
@@ -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 全局与运行时的统一策略配置"""
+9 -7
View File
@@ -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)
+1 -1
View File
@@ -9,7 +9,7 @@ from zhenxun.services.ai.core.exceptions import (
from zhenxun.services.ai.core.models import CancellationToken
from zhenxun.services.ai.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
+7 -4
View File
@@ -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 的上下文历史记录(可选)"""
+12 -4
View File
@@ -8,10 +8,11 @@ from nonebot.utils import is_coroutine_callable
from zhenxun.services.ai.core.messages import AgentMessage
from zhenxun.services.ai.core.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(
+11 -12
View File
@@ -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,
+65 -55
View File
@@ -10,19 +10,26 @@ from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent
from zhenxun.services.ai.core.templates import PromptTemplate
from zhenxun.services.ai.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 = []
+2 -4
View File
@@ -6,8 +6,8 @@ from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.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
+39 -40
View File
@@ -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))
+5 -4
View File
@@ -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):
+53 -12
View File
@@ -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)
+8 -7
View File
@@ -5,17 +5,18 @@ from typing import Any, cast
from zhenxun.services.ai.core.messages import PromptInput
from zhenxun.services.ai.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):
+2 -1
View File
@@ -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",
+3 -4
View File
@@ -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
+3 -17
View File
@@ -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
+2 -1
View File
@@ -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())
+4 -3
View File
@@ -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
)
+5 -4
View File
@@ -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 = []
+4 -3
View File
@@ -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):
+2 -2
View File
@@ -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,
+29 -14
View File
@@ -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):
+2 -2
View File
@@ -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)
+1 -20
View File
@@ -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__}"
)
+7 -10
View File
@@ -16,7 +16,7 @@ from zhenxun.services.ai.llm.manager import (
)
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities
from zhenxun.services.ai.llm.system.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(
+15 -19
View File
@@ -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,
+8 -6
View File
@@ -13,12 +13,13 @@ from zhenxun.services.ai.config import (
from zhenxun.services.ai.core.exceptions import ConfigurationException
from zhenxun.services.ai.core.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()
+28 -7
View File
@@ -8,11 +8,12 @@ from zhenxun.services.ai.core.models import ModelDetail, ModelModality
from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.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(
+14 -1
View File
@@ -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),
+1 -1
View File
@@ -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
+13 -52
View File
@@ -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)
-3
View File
@@ -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",
]
-40
View File
@@ -1,40 +0,0 @@
from __future__ import annotations
from collections import defaultdict
from zhenxun.services.ai.capabilities import (
AbstractCapability,
)
from zhenxun.services.log import logger
from zhenxun.utils.utils import infer_plugin_namespace
GLOBAL_CAPABILITIES: dict[str, list[AbstractCapability]] = defaultdict(list)
from zhenxun.services.ai.capabilities.builtin import (
BillingCapability,
GlobalCycleLimitCapability,
PermissionCapability,
ReflexionCapability,
StuckDetectionCapability,
ToolRetryAndReflectionCapability,
)
for _cap in [
GlobalCycleLimitCapability(),
StuckDetectionCapability(),
PermissionCapability(),
BillingCapability(),
ToolRetryAndReflectionCapability(),
ReflexionCapability(),
]:
GLOBAL_CAPABILITIES["global"].append(_cap)
def register_global_capability(
capability: AbstractCapability, scope: str | None = None
) -> None:
ns = scope if scope is not None else infer_plugin_namespace()
GLOBAL_CAPABILITIES[ns].append(capability)
logger.debug(
f"已注册全局 Capability: {capability.__class__.__name__} -> Namespace: {ns}"
)
+78 -36
View File
@@ -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",
+18 -1
View File
@@ -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()
+8 -6
View File
@@ -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)
+16 -28
View File
@@ -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",
]
+1 -1
View File
@@ -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(
+27 -6
View File
@@ -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 开启时发送给平台"""
+34 -33
View File
@@ -1,17 +1,20 @@
from typing import TYPE_CHECKING, Any
from __future__ import annotations
from typing import Any
from nonebot_plugin_alconna import UniMessage
from 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
)
+1 -1
View File
@@ -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 服务"""
+7 -21
View File
@@ -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",
+3 -3
View File
@@ -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):
+8 -22
View File
@@ -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:
"""从生存期管理器中注销并销毁指定会话的沙箱环境"""
+2 -5
View File
@@ -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}' "
+1 -1
View File
@@ -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):
+3 -3
View File
@@ -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:
+5 -4
View File
@@ -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