Files
zhenxun_bot/zhenxun/services/ai/run/session.py
T
0b32d69c9c ♻️ refactor(tools): 重构工具装饰器系统并优化沙箱与文档注释 (#2147)
* ♻️ refactor(ai): 重构 AI 服务模块并完善文档注释

- 【重构】统一清理并优化所有 AI 服务模块文件的导入语句,将其移至文件顶部
- 【重构】重构 `hooks.py` 中的 `Hooks` 派发逻辑,使用通用管道函数消除重复代码,并引入 `HookPoint` 描述符
- 【重构】重构工具装饰器实现,新增 `toolkit` 类装饰器,优化 `BaseToolkit` 配置合并与前缀处理
- 【功能】Docker 沙箱容器创建时支持自动注入系统代理环境变量并配置 `ExtraHosts`
- 【功能】Jupyter 服务启动前自动清理旧进程并初始化临时目录权限
- 【修复】优化 Pydantic 结构化输出校验失败时的错误信息提取,提供更详细的字段级错误反馈
- 【修复】在 `api.py` 中避免将 `ModelRetry` 和 `ControlFlowExit` 异常错误地包装为 `LLMException`
- 【文档】为 AI 服务、沙箱、工具链、工作流等核心模块补充完整的 Docstring 和类型注释

* 📝 docs(ai): 补全核心模块文档注释并清理冗余代码

- 补全 `run/context`、`run/hooks` 和 `tools/engine/registry` 中类与方法的中文文档注释
- 清理 `tools/providers/builtin/sandbox` 中未使用的 `PythonPluginProtocol` 协议及相关导入
- 规范化部分代码的格式与尾随逗号

* ♻️ refactor!(flow): 重构 Task 为 AgentTask 并优化工作流元数据定义

- 【Breaking Change】将 `Task` 重命名为 `AgentTask` 以避免命名冲突
- 更新 Agent、Team、Workflow 等模块中的类型声明与相关逻辑
- 引入 `AutoNodeMeta` 强类型元数据,替换工作流装饰器中的裸字典定义
- 将 `StepMeta`、`ConditionMeta` 和 `RouterMeta` 统一移动至 `types.py`
- 优化 `RunnableNode` 对上游 `AgentTask` 的处理与拼接逻辑
- 调整团队协作策略中 `FinishAction` 的返回值为完整结果对象

* ♻️ refactor(workflow): 移除人工确认机制并重构错误策略

- 移除工作流节点的人工确认(HITL)与挂起继续机制
- 删除 `auto` 自动化工作流及相关装饰器文件
- 将错误处理策略类从 `types.py` 拆分并移动到新文件 `policies.py`
- 优化节点执行失败时的异常信息格式化输出
- 移除 `WorkflowRunResult` 和 `StepOutput` 中与挂起相关的状态字段

* 🚨 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>
2026-07-05 11:47:21 +08:00

178 lines
6.7 KiB
Python

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:
"""获取指定会话的信息,不存在则返回 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)