mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-18 18:50:05 +08:00
✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 (#2146)
* ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 - 【重构】将原 services/llm 重构并迁移至全新的 services/ai 架构,提供向下兼容垫片 - 【新增】引入 Agent、Team、Workflow 三大智能体与工作流编排范式 - 【新增】引入基于 RAG 的长期向量记忆与中期槽位记忆系统 - 【新增】引入基于 Docker 的安全代码执行沙箱环境 - 【新增】支持 MCP 协议,允许动态管理和调用 MCP 服务 - 【新增】引入输入输出安全合规护栏与自愈反思机制 - 【优化】重构并优化多厂商 API 适配器 (Gemini, OpenAI, DeepSeek, GLM 等) - 【优化】优化日志脱敏与 Token 预估机制 - 【移除】移除旧版 llm default 和 llm reset-key 命令,新增 llm mcp 管理命令 * 🔧 chore(deps): 更新项目依赖与配置 - 添加 mcp、jieba 和 aiodocker 依赖到配置文件及 requirements.txt - 在 pyright 配置中设置 reportMissingImports 为 none - 调整 .gitignore 中 resources 目录的忽略规则 * ♻️ refactor(tools): 重构工具终止机制并清理知识库日志输出 - 统一使用 `context.state["__end_run__"]` 替代 `EndRunResult` 控制任务结束 - 移除文件系统和向量知识库检索工具中 `ToolResult` 的 `.with_log` 调用 - 调整指令处理器(Directive)的返回值为 `tool_res.output` - 修复部分类型检查警告并优化联合类型判断语法 * ♻️ refactor(tools): 重构工具副作用指令与控制流熔断机制 - 引入 `DirectivePayload` 及 `ToolResult` 的子类以结构化表达工具副作用 - 移除通过 `context.state` 传递魔术变量的隐式控制流设计 - 重构 `DirectiveManager` 处理器接口,直接在处理器中修改 `AgentState` 并构建 `AgentRunResult` - 在 `StandardAgentExecutor` 中统一通过 `directive_manager` 调度工具返回的副作用指令 - 补全 `MessageBuilder` 中部分核心方法的文档注释 * 🐛 fix(sandbox): 修复 Docker 沙箱容器状态检测与会话清理逻辑 -【修复】修正 `is_alive` 中直接读取私有属性的问题,改用 `show()` 返回值 -【修复】解决 `execute_code` 中缓存的执行器与当前会话不一致的问题 -【优化】在清理工作区前增加容器存活检测,避免向已死容器发送请求 -【优化】创建容器时增加运行状态校验,若已停止则自动从缓存中移除并重建 -【优化】优化容器销毁和清理逻辑,静默处理容器不存在 (404) 的异常 * 📝 docs(core): 补充核心模块初始化方法的文档注释 * 🚨 auto fix by pre-commit hooks --------- Co-authored-by: webjoin111 <455457521@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bdc1374848
commit
80fc5b86a7
@@ -0,0 +1,38 @@
|
||||
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,
|
||||
get_current_run_context,
|
||||
)
|
||||
from .di import Hidden, Inject
|
||||
from .hitl import HITLController
|
||||
from .hooks import Hooks
|
||||
from .models import (
|
||||
AgentRunResult,
|
||||
StreamedRunResult,
|
||||
Task,
|
||||
)
|
||||
from .session import session_manager
|
||||
from .ui import UIController
|
||||
|
||||
__all__ = [
|
||||
"GLOBAL_CAPABILITIES",
|
||||
"AgentRunResult",
|
||||
"BlackboardManager",
|
||||
"CancellationToken",
|
||||
"HITLController",
|
||||
"Hidden",
|
||||
"Hooks",
|
||||
"Inject",
|
||||
"NoneBotDeps",
|
||||
"RunContext",
|
||||
"StreamedRunResult",
|
||||
"Task",
|
||||
"UIController",
|
||||
"get_current_run_context",
|
||||
"register_global_capability",
|
||||
"session_manager",
|
||||
]
|
||||
@@ -0,0 +1,75 @@
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from zhenxun.utils.pydantic_compat import (
|
||||
model_dump,
|
||||
model_json_schema,
|
||||
model_validate,
|
||||
)
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class BlackboardManager(Generic[T]):
|
||||
"""
|
||||
结构化黑板管理器,提供带锁的并发读写和 Schema 校验。
|
||||
解决多智能体 (Team) 并发状态竞争及无结构脏写的问题。
|
||||
"""
|
||||
|
||||
def __init__(self, schema: type[T], initial_state: T | None = None):
|
||||
self.schema = schema
|
||||
if initial_state is not None:
|
||||
self._state = initial_state
|
||||
else:
|
||||
try:
|
||||
self._state = schema()
|
||||
except ValidationError as e:
|
||||
raise ValueError(
|
||||
"初始化黑板状态失败,因为 Schema 存在无默认值的必填字段,"
|
||||
f"请显式提供 initial_state: {e}"
|
||||
)
|
||||
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def read(self) -> str:
|
||||
"""安全读取当前状态的 JSON 字符串表示"""
|
||||
async with self._lock:
|
||||
state_dict = model_dump(self._state)
|
||||
return json.dumps(state_dict, ensure_ascii=False)
|
||||
|
||||
async def update(self, **kwargs: Any) -> str:
|
||||
"""部分更新状态"""
|
||||
async with self._lock:
|
||||
current_dict = model_dump(self._state)
|
||||
|
||||
def deep_update(d: dict, u: dict) -> dict:
|
||||
for k, v in u.items():
|
||||
if isinstance(v, dict) and k in d and isinstance(d[k], dict):
|
||||
deep_update(d[k], v)
|
||||
elif isinstance(v, list) and k in d and isinstance(d[k], list):
|
||||
d[k].extend(x for x in v if x not in d[k])
|
||||
else:
|
||||
d[k] = v
|
||||
return d
|
||||
|
||||
merged_dict = deep_update(current_dict, kwargs)
|
||||
|
||||
try:
|
||||
new_state = model_validate(self.schema, merged_dict)
|
||||
self._state = new_state
|
||||
return "全局黑板状态更新成功"
|
||||
except ValidationError as e:
|
||||
error_msgs = []
|
||||
for err in e.errors():
|
||||
loc = ".".join(str(x) for x in err["loc"])
|
||||
msg = err.get("msg", "")
|
||||
error_msgs.append(f"字段 `{loc}`: {msg}")
|
||||
err_msg = "\n".join(error_msgs)
|
||||
raise ValueError(f"黑板状态更新失败,非法的数据格式:\n{err_msg}")
|
||||
|
||||
def get_schema(self) -> dict[str, Any]:
|
||||
"""获取结构化 Schema"""
|
||||
return model_json_schema(self.schema)
|
||||
@@ -0,0 +1,40 @@
|
||||
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}"
|
||||
)
|
||||
@@ -0,0 +1,410 @@
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
import dataclasses
|
||||
import inspect
|
||||
from typing import Any, Generic, cast, get_origin
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from zhenxun.services.ai.core.messages import AgentEvent, AgentMessage
|
||||
from zhenxun.services.ai.utils import ContextUtils
|
||||
from zhenxun.utils.utils import infer_plugin_namespace
|
||||
|
||||
AgentDepsT = TypeVar("AgentDepsT", default=Any)
|
||||
"""泛型类型变量:外部环境依赖对象 (Agent Dependencies)。"""
|
||||
ProviderFunc = Callable[["RunContext"], Any | Awaitable[Any]]
|
||||
"""函数签名类型别名:依赖提供者函数 (Dependency Provider)。"""
|
||||
ToolsPrepareFunc = Callable[["RunContext[AgentDepsT]", list[Any]], Any | Awaitable[Any]]
|
||||
"""全局/Agent 级动态工具干预函数类型"""
|
||||
|
||||
|
||||
class NoneBotDeps(BaseModel):
|
||||
"""NoneBot 环境下的标准依赖容器。"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
bot: Bot | None = Field(default=None)
|
||||
"""当前触发事件的 Bot 实例"""
|
||||
|
||||
event: Event | None = Field(default=None)
|
||||
"""当前触发的事件实例"""
|
||||
|
||||
matcher: Matcher | None = Field(default=None)
|
||||
"""当前处理该事件的 Matcher 实例"""
|
||||
|
||||
@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)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SessionContext(Generic[AgentDepsT]):
|
||||
"""
|
||||
生命周期:Session(会话)层。
|
||||
跨越多次对话轮次,负责保存长线状态与物理隔离信息。
|
||||
"""
|
||||
|
||||
session_id: str
|
||||
"""核心会话标识符,用于区分不同用户或群组的上下文隔离。"""
|
||||
deps: 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
|
||||
"""结构化黑板管理器,作为共享状态的高级替代方案,提供并发锁和强类型校验。"""
|
||||
namespace: str = "global"
|
||||
"""触发事件的插件命名空间"""
|
||||
append_only_manager: Any = dataclasses.field(default=None)
|
||||
"""用于大模型前缀缓存命中优化的追加写入管理器。"""
|
||||
|
||||
@property
|
||||
def memory(self) -> Any:
|
||||
"""
|
||||
获取当前会话的持久化记忆访问门面 (AgentSessionFacade)。
|
||||
提供极简的 history 和 slots 操作 API。
|
||||
"""
|
||||
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)
|
||||
platform = ContextUtils.extract_platform(self.deps)
|
||||
|
||||
meta = SessionMetadata(
|
||||
session_id=self.session_id,
|
||||
selector=ScopeSelector(
|
||||
user_id=user_id,
|
||||
group_id=group_id,
|
||||
platform=platform,
|
||||
namespace=self.namespace,
|
||||
),
|
||||
)
|
||||
return AgentSessionFacade(memory_manager, meta)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AgentRunContext(Generic[AgentDepsT]):
|
||||
"""
|
||||
生命周期:Run(运行)层。
|
||||
伴随 Agent 的单次执行 (run_stream),保存大模型推理时的状态与原生消息历史。
|
||||
"""
|
||||
|
||||
session: SessionContext[AgentDepsT]
|
||||
"""指向所属 Session 层上下文的引用。"""
|
||||
state: dict[str, Any] = dataclasses.field(default_factory=dict)
|
||||
"""状态字典:用于在当前 Agent 执行轮次、工具和中间件中透传动态变量。"""
|
||||
agent_name: str | None = None
|
||||
"""当前正在执行的 Agent 名称。"""
|
||||
current_model: str | None = None
|
||||
"""当前实际调用的底层大模型名称 (Provider/Model)。"""
|
||||
user_input: str | None = None
|
||||
"""当前轮次用户的原始文本输入。"""
|
||||
messages: list[AgentMessage] = dataclasses.field(default_factory=list)
|
||||
"""大模型原生上下文 (LLMMessage 列表),与执行器中的执行历史保持内存引用同步。"""
|
||||
hitl_locks: dict[str, asyncio.Lock] = dataclasses.field(default_factory=dict)
|
||||
"""人机交互 (HITL) 并发锁,防止同群组内并发审批冲突。"""
|
||||
delegate_depth: int = 0
|
||||
"""子智能体委派深度标记,用于防范无限递归嵌套。"""
|
||||
tool_retries: dict[str, int] = dataclasses.field(default_factory=dict)
|
||||
"""记录当前轮次内各个工具的累积失败重试次数,用于系统熔断。"""
|
||||
cancellation_token: Any | None = None
|
||||
"""全局级联取消令牌,用于跨 Agent 的协程挂起中断。"""
|
||||
event_bus: Any | None = None
|
||||
"""底层的事件流发射器 (EventBus),由执行引擎在运行时挂载。"""
|
||||
|
||||
dynamic_prompts: dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
"""动态提示词字典(保持插入顺序并去重)。
|
||||
仅在 HTTP 请求前 JIT 渲染,不会污染持久化的上下文对话历史。"""
|
||||
|
||||
def add_system_prompt(self, prompt: str, key: str | None = None) -> None:
|
||||
"""动态追加临时系统提示词到大模型上下文中(实时生效且不污染历史)。"""
|
||||
dict_key = key or prompt
|
||||
if prompt:
|
||||
self.dynamic_prompts[dict_key] = prompt
|
||||
|
||||
def add_event(self, event: AgentEvent) -> None:
|
||||
"""向当前运行上下文中安全追加业务事件"""
|
||||
self.messages.append(event)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ToolCallContext(Generic[AgentDepsT]):
|
||||
"""
|
||||
生命周期:Call(工具调用)层。
|
||||
单次工具调用分配的绝对私有状态,彻底消灭并发调用时的属性污染。
|
||||
"""
|
||||
|
||||
run: AgentRunContext[AgentDepsT]
|
||||
"""指向所属 Run 层上下文的引用。"""
|
||||
tool_call_id: str = "unknown"
|
||||
"""大模型为本次工具调用分配的唯一 ID。"""
|
||||
tool_name: str = "unknown"
|
||||
"""本次调用的工具名称。"""
|
||||
retry_count: int = 0
|
||||
"""当前工具调用的重试序号 (第几次重试)。"""
|
||||
current_tool: Any | None = None
|
||||
"""当前工具的可执行实例 (ToolExecutable)。"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RunContext(Generic[AgentDepsT]):
|
||||
"""
|
||||
依赖注入容器(DI Container),保留原有上下文信息的同时提升获取类型的能力。
|
||||
"""
|
||||
|
||||
session_id: str | None = None
|
||||
"""当前运行所在的会话ID,用于区分不同用户的独立上下文"""
|
||||
|
||||
di_cache: dict[str, Any] = dataclasses.field(default_factory=dict)
|
||||
"""依赖注入引擎的缓存容器,支持父子层级浅拷贝隔离"""
|
||||
|
||||
state: dict[str, Any] = dataclasses.field(default_factory=dict)
|
||||
"""状态字典:用于在会话轮次、工具和中间件中透传动态变量"""
|
||||
|
||||
shared_state: dict[str, Any] = dataclasses.field(default_factory=dict)
|
||||
"""共享状态字典:全局引用穿透,用于主智能体与嵌套子智能体之间的
|
||||
黑板模式 (Blackboard) 数据通信"""
|
||||
|
||||
upstream_results: dict[str, Any] = dataclasses.field(default_factory=dict)
|
||||
"""前置节点产出字典:标准化的数据流载荷契约,键为 Agent Name,值为输出内容"""
|
||||
|
||||
capabilities: list[Any] = dataclasses.field(default_factory=list)
|
||||
"""当前上下文绑定的拦截器 (Capabilities) 链,用于生命周期拦截"""
|
||||
|
||||
deps: AgentDepsT = dataclasses.field(default=cast(AgentDepsT, None))
|
||||
"""强类型的外部依赖注入对象,用于跨工具共享业务状态或配置"""
|
||||
|
||||
session: SessionContext[AgentDepsT] = dataclasses.field(
|
||||
init=False, repr=False, compare=False
|
||||
)
|
||||
"""会话层上下文:承载跨轮次共享的依赖与共享状态引用。"""
|
||||
run: AgentRunContext[AgentDepsT] = dataclasses.field(
|
||||
init=False, repr=False, compare=False
|
||||
)
|
||||
"""运行层上下文:承载当前 Agent 执行轮次的模型状态与运行时元信息。"""
|
||||
call: ToolCallContext[AgentDepsT] = dataclasses.field(
|
||||
init=False, repr=False, compare=False
|
||||
)
|
||||
"""调用层上下文:承载单次工具调用的私有状态与执行引用。"""
|
||||
_is_auto_session_id: bool = dataclasses.field(
|
||||
default=False, init=False, repr=False, compare=False
|
||||
)
|
||||
"""标记 session_id 是否为框架隐式生成的。"""
|
||||
|
||||
def get_bot(self) -> Bot | None:
|
||||
"""强类型安全地提取 Bot 实例"""
|
||||
if not self.deps:
|
||||
return None
|
||||
bot = getattr(self.deps, "bot", None)
|
||||
return bot if isinstance(bot, Bot) else None
|
||||
|
||||
def get_event(self) -> Event | None:
|
||||
"""强类型安全地提取 Event 实例"""
|
||||
if not self.deps:
|
||||
return None
|
||||
event = getattr(self.deps, "event", None)
|
||||
return event if isinstance(event, Event) else None
|
||||
|
||||
def get_matcher(self) -> Matcher | None:
|
||||
"""强类型安全地提取 Matcher 实例"""
|
||||
if not self.deps:
|
||||
return None
|
||||
matcher = getattr(self.deps, "matcher", None)
|
||||
return matcher if isinstance(matcher, Matcher) else None
|
||||
|
||||
def get_user_id(self) -> str | None:
|
||||
"""安全提取当前触发任务的用户 ID"""
|
||||
return ContextUtils.extract_user_id(self.deps)
|
||||
|
||||
def get_group_id(self) -> str | None:
|
||||
"""安全提取当前触发任务的群组 ID(私聊则为 None)"""
|
||||
return ContextUtils.extract_group_id(self.deps)
|
||||
|
||||
def get_platform(self) -> str:
|
||||
"""安全提取当前连接的适配器平台标识"""
|
||||
return ContextUtils.extract_platform(self.deps)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.deps is None:
|
||||
self.deps = cast(AgentDepsT, NoneBotDeps.get_current())
|
||||
|
||||
if not self.session_id and self.deps:
|
||||
from zhenxun.services.ai.context.memory.types import (
|
||||
Isolation,
|
||||
)
|
||||
|
||||
bot = self.get_bot()
|
||||
event = self.get_event()
|
||||
|
||||
if bot and event:
|
||||
meta = ContextUtils.generate_session_meta(
|
||||
bot, event, 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:
|
||||
ns = getattr(self.deps, "namespace", None) or (
|
||||
self.deps.get("namespace") if isinstance(self.deps, dict) else None
|
||||
)
|
||||
if not ns:
|
||||
ns = infer_plugin_namespace(default="global")
|
||||
|
||||
self.session = SessionContext(
|
||||
session_id=self.session_id or "default_session",
|
||||
deps=self.deps,
|
||||
shared_state=self.shared_state,
|
||||
namespace=ns,
|
||||
)
|
||||
from zhenxun.services.ai.core.engine.append_only import (
|
||||
AppendOnlyContextManager,
|
||||
)
|
||||
|
||||
self.session.append_only_manager = AppendOnlyContextManager()
|
||||
|
||||
self.run = AgentRunContext(session=self.session, state=self.state)
|
||||
self.call = ToolCallContext(run=self.run)
|
||||
|
||||
def clone_for_execution(self, **kwargs) -> "RunContext":
|
||||
new_state = self.state.copy()
|
||||
changes = {k: v for k, v in kwargs.items() if hasattr(self, k)}
|
||||
if "state" not in changes:
|
||||
changes["state"] = new_state
|
||||
if "shared_state" not in changes:
|
||||
changes["shared_state"] = self.shared_state
|
||||
|
||||
new_ctx = dataclasses.replace(cast(Any, self), **changes)
|
||||
new_ctx.upstream_results = self.upstream_results.copy()
|
||||
new_ctx.di_cache = self.di_cache.copy()
|
||||
|
||||
new_ctx.session = self.session
|
||||
new_ctx.run = AgentRunContext(
|
||||
session=new_ctx.session,
|
||||
state=new_ctx.state,
|
||||
agent_name=self.run.agent_name,
|
||||
current_model=self.run.current_model,
|
||||
user_input=self.run.user_input,
|
||||
messages=list(self.run.messages),
|
||||
hitl_locks=self.run.hitl_locks,
|
||||
delegate_depth=self.run.delegate_depth,
|
||||
tool_retries=self.run.tool_retries,
|
||||
cancellation_token=self.run.cancellation_token,
|
||||
event_bus=self.run.event_bus,
|
||||
dynamic_prompts=self.run.dynamic_prompts.copy(),
|
||||
)
|
||||
new_ctx.call = ToolCallContext(run=new_ctx.run)
|
||||
return new_ctx
|
||||
|
||||
def clone_for_tool_call(self, tool_call_id: str, tool_name: str) -> "RunContext":
|
||||
"""
|
||||
为每个并发的工具调用派生绝对独立的 ToolCallContext。消除多工具并行时的属性污染。
|
||||
"""
|
||||
new_ctx = dataclasses.replace(cast(Any, self))
|
||||
new_ctx.di_cache = self.di_cache.copy()
|
||||
new_ctx.session = self.session
|
||||
new_ctx.run = self.run
|
||||
new_ctx.call = ToolCallContext(
|
||||
run=new_ctx.run,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
retry_count=new_ctx.run.tool_retries.get(tool_name, 0),
|
||||
)
|
||||
return new_ctx
|
||||
|
||||
def clone_for_member(self, member_name: str = "unknown") -> "RunContext":
|
||||
"""
|
||||
为团队子成员克隆上下文。
|
||||
强制清空消息历史并分配独立 SessionID,实现绝对的记忆沙箱物理隔离。
|
||||
"""
|
||||
new_ctx = self.clone_for_execution()
|
||||
new_ctx.run.delegate_depth = 0
|
||||
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
|
||||
return new_ctx
|
||||
|
||||
|
||||
_CURRENT_RUN_CONTEXT: ContextVar[RunContext | None] = ContextVar(
|
||||
"current_run_context", default=None
|
||||
)
|
||||
|
||||
|
||||
def get_current_run_context() -> RunContext | None:
|
||||
"""
|
||||
[全局逃生舱] 获取当前运行中的上下文对象。
|
||||
适用于深层嵌套业务逻辑,无需层层透传 context 参数。
|
||||
"""
|
||||
return _CURRENT_RUN_CONTEXT.get()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def set_run_context(ctx: RunContext):
|
||||
"""[内部 API] 挂载当前上下文至全局"""
|
||||
token = _CURRENT_RUN_CONTEXT.set(ctx)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_CURRENT_RUN_CONTEXT.reset(token)
|
||||
|
||||
|
||||
def _is_run_context_type(annotation: Any) -> bool:
|
||||
if annotation is RunContext:
|
||||
return True
|
||||
origin = get_origin(annotation)
|
||||
if origin is RunContext:
|
||||
return True
|
||||
if inspect.isclass(origin) and issubclass(origin, RunContext):
|
||||
return True
|
||||
if inspect.isclass(annotation) and issubclass(annotation, RunContext):
|
||||
return True
|
||||
if "RunContext" in str(annotation):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentDepsT",
|
||||
"AgentRunContext",
|
||||
"NoneBotDeps",
|
||||
"RunContext",
|
||||
"SessionContext",
|
||||
"ToolCallContext",
|
||||
"ToolsPrepareFunc",
|
||||
"get_current_run_context",
|
||||
"set_run_context",
|
||||
]
|
||||
@@ -0,0 +1,387 @@
|
||||
from collections import defaultdict
|
||||
from collections.abc import Awaitable, Callable
|
||||
import inspect
|
||||
from typing import Annotated, Any, ClassVar, cast
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.utils import is_coroutine_callable
|
||||
from nonebot_plugin_session import EventSession, extract_session
|
||||
|
||||
from zhenxun.services.ai.context.memory.facades import AgentSessionFacade
|
||||
from zhenxun.services.ai.run.blackboard import BlackboardManager
|
||||
from zhenxun.utils.utils import infer_plugin_namespace
|
||||
|
||||
from .context import ProviderFunc, RunContext, _is_run_context_type
|
||||
from .hitl import HITLController
|
||||
from .ui import UIController
|
||||
|
||||
|
||||
class Hidden:
|
||||
"""
|
||||
标记一个参数为隐藏参数。
|
||||
在生成大模型工具 Schema 时,带有该标记的 Annotated 参数将被剔除。
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class _InjectMarker:
|
||||
"""内部魔术标记,用于识别 DI 类型糖"""
|
||||
|
||||
def __init__(self, key: str):
|
||||
self.key = key
|
||||
|
||||
|
||||
class _UpstreamResultMarker:
|
||||
"""内部魔术标记,用于精准捕获上游特定节点的产出"""
|
||||
|
||||
def __init__(self, step_name: str):
|
||||
self.step_name = step_name
|
||||
|
||||
|
||||
CurrentUserId = Annotated[str | None, Hidden(), _InjectMarker("user_id")]
|
||||
CurrentGroupId = Annotated[str | None, Hidden(), _InjectMarker("group_id")]
|
||||
CurrentPlatform = Annotated[str, Hidden(), _InjectMarker("platform")]
|
||||
CurrentBot = Annotated[Bot | None, Hidden(), _InjectMarker("bot")]
|
||||
CurrentEvent = Annotated[Event | None, Hidden(), _InjectMarker("event")]
|
||||
CurrentMatcher = Annotated[Matcher | None, Hidden(), _InjectMarker("matcher")]
|
||||
CurrentSession = Annotated[EventSession | None, Hidden(), _InjectMarker("session")]
|
||||
CurrentUI = Annotated[UIController, Hidden(), _InjectMarker("ui")]
|
||||
CurrentHITL = Annotated[HITLController, Hidden(), _InjectMarker("hitl")]
|
||||
CurrentModelName = Annotated[str | None, Hidden(), _InjectMarker("model_name")]
|
||||
CurrentToolRetries = Annotated[int, Hidden(), _InjectMarker("tool_retries")]
|
||||
CurrentState = Annotated[dict[str, Any], Hidden(), _InjectMarker("state")]
|
||||
CurrentSharedState = Annotated[dict[str, Any], Hidden(), _InjectMarker("shared_state")]
|
||||
CurrentOriginalInput = Annotated[str, Hidden(), _InjectMarker("original_input")]
|
||||
UpstreamResults = Annotated[dict[str, Any], Hidden(), _InjectMarker("upstream_results")]
|
||||
CurrentEventPayload = Annotated[Any, Hidden(), _InjectMarker("stream_event")]
|
||||
CurrentBlackboard = Annotated[
|
||||
BlackboardManager | None, Hidden(), _InjectMarker("blackboard")
|
||||
]
|
||||
CurrentMemory = Annotated[AgentSessionFacade, Hidden(), _InjectMarker("memory")]
|
||||
CurrentSandbox = Annotated[Any, Hidden(), _InjectMarker("sandbox")]
|
||||
|
||||
|
||||
class Inject:
|
||||
"""
|
||||
[命名空间] 大模型工具依赖注入类型糖。
|
||||
用于在工具参数中快捷获取群聊/用户的上下文,或注册自定义的依赖提供者。
|
||||
"""
|
||||
|
||||
_providers: ClassVar[dict[str, dict[str, ProviderFunc]]] = defaultdict(dict)
|
||||
|
||||
@classmethod
|
||||
def register_provider(
|
||||
cls, key: str, provider: ProviderFunc, scope: str | None = None
|
||||
) -> None:
|
||||
"""
|
||||
(底层) 注册一个自定义的依赖提供者。
|
||||
"""
|
||||
ns = scope if scope is not None else infer_plugin_namespace()
|
||||
cls._providers[key][ns] = provider
|
||||
|
||||
@classmethod
|
||||
def provider(cls, key: str, scope: str | None = None):
|
||||
def decorator(func: ProviderFunc) -> ProviderFunc:
|
||||
cls.register_provider(key, func, scope)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
@classmethod
|
||||
def bind(cls, key: str, type_hint: Any = Any) -> Any:
|
||||
"""
|
||||
动态生成一个用于类型注解的 Inject 标记。
|
||||
示例:DbSession = Inject.bind("db_session", AsyncSession)
|
||||
"""
|
||||
return Annotated[type_hint, Hidden(), _InjectMarker(key)]
|
||||
|
||||
@classmethod
|
||||
def UpstreamResult(cls, step_name: str) -> Any:
|
||||
"""
|
||||
自动注入:精准获取工作流中指定前置节点的产出。可作为参数的默认值使用以通过静态检查。
|
||||
示例: def my_step(data: str = Inject.UpstreamResult("AgentA")):
|
||||
"""
|
||||
return _UpstreamResultMarker(step_name)
|
||||
|
||||
UserId = CurrentUserId
|
||||
"""自动注入:触发当前任务的用户 ID"""
|
||||
|
||||
GroupId = CurrentGroupId
|
||||
"""自动注入:触发当前任务的群聊 ID(私聊时为 None)"""
|
||||
|
||||
Platform = CurrentPlatform
|
||||
"""自动注入:当前对接的平台名称(如 qq)"""
|
||||
|
||||
Bot = CurrentBot
|
||||
"""自动注入:当前的 NoneBot Bot 实例"""
|
||||
|
||||
Event = CurrentEvent
|
||||
"""自动注入:当前的 NoneBot Event 实例"""
|
||||
|
||||
Matcher = CurrentMatcher
|
||||
"""自动注入:当前的 NoneBot Matcher 实例"""
|
||||
|
||||
Session = CurrentSession
|
||||
"""自动注入:当前的 nonebot_plugin_session 会话实例"""
|
||||
|
||||
UI = CurrentUI
|
||||
"""自动注入:前端 UI 控制器实例"""
|
||||
|
||||
HITL = CurrentHITL
|
||||
"""自动注入:人机协同控制器实例 (提供底层 waiter 交互封装)"""
|
||||
|
||||
ModelName = CurrentModelName
|
||||
"""自动注入:当前大模型正在执行的底层模型名称"""
|
||||
|
||||
ToolRetries = CurrentToolRetries
|
||||
"""自动注入:当前工具正在经历的重试次数 (Int)"""
|
||||
|
||||
State = CurrentState
|
||||
"""自动注入:当前 Agent 轮次隔离的业务状态字典"""
|
||||
|
||||
OriginalInput = CurrentOriginalInput
|
||||
"""自动注入:当前工作流最初始的用户输入文本"""
|
||||
|
||||
SharedState = CurrentSharedState
|
||||
"""自动注入:当前 Team 或全局穿透的共享黑板状态字典"""
|
||||
|
||||
UpstreamResults = UpstreamResults
|
||||
"""自动注入:当前工作流中所有上游节点的产出字典 (Key: Agent Name)"""
|
||||
|
||||
EventPayload = CurrentEventPayload
|
||||
"""自动注入:当前生命周期钩子流转中的 EventBus 载荷实体"""
|
||||
|
||||
Blackboard = CurrentBlackboard
|
||||
"""自动注入:当前工作流/团队挂载的强类型黑板 (BlackboardManager) 实例"""
|
||||
|
||||
Memory = CurrentMemory
|
||||
"""自动注入:当前会话的持久化记忆存取门面 (AgentSessionFacade) 实例"""
|
||||
|
||||
Sandbox = CurrentSandbox
|
||||
"""自动注入:当前沙箱环境管理器实例"""
|
||||
|
||||
|
||||
class BaseParamResolver:
|
||||
"""可插拔参数解析器基类协议"""
|
||||
|
||||
def match(
|
||||
self,
|
||||
name: str,
|
||||
param: inspect.Parameter,
|
||||
context: RunContext,
|
||||
) -> bool:
|
||||
return False
|
||||
|
||||
def static_match(self, param: inspect.Parameter) -> bool:
|
||||
return False
|
||||
|
||||
async def resolve(
|
||||
self,
|
||||
name: str,
|
||||
param: inspect.Parameter,
|
||||
context: RunContext,
|
||||
) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class RunContextResolver(BaseParamResolver):
|
||||
def match(self, name, param, context) -> bool:
|
||||
return _is_run_context_type(param.annotation)
|
||||
|
||||
def static_match(self, param: inspect.Parameter) -> bool:
|
||||
return _is_run_context_type(param.annotation)
|
||||
|
||||
async def resolve(self, name, param, context) -> Any:
|
||||
return context
|
||||
|
||||
|
||||
class TypeSugarResolver(BaseParamResolver):
|
||||
def _get_marker(self, param: inspect.Parameter) -> Any | None:
|
||||
anno = param.annotation
|
||||
if hasattr(anno, "__metadata__"):
|
||||
for arg in anno.__metadata__:
|
||||
if isinstance(arg, _InjectMarker | _UpstreamResultMarker):
|
||||
return arg
|
||||
default_val = param.default
|
||||
if isinstance(default_val, _InjectMarker | _UpstreamResultMarker):
|
||||
return default_val
|
||||
if hasattr(default_val, "__metadata__"):
|
||||
for arg in default_val.__metadata__:
|
||||
if isinstance(arg, _InjectMarker | _UpstreamResultMarker):
|
||||
return arg
|
||||
return None
|
||||
|
||||
def match(self, name, param, context) -> bool:
|
||||
return self._get_marker(param) is not None
|
||||
|
||||
def static_match(self, param: inspect.Parameter) -> bool:
|
||||
return self._get_marker(param) is not None
|
||||
|
||||
async def resolve(self, name, param, context) -> Any:
|
||||
marker = self._get_marker(param)
|
||||
if not marker:
|
||||
raise ValueError(f"参数 {name} 缺失 Inject 标记")
|
||||
if isinstance(marker, _UpstreamResultMarker):
|
||||
return context.upstream_results.get(marker.step_name)
|
||||
|
||||
marker_key = marker.key
|
||||
|
||||
if marker_key in context.di_cache:
|
||||
return context.di_cache[marker_key]
|
||||
|
||||
provider_dict = Inject._providers.get(marker_key, {})
|
||||
ns = getattr(context.session, "namespace", "global")
|
||||
provider = provider_dict.get(ns) or provider_dict.get("global")
|
||||
|
||||
if not provider:
|
||||
raise ValueError(f"未知的类型糖标记或未注册的 Provider: {marker_key}")
|
||||
|
||||
if is_coroutine_callable(provider):
|
||||
result = await provider(context)
|
||||
else:
|
||||
result = provider(context)
|
||||
|
||||
context.di_cache[marker_key] = result
|
||||
return result
|
||||
|
||||
|
||||
class DependencyInjector:
|
||||
"""可插拔依赖注入管线 (Resolver Pipeline)"""
|
||||
|
||||
_resolvers: ClassVar[list[BaseParamResolver]] = []
|
||||
|
||||
@classmethod
|
||||
def register(cls, resolver: BaseParamResolver) -> None:
|
||||
cls._resolvers.append(resolver)
|
||||
|
||||
@classmethod
|
||||
def can_resolve_statically(cls, param: inspect.Parameter) -> bool:
|
||||
for resolver in cls._resolvers:
|
||||
if resolver.static_match(param):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def resolve_all(
|
||||
cls,
|
||||
sig: inspect.Signature,
|
||||
call_kwargs: dict[str, Any],
|
||||
context: RunContext,
|
||||
) -> dict[str, Any]:
|
||||
resolved_kwargs = dict(call_kwargs)
|
||||
for k, v in call_kwargs.items():
|
||||
context.di_cache[k] = v
|
||||
for name, param in sig.parameters.items():
|
||||
if name in ("self", "cls") or name in resolved_kwargs:
|
||||
continue
|
||||
|
||||
if param.kind in (
|
||||
inspect.Parameter.VAR_POSITIONAL,
|
||||
inspect.Parameter.VAR_KEYWORD,
|
||||
):
|
||||
continue
|
||||
|
||||
resolved = False
|
||||
for resolver in cls._resolvers:
|
||||
if resolver.match(name, param, context):
|
||||
val = await resolver.resolve(name, param, context)
|
||||
resolved_kwargs[name] = val
|
||||
resolved = True
|
||||
break
|
||||
|
||||
if not resolved:
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
continue
|
||||
|
||||
raise ValueError(
|
||||
f"参数 '{name}' 未被大模型提供且缺少显式的依赖注入标记"
|
||||
"(如 Inject.* 或 RunContext)。"
|
||||
)
|
||||
|
||||
return resolved_kwargs
|
||||
|
||||
@classmethod
|
||||
async def invoke(
|
||||
cls,
|
||||
func: Callable[..., Any],
|
||||
call_kwargs: dict[str, Any],
|
||||
context: RunContext,
|
||||
) -> Any:
|
||||
"""统一执行带有依赖注入的函数 (支持同步/异步)"""
|
||||
sig = inspect.signature(func)
|
||||
resolved_kwargs = await cls.resolve_all(sig, call_kwargs, context)
|
||||
filtered_kwargs = {
|
||||
k: v for k, v in resolved_kwargs.items() if k in sig.parameters
|
||||
}
|
||||
|
||||
if is_coroutine_callable(func):
|
||||
return await cast(Callable[..., Awaitable[Any]], func)(**filtered_kwargs)
|
||||
return cast(Callable[..., Any], func)(**filtered_kwargs)
|
||||
|
||||
|
||||
DependencyInjector.register(RunContextResolver())
|
||||
DependencyInjector.register(TypeSugarResolver())
|
||||
|
||||
Inject.register_provider("user_id", lambda ctx: ctx.get_user_id(), scope="global")
|
||||
Inject.register_provider("group_id", lambda ctx: ctx.get_group_id(), scope="global")
|
||||
Inject.register_provider("platform", lambda ctx: ctx.get_platform(), scope="global")
|
||||
Inject.register_provider("bot", lambda ctx: ctx.get_bot(), scope="global")
|
||||
Inject.register_provider("event", lambda ctx: ctx.get_event(), scope="global")
|
||||
Inject.register_provider("matcher", lambda ctx: ctx.get_matcher(), scope="global")
|
||||
|
||||
Inject.register_provider("hitl", lambda ctx: HITLController(ctx), scope="global")
|
||||
Inject.register_provider(
|
||||
"model_name", lambda ctx: ctx.run.current_model, scope="global"
|
||||
)
|
||||
|
||||
Inject.register_provider(
|
||||
"tool_retries", lambda ctx: ctx.call.retry_count, scope="global"
|
||||
)
|
||||
|
||||
Inject.register_provider(
|
||||
"original_input", lambda ctx: ctx.run.user_input, scope="global"
|
||||
)
|
||||
Inject.register_provider("state", lambda ctx: ctx.state, scope="global")
|
||||
Inject.register_provider(
|
||||
"upstream_results", lambda ctx: ctx.upstream_results, scope="global"
|
||||
)
|
||||
Inject.register_provider(
|
||||
"shared_state", lambda ctx: ctx.session.shared_state, scope="global"
|
||||
)
|
||||
Inject.register_provider("memory", lambda ctx: ctx.session.memory, scope="global")
|
||||
|
||||
|
||||
def _resolve_blackboard(ctx: RunContext):
|
||||
bb = ctx.session.blackboard
|
||||
if bb is None:
|
||||
raise ValueError(
|
||||
"Inject.Blackboard 注入失败:"
|
||||
"当前会话上下文中未挂载 BlackboardManager 实例。"
|
||||
)
|
||||
return bb
|
||||
|
||||
|
||||
Inject.register_provider("blackboard", _resolve_blackboard, scope="global")
|
||||
|
||||
|
||||
def _resolve_session(ctx):
|
||||
bot = ctx.get_bot()
|
||||
event = ctx.get_event()
|
||||
return extract_session(bot, event) if bot and event else None
|
||||
|
||||
|
||||
Inject.register_provider("session", _resolve_session, scope="global")
|
||||
Inject.register_provider("ui", lambda ctx: UIController(ctx), scope="global")
|
||||
|
||||
from zhenxun.services.ai.sandbox.manager import sandbox_manager
|
||||
|
||||
Inject.register_provider("sandbox", lambda ctx: sandbox_manager, scope="global")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DependencyInjector",
|
||||
"Hidden",
|
||||
"Inject",
|
||||
]
|
||||
@@ -0,0 +1,144 @@
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from zhenxun.services.ai.run.context import RunContext
|
||||
|
||||
CANCEL_WORDS = {"取消", "cancel", "0", "退出", "quit"}
|
||||
CONFIRM_WORDS = {"y", "yes", "是", "1", "ok", "确认"}
|
||||
REJECT_WORDS = {"n", "no", "否", "0", "拒绝"}
|
||||
|
||||
|
||||
class HITLController:
|
||||
"""
|
||||
人机协同控制器 (Human-in-the-Loop Controller)。
|
||||
封装底层的物理环境隔离等待逻辑,供上层工具和中间件发起提问或审批。
|
||||
"""
|
||||
|
||||
def __init__(self, context: "RunContext"):
|
||||
self.context = context
|
||||
|
||||
async def wait_event(
|
||||
self,
|
||||
prompt_msg: str | None = None,
|
||||
timeout: float = 60.0,
|
||||
custom_checker: Callable[[Event], bool] | None = None,
|
||||
strict_user: bool = True,
|
||||
) -> Event | None:
|
||||
"""
|
||||
底层交互方法:进行严格群组与用户隔离的挂起等待。
|
||||
"""
|
||||
bot = self.context.get_bot()
|
||||
event = self.context.get_event()
|
||||
|
||||
if not bot or not event:
|
||||
logger.warning("HITLController: 当前环境无 Bot/Event 实例,无法发起交互。")
|
||||
return None
|
||||
|
||||
if prompt_msg:
|
||||
await bot.send(event, prompt_msg)
|
||||
|
||||
orig_user_id = self.context.get_user_id()
|
||||
orig_group_id = self.context.get_group_id()
|
||||
|
||||
@waiter(waits=["message"], keep_session=False)
|
||||
async def event_waiter(e: Event):
|
||||
raw_curr_group = getattr(e, "group_id", getattr(e, "channel_id", None))
|
||||
|
||||
curr_group_id = str(raw_curr_group) if raw_curr_group else None
|
||||
tgt_group_id = str(orig_group_id) if orig_group_id else None
|
||||
if curr_group_id != tgt_group_id:
|
||||
return None
|
||||
|
||||
if strict_user and str(e.get_user_id()) != orig_user_id:
|
||||
return None
|
||||
|
||||
if custom_checker and not custom_checker(e):
|
||||
return None
|
||||
|
||||
return e
|
||||
|
||||
task = asyncio.create_task(event_waiter.wait(timeout=timeout))
|
||||
cancellation_token = self.context.run.cancellation_token
|
||||
if cancellation_token:
|
||||
cancellation_token.link_future(task)
|
||||
|
||||
return await task
|
||||
|
||||
async def ask_text(self, prompt_msg: str, timeout: float = 60.0) -> str:
|
||||
"""
|
||||
高阶交互:发起文本提问,内置超时与取消词检测。
|
||||
如用户取消或超时,自动抛出异常切断 Agent 思考循环。
|
||||
"""
|
||||
event = await self.wait_event(prompt_msg, timeout=timeout, strict_user=True)
|
||||
if event is None:
|
||||
logger.warning("🛡️ [HITL] 文本参数收集超时,任务已被系统强杀。")
|
||||
raise ToolFatalError("参数收集超时,用户已离开,任务已中止。")
|
||||
|
||||
user_input = event.get_plaintext().strip()
|
||||
if user_input.lower() in CANCEL_WORDS:
|
||||
raise AbortException(reason="用户主动取消了操作")
|
||||
|
||||
return user_input
|
||||
|
||||
async def ask_confirm(self, prompt_msg: str, timeout: float = 60.0) -> bool:
|
||||
"""
|
||||
高阶交互:发起 Y/N 确认审批。
|
||||
支持超管越权代批。用户拒绝或超时自动抛出异常切断循环。
|
||||
"""
|
||||
orig_user_id = self.context.get_user_id()
|
||||
bot = self.context.get_bot()
|
||||
|
||||
if not bot:
|
||||
raise ToolFatalError("非交互环境无法发起人工审批。")
|
||||
|
||||
full_prompt = (
|
||||
f"{prompt_msg}\n\n请在规定时间内回复 [Y/是] 确认,或 [N/否/取消] 拒绝。"
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
is_first_prompt = True
|
||||
|
||||
while True:
|
||||
remaining = timeout - (time.time() - start_time)
|
||||
if remaining <= 0:
|
||||
raise ToolFatalError("审批超时")
|
||||
|
||||
event = await self.wait_event(
|
||||
prompt_msg=full_prompt if is_first_prompt else None,
|
||||
timeout=remaining,
|
||||
strict_user=False,
|
||||
)
|
||||
is_first_prompt = False
|
||||
|
||||
if event is None:
|
||||
raise ToolFatalError("审批超时")
|
||||
|
||||
text = event.get_plaintext().strip().lower()
|
||||
if (
|
||||
text not in CONFIRM_WORDS
|
||||
and text not in REJECT_WORDS
|
||||
and text not in CANCEL_WORDS
|
||||
):
|
||||
continue
|
||||
|
||||
curr_user_id = str(event.get_user_id())
|
||||
is_authorized = curr_user_id == orig_user_id or await SUPERUSER(bot, event)
|
||||
|
||||
if not is_authorized:
|
||||
await bot.send(event, "⚠️ 权限不足,你无权审批此操作。")
|
||||
continue
|
||||
|
||||
if text in CONFIRM_WORDS:
|
||||
return True
|
||||
|
||||
raise AbortException(reason="审批被拒绝")
|
||||
@@ -0,0 +1,705 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, Protocol, TypeVar
|
||||
|
||||
import anyio
|
||||
from nonebot.utils import is_coroutine_callable
|
||||
|
||||
from zhenxun.services.ai.capabilities import (
|
||||
AbstractCapability,
|
||||
WrapModelRequestHandler,
|
||||
WrapRunHandler,
|
||||
WrapToolExecuteHandler,
|
||||
WrapToolValidateHandler,
|
||||
)
|
||||
from zhenxun.services.ai.core.messages import ChatRequest, ChatResponse
|
||||
from zhenxun.services.ai.core.models import LLMContext
|
||||
|
||||
from .context import RunContext
|
||||
from .models import AgentRunResult
|
||||
|
||||
_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])
|
||||
|
||||
|
||||
class HookTimeoutError(TimeoutError):
|
||||
"""当 Hook 函数执行超过配置的时间时抛出此异常。"""
|
||||
|
||||
def __init__(self, hook_name: str, func_name: str, timeout: float):
|
||||
self.hook_name = hook_name
|
||||
self.func_name = func_name
|
||||
self.timeout = timeout
|
||||
super().__init__(
|
||||
f"Hook {hook_name!r} 中的函数 {func_name!r} 执行超时 ({timeout}s)"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _HookEntry(Generic[_FuncT]):
|
||||
"""基础 Hook 注册实体,支持超时配置"""
|
||||
|
||||
func: _FuncT
|
||||
timeout: float | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ToolHookEntry(_HookEntry[_FuncT]):
|
||||
"""工具层 Hook 注册实体,支持工具过滤器"""
|
||||
|
||||
tools: frozenset[str] | None = None
|
||||
|
||||
|
||||
class BeforeRunHookFunc(Protocol):
|
||||
def __call__(self, ctx: RunContext[Any], /) -> None | Awaitable[None]: ...
|
||||
|
||||
|
||||
class AfterRunHookFunc(Protocol):
|
||||
def __call__(
|
||||
self, ctx: RunContext[Any], /, *, result: AgentRunResult[Any]
|
||||
) -> AgentRunResult[Any] | Awaitable[AgentRunResult[Any]]: ...
|
||||
|
||||
|
||||
class WrapRunHookFunc(Protocol):
|
||||
def __call__(
|
||||
self, ctx: RunContext[Any], /, *, handler: WrapRunHandler
|
||||
) -> AgentRunResult[Any] | Awaitable[AgentRunResult[Any]]: ...
|
||||
|
||||
|
||||
class OnRunErrorHookFunc(Protocol):
|
||||
def __call__(
|
||||
self, ctx: RunContext[Any], /, *, error: BaseException
|
||||
) -> AgentRunResult[Any] | Awaitable[AgentRunResult[Any]]: ...
|
||||
|
||||
|
||||
class BeforeModelRequestHookFunc(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
ctx: RunContext[Any],
|
||||
request_context: LLMContext[ChatRequest, ChatResponse],
|
||||
/,
|
||||
) -> (
|
||||
LLMContext[ChatRequest, ChatResponse]
|
||||
| Awaitable[LLMContext[ChatRequest, ChatResponse]]
|
||||
): ...
|
||||
|
||||
|
||||
class AfterModelRequestHookFunc(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
ctx: RunContext[Any],
|
||||
/,
|
||||
*,
|
||||
request_context: LLMContext[ChatRequest, ChatResponse],
|
||||
response: ChatResponse,
|
||||
) -> ChatResponse | Awaitable[ChatResponse]: ...
|
||||
|
||||
|
||||
class WrapModelRequestHookFunc(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
ctx: RunContext[Any],
|
||||
/,
|
||||
*,
|
||||
request_context: LLMContext[ChatRequest, ChatResponse],
|
||||
handler: WrapModelRequestHandler,
|
||||
) -> ChatResponse | Awaitable[ChatResponse]: ...
|
||||
|
||||
|
||||
class OnModelRequestErrorHookFunc(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
ctx: RunContext[Any],
|
||||
/,
|
||||
*,
|
||||
request_context: LLMContext[ChatRequest, ChatResponse],
|
||||
error: Exception,
|
||||
) -> ChatResponse | Awaitable[ChatResponse]: ...
|
||||
|
||||
|
||||
class PrepareToolsHookFunc(Protocol):
|
||||
def __call__(
|
||||
self, ctx: RunContext[Any], tool_defs: list[Any], /
|
||||
) -> list[Any] | Awaitable[list[Any]]: ...
|
||||
|
||||
|
||||
class BeforeToolValidateHookFunc(Protocol):
|
||||
def __call__(
|
||||
self, ctx: RunContext[Any], /, *, tool_name: str, args: str | dict[str, Any]
|
||||
) -> str | dict[str, Any] | Awaitable[str | dict[str, Any]]: ...
|
||||
|
||||
|
||||
class AfterToolValidateHookFunc(Protocol):
|
||||
def __call__(
|
||||
self, ctx: RunContext[Any], /, *, tool_name: str, args: dict[str, Any]
|
||||
) -> dict[str, Any] | Awaitable[dict[str, Any]]: ...
|
||||
|
||||
|
||||
class WrapToolValidateHookFunc(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
ctx: RunContext[Any],
|
||||
/,
|
||||
*,
|
||||
tool_name: str,
|
||||
args: str | dict[str, Any],
|
||||
handler: WrapToolValidateHandler,
|
||||
) -> dict[str, Any] | Awaitable[dict[str, Any]]: ...
|
||||
|
||||
|
||||
class OnToolValidateErrorHookFunc(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
ctx: RunContext[Any],
|
||||
/,
|
||||
*,
|
||||
tool_name: str,
|
||||
args: str | dict[str, Any],
|
||||
error: Exception,
|
||||
) -> dict[str, Any] | Awaitable[dict[str, Any]]: ...
|
||||
|
||||
|
||||
class BeforeToolExecuteHookFunc(Protocol):
|
||||
def __call__(
|
||||
self, ctx: RunContext[Any], /, *, tool_name: str, arguments: dict[str, Any]
|
||||
) -> dict[str, Any] | Awaitable[dict[str, Any]]: ...
|
||||
|
||||
|
||||
class AfterToolExecuteHookFunc(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
ctx: RunContext[Any],
|
||||
/,
|
||||
*,
|
||||
tool_name: str,
|
||||
arguments: dict[str, Any],
|
||||
result: Any,
|
||||
) -> Any | Awaitable[Any]: ...
|
||||
|
||||
|
||||
class WrapToolExecuteHookFunc(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
ctx: RunContext[Any],
|
||||
/,
|
||||
*,
|
||||
tool_name: str,
|
||||
arguments: dict[str, Any],
|
||||
handler: WrapToolExecuteHandler,
|
||||
) -> Any | Awaitable[Any]: ...
|
||||
|
||||
|
||||
class OnToolExecuteErrorHookFunc(Protocol):
|
||||
def __call__(
|
||||
self, ctx: RunContext[Any], /, *, tool_name: str, error: Exception
|
||||
) -> Any | Awaitable[Any]: ...
|
||||
|
||||
|
||||
async def _call_func(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
||||
if is_coroutine_callable(func):
|
||||
return await func(*args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
async def _call_entry(
|
||||
entry: _HookEntry[Any], hook_name: str, *args: Any, **kwargs: Any
|
||||
) -> Any:
|
||||
"""调用 Hook 函数实体,并自动应用熔断超时保护"""
|
||||
func = entry.func
|
||||
if entry.timeout is not None:
|
||||
try:
|
||||
with anyio.fail_after(entry.timeout):
|
||||
return await _call_func(func, *args, **kwargs)
|
||||
except TimeoutError:
|
||||
raise HookTimeoutError(
|
||||
hook_name=hook_name,
|
||||
func_name=getattr(func, "__name__", repr(func)),
|
||||
timeout=entry.timeout,
|
||||
) from None
|
||||
return await _call_func(func, *args, **kwargs)
|
||||
|
||||
|
||||
def _filter_tool_entries(
|
||||
entries: list[_HookEntry[Any]], *, tool_name: str
|
||||
) -> list[_HookEntry[Any]]:
|
||||
"""按工具名称过滤 Hook 实体"""
|
||||
return [
|
||||
entry
|
||||
for entry in entries
|
||||
if not (
|
||||
isinstance(entry, _ToolHookEntry)
|
||||
and entry.tools is not None
|
||||
and tool_name not in entry.tools
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _bare_or_parameterized(
|
||||
registry: dict[str, list[_HookEntry[Any]]],
|
||||
key: str,
|
||||
func: _FuncT | None,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
) -> _FuncT | Callable[[_FuncT], _FuncT]:
|
||||
"""处理无参数钩子的带参/不带参装饰器逻辑"""
|
||||
if func is not None:
|
||||
registry.setdefault(key, []).append(_HookEntry(func, timeout=timeout))
|
||||
return func
|
||||
|
||||
def decorator(f: _FuncT) -> _FuncT:
|
||||
registry.setdefault(key, []).append(_HookEntry(f, timeout=timeout))
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _tool_bare_or_parameterized(
|
||||
registry: dict[str, list[_HookEntry[Any]]],
|
||||
key: str,
|
||||
func: _FuncT | None,
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> _FuncT | Callable[[_FuncT], _FuncT]:
|
||||
"""处理工具钩子的带参/不带参装饰器逻辑"""
|
||||
frozen_tools = frozenset(tools) if tools is not None else None
|
||||
if func is not None:
|
||||
registry.setdefault(key, []).append(
|
||||
_ToolHookEntry(func, timeout=timeout, tools=frozen_tools)
|
||||
)
|
||||
return func
|
||||
|
||||
def decorator(f: _FuncT) -> _FuncT:
|
||||
registry.setdefault(key, []).append(
|
||||
_ToolHookEntry(f, timeout=timeout, tools=frozen_tools)
|
||||
)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class _HookRegistration:
|
||||
"""
|
||||
Hooks 的装饰器命名空间。
|
||||
利用 @overload 提供完美的 IDE 强类型补全和文档提示。
|
||||
"""
|
||||
|
||||
def __init__(self, hooks: "Hooks"):
|
||||
self._hooks = hooks
|
||||
|
||||
@property
|
||||
def _r(self) -> dict[str, list[_HookEntry[Any]]]:
|
||||
return self._hooks._registry
|
||||
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def before_run(self, func: BeforeRunHookFunc, /) -> BeforeRunHookFunc: ...
|
||||
@overload
|
||||
def before_run(
|
||||
self, *, timeout: float | None = None
|
||||
) -> Callable[[BeforeRunHookFunc], BeforeRunHookFunc]: ...
|
||||
def before_run(
|
||||
self, func: BeforeRunHookFunc | None = None, *, timeout: float | None = None
|
||||
) -> Any:
|
||||
"""注册运行前钩子。在 Agent 启动任何流转前触发。"""
|
||||
return _bare_or_parameterized(self._r, "before_run", func, timeout=timeout)
|
||||
|
||||
@overload
|
||||
def after_run(self, func: AfterRunHookFunc, /) -> AfterRunHookFunc: ...
|
||||
@overload
|
||||
def after_run(
|
||||
self, *, timeout: float | None = None
|
||||
) -> Callable[[AfterRunHookFunc], AfterRunHookFunc]: ...
|
||||
def after_run(
|
||||
self, func: AfterRunHookFunc | None = None, *, timeout: float | None = None
|
||||
) -> Any:
|
||||
"""注册运行后钩子。在 Agent 获取最终结果后触发,可修改结果。"""
|
||||
return _bare_or_parameterized(self._r, "after_run", func, timeout=timeout)
|
||||
|
||||
@overload
|
||||
def wrap_run(self, func: WrapRunHookFunc, /) -> WrapRunHookFunc: ...
|
||||
@overload
|
||||
def wrap_run(
|
||||
self, *, timeout: float | None = None
|
||||
) -> Callable[[WrapRunHookFunc], WrapRunHookFunc]: ...
|
||||
def wrap_run(
|
||||
self, func: WrapRunHookFunc | None = None, *, timeout: float | None = None
|
||||
) -> Any:
|
||||
"""注册运行包裹钩子。以洋葱模型接管整个 Agent 运行过程。"""
|
||||
return _bare_or_parameterized(self._r, "wrap_run", func, timeout=timeout)
|
||||
|
||||
@overload
|
||||
def on_run_error(self, func: OnRunErrorHookFunc, /) -> OnRunErrorHookFunc: ...
|
||||
@overload
|
||||
def on_run_error(
|
||||
self, *, timeout: float | None = None
|
||||
) -> Callable[[OnRunErrorHookFunc], OnRunErrorHookFunc]: ...
|
||||
def on_run_error(
|
||||
self, func: OnRunErrorHookFunc | None = None, *, timeout: float | None = None
|
||||
) -> Any:
|
||||
"""注册运行异常钩子。捕获 Agent 级别的致命错误。"""
|
||||
return _bare_or_parameterized(self._r, "on_run_error", func, timeout=timeout)
|
||||
|
||||
@overload
|
||||
def before_model_request(
|
||||
self, func: BeforeModelRequestHookFunc, /
|
||||
) -> BeforeModelRequestHookFunc: ...
|
||||
@overload
|
||||
def before_model_request(
|
||||
self, *, timeout: float | None = None
|
||||
) -> Callable[[BeforeModelRequestHookFunc], BeforeModelRequestHookFunc]: ...
|
||||
def before_model_request(
|
||||
self,
|
||||
func: BeforeModelRequestHookFunc | None = None,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
"""注册大模型请求前钩子。可在此修改发送给 LLM 的 Messages 等上下文。"""
|
||||
return _bare_or_parameterized(
|
||||
self._r, "before_model_request", func, timeout=timeout
|
||||
)
|
||||
|
||||
@overload
|
||||
def after_model_request(
|
||||
self, func: AfterModelRequestHookFunc, /
|
||||
) -> AfterModelRequestHookFunc: ...
|
||||
@overload
|
||||
def after_model_request(
|
||||
self, *, timeout: float | None = None
|
||||
) -> Callable[[AfterModelRequestHookFunc], AfterModelRequestHookFunc]: ...
|
||||
def after_model_request(
|
||||
self,
|
||||
func: AfterModelRequestHookFunc | None = None,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
"""注册大模型请求后钩子。可在此验证或修改 LLM 的原始 Response。"""
|
||||
return _bare_or_parameterized(
|
||||
self._r, "after_model_request", func, timeout=timeout
|
||||
)
|
||||
|
||||
@overload
|
||||
def wrap_model_request(
|
||||
self, func: WrapModelRequestHookFunc, /
|
||||
) -> WrapModelRequestHookFunc: ...
|
||||
@overload
|
||||
def wrap_model_request(
|
||||
self, *, timeout: float | None = None
|
||||
) -> Callable[[WrapModelRequestHookFunc], WrapModelRequestHookFunc]: ...
|
||||
def wrap_model_request(
|
||||
self,
|
||||
func: WrapModelRequestHookFunc | None = None,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
"""注册大模型请求包裹钩子。以洋葱模型接管 LLM 的网络请求过程。"""
|
||||
return _bare_or_parameterized(
|
||||
self._r, "wrap_model_request", func, timeout=timeout
|
||||
)
|
||||
|
||||
@overload
|
||||
def on_model_request_error(
|
||||
self, func: OnModelRequestErrorHookFunc, /
|
||||
) -> OnModelRequestErrorHookFunc: ...
|
||||
@overload
|
||||
def on_model_request_error(
|
||||
self, *, timeout: float | None = None
|
||||
) -> Callable[[OnModelRequestErrorHookFunc], OnModelRequestErrorHookFunc]: ...
|
||||
def on_model_request_error(
|
||||
self,
|
||||
func: OnModelRequestErrorHookFunc | None = None,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
"""注册大模型请求异常钩子。捕获超时或网络等异常。"""
|
||||
return _bare_or_parameterized(
|
||||
self._r, "on_model_request_error", func, timeout=timeout
|
||||
)
|
||||
|
||||
@overload
|
||||
def before_tool_execute(
|
||||
self, func: BeforeToolExecuteHookFunc, /
|
||||
) -> BeforeToolExecuteHookFunc: ...
|
||||
@overload
|
||||
def before_tool_execute(
|
||||
self, *, tools: list[str] | None = None, timeout: float | None = None
|
||||
) -> Callable[[BeforeToolExecuteHookFunc], BeforeToolExecuteHookFunc]: ...
|
||||
def before_tool_execute(
|
||||
self,
|
||||
func: BeforeToolExecuteHookFunc | None = None,
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
"""注册工具执行前钩子。可通过 tools 参数指定拦截特定工具,可篡改传入参数。"""
|
||||
return _tool_bare_or_parameterized(
|
||||
self._r, "before_tool_execute", func, tools=tools, timeout=timeout
|
||||
)
|
||||
|
||||
@overload
|
||||
def after_tool_execute(
|
||||
self, func: AfterToolExecuteHookFunc, /
|
||||
) -> AfterToolExecuteHookFunc: ...
|
||||
@overload
|
||||
def after_tool_execute(
|
||||
self, *, tools: list[str] | None = None, timeout: float | None = None
|
||||
) -> Callable[[AfterToolExecuteHookFunc], AfterToolExecuteHookFunc]: ...
|
||||
def after_tool_execute(
|
||||
self,
|
||||
func: AfterToolExecuteHookFunc | None = None,
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
"""注册工具执行后钩子。可通过 tools 参数指定拦截特定工具,可篡改返回结果。"""
|
||||
return _tool_bare_or_parameterized(
|
||||
self._r, "after_tool_execute", func, tools=tools, timeout=timeout
|
||||
)
|
||||
|
||||
@overload
|
||||
def wrap_tool_execute(
|
||||
self, func: WrapToolExecuteHookFunc, /
|
||||
) -> WrapToolExecuteHookFunc: ...
|
||||
@overload
|
||||
def wrap_tool_execute(
|
||||
self, *, tools: list[str] | None = None, timeout: float | None = None
|
||||
) -> Callable[[WrapToolExecuteHookFunc], WrapToolExecuteHookFunc]: ...
|
||||
def wrap_tool_execute(
|
||||
self,
|
||||
func: WrapToolExecuteHookFunc | None = None,
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
"""注册工具执行包裹钩子。以洋葱模型接管特定工具的执行逻辑。"""
|
||||
return _tool_bare_or_parameterized(
|
||||
self._r, "wrap_tool_execute", func, tools=tools, timeout=timeout
|
||||
)
|
||||
|
||||
@overload
|
||||
def on_tool_execute_error(
|
||||
self, func: OnToolExecuteErrorHookFunc, /
|
||||
) -> OnToolExecuteErrorHookFunc: ...
|
||||
@overload
|
||||
def on_tool_execute_error(
|
||||
self, *, tools: list[str] | None = None, timeout: float | None = None
|
||||
) -> Callable[[OnToolExecuteErrorHookFunc], OnToolExecuteErrorHookFunc]: ...
|
||||
def on_tool_execute_error(
|
||||
self,
|
||||
func: OnToolExecuteErrorHookFunc | None = None,
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
"""注册工具执行异常钩子。捕获特定工具的崩溃异常,可用于自愈重试。"""
|
||||
return _tool_bare_or_parameterized(
|
||||
self._r, "on_tool_execute_error", func, tools=tools, timeout=timeout
|
||||
)
|
||||
|
||||
|
||||
class Hooks(AbstractCapability):
|
||||
"""
|
||||
面向开发者的极简拦截器语法糖。
|
||||
允许通过 `@hooks.on.xxx` 装饰器快速介入大模型及工具生命周期的各个阶段。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._registry: dict[str, list[_HookEntry[Any]]] = {}
|
||||
self.on = _HookRegistration(self)
|
||||
|
||||
async def wrap_run(
|
||||
self, context: RunContext, handler: WrapRunHandler
|
||||
) -> AgentRunResult[Any]:
|
||||
"""派发:洋葱模型组装与全周期执行接管"""
|
||||
for entry in self._registry.get("before_run", []):
|
||||
await _call_entry(entry, "before_run", context)
|
||||
|
||||
entries = self._registry.get("wrap_run", [])
|
||||
chain = handler
|
||||
if entries:
|
||||
for entry in reversed(entries):
|
||||
|
||||
def _wrap(
|
||||
e: _HookEntry[Any], h: Callable[..., Any]
|
||||
) -> Callable[..., Any]:
|
||||
async def _wrapped() -> Any:
|
||||
return await _call_entry(e, "wrap_run", context, h)
|
||||
|
||||
return _wrapped
|
||||
|
||||
chain = _wrap(entry, chain)
|
||||
|
||||
try:
|
||||
result = await chain()
|
||||
except BaseException as error:
|
||||
for err_entry in reversed(self._registry.get("on_run_error", [])):
|
||||
try:
|
||||
return await _call_entry(err_entry, "on_run_error", context, error)
|
||||
except BaseException as new_err:
|
||||
error = new_err
|
||||
raise error
|
||||
|
||||
for after_entry in reversed(self._registry.get("after_run", [])):
|
||||
result = await _call_entry(after_entry, "after_run", context, result)
|
||||
return result
|
||||
|
||||
async def wrap_model_request(
|
||||
self,
|
||||
context: RunContext,
|
||||
llm_context: LLMContext[ChatRequest, ChatResponse],
|
||||
handler: WrapModelRequestHandler,
|
||||
) -> ChatResponse:
|
||||
"""派发:洋葱模型接管网络请求及前后生命周期"""
|
||||
for entry in self._registry.get("before_model_request", []):
|
||||
llm_context = await _call_entry(
|
||||
entry, "before_model_request", context, llm_context
|
||||
)
|
||||
|
||||
entries = self._registry.get("wrap_model_request", [])
|
||||
chain = handler
|
||||
if entries:
|
||||
for entry in reversed(entries):
|
||||
|
||||
def _make_chain(
|
||||
e: _HookEntry[Any], h: Callable[..., Any]
|
||||
) -> Callable[..., Any]:
|
||||
async def _wrapped(
|
||||
ctx_inner: LLMContext[ChatRequest, ChatResponse],
|
||||
) -> Any:
|
||||
return await _call_entry(
|
||||
e, "wrap_model_request", context, ctx_inner, h
|
||||
)
|
||||
|
||||
return _wrapped
|
||||
|
||||
chain = _make_chain(entry, chain)
|
||||
|
||||
try:
|
||||
response = await chain(llm_context)
|
||||
except Exception as error:
|
||||
for err_entry in reversed(self._registry.get("on_model_request_error", [])):
|
||||
try:
|
||||
return await _call_entry(
|
||||
err_entry, "on_model_request_error", context, llm_context, error
|
||||
)
|
||||
except Exception as new_err:
|
||||
error = new_err
|
||||
raise error
|
||||
|
||||
for after_entry in reversed(self._registry.get("after_model_request", [])):
|
||||
response = await _call_entry(
|
||||
after_entry, "after_model_request", context, llm_context, response
|
||||
)
|
||||
return response
|
||||
|
||||
async def wrap_tool_validate(
|
||||
self,
|
||||
context: RunContext,
|
||||
tool_name: str,
|
||||
args: str | dict[str, Any],
|
||||
handler: WrapToolValidateHandler,
|
||||
) -> dict[str, Any]:
|
||||
"""派发:洋葱模型接管工具参数校验及生命周期"""
|
||||
for entry in self._registry.get("before_tool_validate", []):
|
||||
args = await _call_entry(
|
||||
entry, "before_tool_validate", context, tool_name, args
|
||||
)
|
||||
|
||||
entries = self._registry.get("wrap_tool_validate", [])
|
||||
chain = handler
|
||||
if entries:
|
||||
for entry in reversed(entries):
|
||||
|
||||
def _wrap(
|
||||
e: _HookEntry[Any], h: Callable[..., Any]
|
||||
) -> Callable[..., Any]:
|
||||
async def _wrapped(args_inner: str | dict[str, Any]) -> Any:
|
||||
return await _call_entry(
|
||||
e, "wrap_tool_validate", context, tool_name, args_inner, h
|
||||
)
|
||||
|
||||
return _wrapped
|
||||
|
||||
chain = _wrap(entry, chain)
|
||||
|
||||
try:
|
||||
validated_args = await chain(args)
|
||||
except Exception as error:
|
||||
for err_entry in reversed(self._registry.get("on_tool_validate_error", [])):
|
||||
try:
|
||||
return await _call_entry(
|
||||
err_entry,
|
||||
"on_tool_validate_error",
|
||||
context,
|
||||
tool_name,
|
||||
args,
|
||||
error,
|
||||
)
|
||||
except Exception as new_err:
|
||||
error = new_err
|
||||
raise error
|
||||
|
||||
for after_entry in reversed(self._registry.get("after_tool_validate", [])):
|
||||
validated_args = await _call_entry(
|
||||
after_entry, "after_tool_validate", context, tool_name, validated_args
|
||||
)
|
||||
return validated_args
|
||||
|
||||
async def wrap_tool_execute(
|
||||
self,
|
||||
context: RunContext,
|
||||
tool_name: str,
|
||||
arguments: dict[str, Any],
|
||||
handler: WrapToolExecuteHandler,
|
||||
) -> Any:
|
||||
"""派发:洋葱模型接管工具执行(支持匹配名称)及生命周期"""
|
||||
for entry in _filter_tool_entries(
|
||||
self._registry.get("before_tool_execute", []), tool_name=tool_name
|
||||
):
|
||||
arguments = await _call_entry(
|
||||
entry, "before_tool_execute", context, tool_name, arguments
|
||||
)
|
||||
|
||||
entries = _filter_tool_entries(
|
||||
self._registry.get("wrap_tool_execute", []), tool_name=tool_name
|
||||
)
|
||||
chain = handler
|
||||
if entries:
|
||||
for entry in reversed(entries):
|
||||
|
||||
def _wrap(
|
||||
e: _HookEntry[Any], h: Callable[..., Any]
|
||||
) -> Callable[..., Any]:
|
||||
async def _wrapped(args_inner: dict[str, Any]) -> Any:
|
||||
return await _call_entry(
|
||||
e, "wrap_tool_execute", context, tool_name, args_inner, h
|
||||
)
|
||||
|
||||
return _wrapped
|
||||
|
||||
chain = _wrap(entry, chain)
|
||||
|
||||
try:
|
||||
result = await chain(arguments)
|
||||
except Exception as error:
|
||||
for err_entry in reversed(
|
||||
_filter_tool_entries(
|
||||
self._registry.get("on_tool_execute_error", []), tool_name=tool_name
|
||||
)
|
||||
):
|
||||
try:
|
||||
return await _call_entry(
|
||||
err_entry, "on_tool_execute_error", context, tool_name, error
|
||||
)
|
||||
except Exception as new_err:
|
||||
error = new_err
|
||||
raise error
|
||||
|
||||
for after_entry in reversed(
|
||||
_filter_tool_entries(
|
||||
self._registry.get("after_tool_execute", []), tool_name=tool_name
|
||||
)
|
||||
):
|
||||
result = await _call_entry(
|
||||
after_entry, "after_tool_execute", context, tool_name, arguments, result
|
||||
)
|
||||
return result
|
||||
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
运行时(Run)相关核心类型定义
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
import json
|
||||
from typing import Any, Generic, cast
|
||||
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.stream_events import (
|
||||
AgentStreamEvent,
|
||||
EventBus,
|
||||
ToolCallStartEvent,
|
||||
ToolStreamChunkEvent,
|
||||
)
|
||||
from zhenxun.utils.pydantic_compat import model_dump, model_validator
|
||||
|
||||
|
||||
class ChatSummary(BaseModel):
|
||||
total: int = 0
|
||||
"""大模型调用总次数"""
|
||||
total_latency_ms: float = 0.0
|
||||
"""大模型调用总耗时(毫秒)"""
|
||||
by_stop_reason: dict[str, int] = Field(default_factory=dict)
|
||||
"""按停止原因分类的大模型调用计数"""
|
||||
|
||||
|
||||
class ToolSummary(BaseModel):
|
||||
total: int = 0
|
||||
"""工具执行总次数"""
|
||||
ok: int = 0
|
||||
"""工具成功执行次数"""
|
||||
error: int = 0
|
||||
"""工具执行失败次数"""
|
||||
total_latency_ms: float = 0.0
|
||||
"""工具执行总耗时(毫秒)"""
|
||||
by_name: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
||||
"""按工具名称细分的执行状态统计"""
|
||||
|
||||
|
||||
class AgentRunSummary(BaseModel):
|
||||
"""Agent 单次运行的全局可观测性遥测摘要"""
|
||||
|
||||
chats: ChatSummary = Field(default_factory=ChatSummary)
|
||||
"""大模型调用遥测摘要"""
|
||||
tools: ToolSummary = Field(default_factory=ToolSummary)
|
||||
"""工具执行遥测摘要"""
|
||||
usage: UsageInfo = Field(default_factory=UsageInfo)
|
||||
"""Token 消耗总计"""
|
||||
total_latency_ms: float = 0.0
|
||||
"""智能体运行总耗时(毫秒)"""
|
||||
|
||||
|
||||
class HandoffPayload(BaseModel):
|
||||
"""移交信息载荷"""
|
||||
|
||||
target: str
|
||||
"""移交的目标智能体名称"""
|
||||
reason: str = ""
|
||||
"""移交的原因描述"""
|
||||
context_data: Any = ""
|
||||
"""随移交传递的上下文数据"""
|
||||
|
||||
|
||||
OutputDataT = TypeVar("OutputDataT", default=str)
|
||||
|
||||
|
||||
class AgentRunResult(BaseModel, Generic[OutputDataT]):
|
||||
"""Agent 单次无状态运行的结果"""
|
||||
|
||||
output: OutputDataT
|
||||
"""最终输出数据"""
|
||||
messages: list[AgentMessage] = Field(default_factory=list)
|
||||
"""本次运行产生/更新的历史消息"""
|
||||
usage: UsageInfo = Field(default_factory=UsageInfo)
|
||||
"""本次运行的Token消耗总计"""
|
||||
structured_data: Any | None = None
|
||||
"""拦截到的结构化结果字典"""
|
||||
telemetry: AgentRunSummary | None = None
|
||||
"""单次运行的完整可观测性遥测摘要"""
|
||||
handoff: HandoffPayload | None = None
|
||||
"""向外抛出的软移交载荷(存在时说明Agent发起了移交请求)"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
@property
|
||||
def llm_messages(self) -> list[LLMMessage]:
|
||||
"""动态视图:过滤掉内部业务事件 (AgentEvent),
|
||||
仅返回纯净的底层聊天消息历史"""
|
||||
return [m for m in self.messages if isinstance(m, LLMMessage)]
|
||||
|
||||
|
||||
class AgentRunStart(AgentStreamEvent):
|
||||
"""智能体运行开始"""
|
||||
|
||||
agent_name: str
|
||||
"""启动运行的智能体名称"""
|
||||
|
||||
|
||||
class AgentRunError(AgentStreamEvent):
|
||||
"""智能体运行发生异常"""
|
||||
|
||||
error: BaseException
|
||||
"""智能体运行过程中抛出的异常"""
|
||||
|
||||
|
||||
class AgentRunEnd(AgentStreamEvent):
|
||||
"""智能体运行完全结束"""
|
||||
|
||||
result: AgentRunResult[Any]
|
||||
"""智能体运行结束后的完整结果"""
|
||||
|
||||
|
||||
class StreamedRunResult(Generic[OutputDataT]):
|
||||
"""
|
||||
智能体流式运行的结果代理对象。
|
||||
提供高度解耦的方法来消费底层事件流,支持获取纯净文本或全部事件。
|
||||
"""
|
||||
|
||||
def __init__(self, event_bus: EventBus):
|
||||
self._event_bus = event_bus
|
||||
self.is_complete: bool = False
|
||||
self._result: AgentRunResult[OutputDataT] | None = None
|
||||
|
||||
async def stream_events(self) -> AsyncIterator[Any]:
|
||||
"""获取底层的所有原始事件(包含工具调用过程等)"""
|
||||
from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunError
|
||||
|
||||
async for event in self._event_bus:
|
||||
if isinstance(event, AgentRunEnd):
|
||||
self._result = cast(AgentRunResult[OutputDataT], event.result)
|
||||
self.is_complete = True
|
||||
elif isinstance(event, AgentRunError):
|
||||
raise event.error.with_traceback(None) from None
|
||||
yield event
|
||||
|
||||
async def stream_text(self, delta: bool = False) -> AsyncIterator[str]:
|
||||
"""
|
||||
过滤大模型的输出文本。
|
||||
"""
|
||||
full_text = ""
|
||||
async for _ in self.stream_events():
|
||||
pass
|
||||
|
||||
if self._result is not None:
|
||||
output = self._result.output
|
||||
if isinstance(output, str):
|
||||
full_text = output
|
||||
else:
|
||||
if isinstance(output, BaseModel):
|
||||
full_text = json.dumps(model_dump(output), ensure_ascii=False)
|
||||
else:
|
||||
full_text = str(output)
|
||||
|
||||
if delta:
|
||||
yield full_text
|
||||
else:
|
||||
yield full_text
|
||||
|
||||
async def get_output(self) -> OutputDataT:
|
||||
"""阻塞并等待整个 Agent 执行完毕,返回最终的解析输出数据"""
|
||||
if self._result is not None:
|
||||
return self._result.output
|
||||
|
||||
async for _ in self.stream_events():
|
||||
pass
|
||||
|
||||
if self._result is None:
|
||||
raise RuntimeError("Agent 运行异常结束,未产生最终结果。")
|
||||
|
||||
return self._result.output
|
||||
|
||||
async def get_run_result(self) -> AgentRunResult[OutputDataT]:
|
||||
"""获取完整的运行结果对象(包含 Token 消耗等)"""
|
||||
if self._result is not None:
|
||||
return self._result
|
||||
|
||||
await self.get_output()
|
||||
|
||||
return cast(AgentRunResult[OutputDataT], self._result)
|
||||
|
||||
async def forward_to(
|
||||
self, target_event_bus: EventBus | None, prefix_name: str
|
||||
) -> AgentRunResult[OutputDataT]:
|
||||
"""将底层的事件流自动格式化并转发给另一个事件发射器,
|
||||
常用于 DelegateTool 嵌套调用"""
|
||||
async for event in self.stream_events():
|
||||
if not target_event_bus:
|
||||
continue
|
||||
|
||||
if isinstance(event, ToolStreamChunkEvent):
|
||||
await target_event_bus.emit(
|
||||
ToolStreamChunkEvent(
|
||||
tool_name=f"{prefix_name} -> {event.tool_name}",
|
||||
content=event.content,
|
||||
metadata=event.metadata,
|
||||
)
|
||||
)
|
||||
elif isinstance(event, ToolCallStartEvent):
|
||||
intent_str = (
|
||||
f" (意图: {event.intent})" if getattr(event, "intent", None) else ""
|
||||
)
|
||||
await target_event_bus.emit(
|
||||
ToolStreamChunkEvent(
|
||||
tool_name=prefix_name,
|
||||
content=f"🔁 正在调用工具: {event.tool_name}...{intent_str}",
|
||||
)
|
||||
)
|
||||
|
||||
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 Task(BaseModel):
|
||||
"""标准化数据契约(意图载体 Payload),定义大模型需要做什么及产出什么格式"""
|
||||
|
||||
id: str = Field(default_factory=lambda: __import__("uuid").uuid4().hex)
|
||||
"""任务的唯一标识符"""
|
||||
|
||||
name: str | None = None
|
||||
"""任务的简短名称"""
|
||||
|
||||
description: str
|
||||
"""详细的任务指令(告诉大模型具体需要做什么)"""
|
||||
|
||||
expected_output: str
|
||||
"""预期输出的自然语言描述(指导大模型如何组织最终答案)"""
|
||||
|
||||
response_model: Any | None = None
|
||||
"""强制要求返回的强类型结构 (Pydantic Model) 或
|
||||
OutputDefinition,为空则返回普通文本"""
|
||||
|
||||
tools: list[str | Any] | None = None
|
||||
"""针对此特定任务动态追加或覆盖的工具列表"""
|
||||
|
||||
guardrails: list[Any] | None = None
|
||||
"""护栏验证列表。支持传入函数、BaseGuardrail 实例,
|
||||
或直接传入自然语言字符串规则(自动转为 LLM 裁判)"""
|
||||
|
||||
_parsed_guardrails: list[Any] = PrivateAttr(default_factory=list)
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _parse_and_set_guardrails(self) -> "Task":
|
||||
from zhenxun.services.ai.guardrails import parse_guardrails
|
||||
|
||||
self._parsed_guardrails = parse_guardrails(self.guardrails)
|
||||
return self
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentRunResult",
|
||||
"OutputDataT",
|
||||
"StreamedRunResult",
|
||||
"Task",
|
||||
"TaskResult",
|
||||
]
|
||||
@@ -0,0 +1,169 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from contextvars import ContextVar
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from zhenxun.services.ai.core.models import CancellationToken
|
||||
from zhenxun.services.ai.utils.scope import BaseScopeBuilder, ScopeSelector
|
||||
|
||||
|
||||
class PendingMessageQueue:
|
||||
"""简单的运行时挂起消息队列"""
|
||||
|
||||
def __init__(self):
|
||||
self._queue: list[Any] = []
|
||||
|
||||
def enqueue(self, msg: Any):
|
||||
self._queue.append(msg)
|
||||
|
||||
def drain(self) -> list[Any]:
|
||||
msgs = list(self._queue)
|
||||
self._queue.clear()
|
||||
return msgs
|
||||
|
||||
def has_items(self) -> bool:
|
||||
return len(self._queue) > 0
|
||||
|
||||
|
||||
class TaskStopper(BaseScopeBuilder["TaskStopper"]):
|
||||
"""
|
||||
声明式任务中止器 (Fluent Task Stopper)。
|
||||
为第三方开发者提供友好的链式 API,精准中止正在运行或排队的大模型任务。
|
||||
"""
|
||||
|
||||
def __init__(self, manager: "AgentSessionManager"):
|
||||
super().__init__()
|
||||
self.manager = manager
|
||||
|
||||
async def cancel(self) -> int:
|
||||
"""执行中止动作,返回被成功中止的任务数量。"""
|
||||
return await self.manager.cancel_by_query(self._selector)
|
||||
|
||||
|
||||
class SessionInfo(BaseModel):
|
||||
"""会话信息的元数据视图。"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
session_id: str
|
||||
"""会话的唯一标识符"""
|
||||
state: dict[str, Any] = Field(default_factory=dict)
|
||||
"""业务流转的强类型载荷"""
|
||||
created_at: float = Field(default_factory=time.time)
|
||||
"""会话创建的时间戳"""
|
||||
updated_at: float = Field(default_factory=time.time)
|
||||
"""会话最后更新的时间戳"""
|
||||
active_task: Any | None = Field(default=None)
|
||||
"""当前正在执行的 asyncio.Task"""
|
||||
cancel_token: CancellationToken | None = Field(default=None)
|
||||
"""当前任务的取消令牌"""
|
||||
steer_queue: PendingMessageQueue = Field(default_factory=PendingMessageQueue)
|
||||
"""动态转向指令干预队列"""
|
||||
follow_up_queue: PendingMessageQueue = Field(default_factory=PendingMessageQueue)
|
||||
"""后续追加指令干预队列"""
|
||||
|
||||
|
||||
class LockContext(BaseModel):
|
||||
"""并发锁的执行追踪器(解决 INTERRUPT 需要跨 Session 取消任务的问题)"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
active_task: Any | None = None
|
||||
cancel_token: CancellationToken | None = None
|
||||
|
||||
|
||||
class AgentSessionManager:
|
||||
"""
|
||||
Agent 会话状态管理器。
|
||||
彻底拥抱无状态:只维护业务强类型载荷 (state payload) 以及并发锁,不干涉 LLM 历史。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._sessions: dict[str, SessionInfo] = {}
|
||||
self._locks: dict[str, asyncio.Lock] = {}
|
||||
self._exec_locks: dict[str, asyncio.Lock] = {}
|
||||
self.lock_contexts: dict[str, LockContext] = {}
|
||||
self.live_tasks: dict[str, list[tuple[CancellationToken, Any]]] = {}
|
||||
|
||||
def stopper(self) -> TaskStopper:
|
||||
"""获取声明式任务中止器,供第三方开发者极速中止运行中/排队中的任务"""
|
||||
return TaskStopper(self)
|
||||
|
||||
async def cancel_by_query(self, query: ScopeSelector) -> int:
|
||||
"""根据查询条件取消符合条件的会话任务。返回取消的数量"""
|
||||
count = 0
|
||||
scope_prefix = query.scope_prefix
|
||||
|
||||
for sid, tasks in list(self.live_tasks.items()):
|
||||
if sid.startswith(scope_prefix) or (
|
||||
query.session_id and sid == query.session_id
|
||||
):
|
||||
for token, task in tasks:
|
||||
if not token.is_cancelled():
|
||||
token.cancel()
|
||||
count += 1
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
if count > 0:
|
||||
logger.info(
|
||||
f"🛑 [TaskStopper] 已强制终止排队或执行中的会话任务: {sid}"
|
||||
)
|
||||
return count
|
||||
|
||||
def _get_lock(self, session_id: str) -> asyncio.Lock:
|
||||
if session_id not in self._locks:
|
||||
self._locks[session_id] = asyncio.Lock()
|
||||
return self._locks[session_id]
|
||||
|
||||
def get_exec_lock(self, session_id: str) -> asyncio.Lock:
|
||||
if session_id not in self._exec_locks:
|
||||
self._exec_locks[session_id] = asyncio.Lock()
|
||||
return self._exec_locks[session_id]
|
||||
|
||||
async def get_or_create(self, session_id: str) -> SessionInfo:
|
||||
async with self._get_lock(session_id):
|
||||
if session_id not in self._sessions:
|
||||
self._sessions[session_id] = SessionInfo(session_id=session_id)
|
||||
return self._sessions[session_id]
|
||||
|
||||
async def get(self, session_id: str) -> SessionInfo | None:
|
||||
async with self._get_lock(session_id):
|
||||
return self._sessions.get(session_id)
|
||||
|
||||
async def update_state(self, session_id: str, new_state: dict[str, Any]):
|
||||
async with self._get_lock(session_id):
|
||||
if session_id in self._sessions:
|
||||
self._sessions[session_id].state.update(new_state)
|
||||
self._sessions[session_id].updated_at = time.time()
|
||||
|
||||
async def delete(self, session_id: str):
|
||||
async with self._get_lock(session_id):
|
||||
self._sessions.pop(session_id, None)
|
||||
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
|
||||
|
||||
default_ctx = memory_manager.get_chat_context(MemoryConfig())
|
||||
if default_ctx:
|
||||
await default_ctx.clear(SessionMetadata(session_id=session_id))
|
||||
|
||||
|
||||
session_manager = AgentSessionManager()
|
||||
active_session_id: ContextVar[str | None] = ContextVar(
|
||||
"active_session_id", default=None
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def agent_session_scope(session_id: str):
|
||||
"""声明式上下文包装器。进入此作用域后的 Agent 都会自动吸附到指定的 SessionID 上。"""
|
||||
await session_manager.get_or_create(session_id)
|
||||
token = active_session_id.set(session_id)
|
||||
try:
|
||||
yield session_id
|
||||
finally:
|
||||
active_session_id.reset(token)
|
||||
@@ -0,0 +1,138 @@
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
from zhenxun.services.ai.core.messages import BaseContentPart, ImagePart, TextPart
|
||||
from zhenxun.services.ai.core.stream_events import (
|
||||
EventBus,
|
||||
LLMEndEvent,
|
||||
LLMStartEvent,
|
||||
ToolCallEndEvent,
|
||||
ToolCallStartEvent,
|
||||
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
|
||||
|
||||
|
||||
class TelemetrySubscriber:
|
||||
"""纯粹的数据观察者:默默记录时间戳与 Token 消耗"""
|
||||
|
||||
def __init__(self):
|
||||
self.summary = AgentRunSummary()
|
||||
self._start_times: dict[str, float] = {}
|
||||
|
||||
def attach(self, bus: EventBus):
|
||||
bus.subscribe(AgentRunStart, self.on_run_start)
|
||||
bus.subscribe(AgentRunEnd, self.on_run_end)
|
||||
bus.subscribe(LLMStartEvent, self.on_llm_start)
|
||||
bus.subscribe(LLMEndEvent, self.on_llm_end)
|
||||
bus.subscribe(ToolCallStartEvent, self.on_tool_start)
|
||||
bus.subscribe(ToolCallEndEvent, self.on_tool_end)
|
||||
|
||||
async def on_run_start(self, event: AgentRunStart):
|
||||
self._start_times["run"] = time.monotonic()
|
||||
logger.debug(f"🚀 [Telemetry] 智能体 {event.agent_name} 开始运行")
|
||||
|
||||
async def on_run_end(self, event: AgentRunEnd):
|
||||
dur = (time.monotonic() - self._start_times.get("run", time.monotonic())) * 1000
|
||||
self.summary.total_latency_ms = dur
|
||||
event.result.telemetry = self.summary
|
||||
logger.debug(f"🏁 [Telemetry] 智能体运行结束 (总耗时: {dur:.2f}ms)")
|
||||
|
||||
async def on_llm_start(self, event: LLMStartEvent):
|
||||
self._start_times["llm"] = time.monotonic()
|
||||
|
||||
async def on_llm_end(self, event: LLMEndEvent):
|
||||
dur = (time.monotonic() - self._start_times.pop("llm", time.monotonic())) * 1000
|
||||
self.summary.chats.total += 1
|
||||
self.summary.chats.total_latency_ms += dur
|
||||
|
||||
response = event.response
|
||||
if response:
|
||||
stop_reason = "tool_calls" if response.tool_calls else "stop"
|
||||
self.summary.chats.by_stop_reason[stop_reason] = (
|
||||
self.summary.chats.by_stop_reason.get(stop_reason, 0) + 1
|
||||
)
|
||||
logger.debug(f"🧠 [Telemetry] 模型调用完成 (耗时: {dur:.2f}ms)")
|
||||
|
||||
async def on_tool_start(self, event: ToolCallStartEvent):
|
||||
self._start_times[f"tool_{event.tool_name}"] = time.monotonic()
|
||||
|
||||
async def on_tool_end(self, event: ToolCallEndEvent):
|
||||
dur = (
|
||||
time.monotonic()
|
||||
- self._start_times.pop(f"tool_{event.tool_name}", time.monotonic())
|
||||
) * 1000
|
||||
self.summary.tools.total += 1
|
||||
self.summary.tools.total_latency_ms += dur
|
||||
|
||||
tool_stat = self.summary.tools.by_name.setdefault(
|
||||
event.tool_name, {"total": 0, "ok": 0, "error": 0, "latency_ms": 0.0}
|
||||
)
|
||||
tool_stat["total"] += 1
|
||||
tool_stat["latency_ms"] += dur
|
||||
|
||||
if event.is_error:
|
||||
self.summary.tools.error += 1
|
||||
tool_stat["error"] += 1
|
||||
else:
|
||||
self.summary.tools.ok += 1
|
||||
tool_stat["ok"] += 1
|
||||
logger.debug(
|
||||
f"🛠️ [Telemetry] 工具 {event.tool_name} 执行完毕 (耗时: {dur:.2f}ms)"
|
||||
)
|
||||
|
||||
|
||||
class DefaultUISubscriber:
|
||||
"""纯粹的 UI 观察者:负责将特定事件转化为群聊气泡发送"""
|
||||
|
||||
def __init__(
|
||||
self, context: RunContext, reply_to: bool = False, verbose: bool = False
|
||||
):
|
||||
self.context = context
|
||||
self.reply_to = reply_to
|
||||
self.verbose = verbose
|
||||
self.bot = context.get_bot()
|
||||
self.event = context.get_event()
|
||||
|
||||
def attach(self, bus: EventBus):
|
||||
bus.subscribe(ToolStreamChunkEvent, self.on_tool_stream)
|
||||
bus.subscribe(UserCustomEvent, self.on_custom_event)
|
||||
|
||||
async def _send_to_platform(self, display: Any):
|
||||
if not self.bot or not self.event or not display:
|
||||
return
|
||||
|
||||
if (
|
||||
isinstance(display, list)
|
||||
and len(display) > 0
|
||||
and isinstance(display[0], BaseContentPart)
|
||||
):
|
||||
msg = UniMessage()
|
||||
for part in display:
|
||||
if isinstance(part, TextPart) and part.text:
|
||||
msg = msg.text(part.text)
|
||||
elif isinstance(part, ImagePart):
|
||||
if part.raw:
|
||||
msg = msg.image(raw=part.raw)
|
||||
elif part.url:
|
||||
msg = msg.image(url=part.url)
|
||||
elif part.path:
|
||||
msg = msg.image(path=part.path)
|
||||
display = msg
|
||||
|
||||
if isinstance(display, UniMessage):
|
||||
await display.send(self.event, bot=self.bot, reply_to=self.reply_to)
|
||||
else:
|
||||
await self.bot.send(self.event, str(display))
|
||||
|
||||
async def on_tool_stream(self, event: ToolStreamChunkEvent):
|
||||
if self.verbose and event.content:
|
||||
await self._send_to_platform(event.content)
|
||||
|
||||
async def on_custom_event(self, event: UserCustomEvent):
|
||||
await self._send_to_platform(event.display)
|
||||
@@ -0,0 +1,83 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent, UserCustomEvent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from zhenxun.services.ai.run.context import RunContext
|
||||
|
||||
|
||||
class UIController:
|
||||
"""前端 UI 富交互流式控制器 (按需生成模式)"""
|
||||
|
||||
def __init__(self, context: "RunContext"):
|
||||
self.context = context
|
||||
|
||||
@property
|
||||
def tool_name(self) -> str:
|
||||
"""从上下文中动态获取当前调用的工具名"""
|
||||
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}
|
||||
)
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
@staticmethod
|
||||
async def handle_control_flow_exit_display(
|
||||
e: BaseException, context: "RunContext | None", reply_to: bool = False
|
||||
) -> None:
|
||||
"""统一处理 ControlFlowExit 异常带来的 UI 反馈逻辑"""
|
||||
from zhenxun.services.ai.core.exceptions import ControlFlowExit
|
||||
|
||||
if not isinstance(e, ControlFlowExit):
|
||||
return
|
||||
|
||||
display_msg = getattr(e, "display", None) or getattr(e, "display_content", None)
|
||||
if not display_msg:
|
||||
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))
|
||||
else:
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
await MessageUtils.build_message(str(display_msg)).send(
|
||||
reply_to=reply_to
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user