2.1 Orchestrator in workflow:
- New run_orchestrator_node() in workflow_integration.py loads agents from DB,
supports route/sequential/debate/pipeline modes
- New 'orchestrator' node type in workflow_engine.py execute_node dispatch
2.2 Tool-level human approval:
- AgentToolConfig extended with require_approval, approval_timeout_ms,
approval_default fields
- New ApprovalManager (approval_manager.py) with asyncio.Event-based
create/wait_for_decision/resolve pattern
- AgentRuntime run() and run_stream() intercept tool execution,
wait for approval decision before executing
- New POST /api/v1/approval/{id}/resolve REST endpoint
- Frontend: approval_required SSE event handling, approval dialog UI
with approve/deny/skip buttons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
95 lines
3.4 KiB
Python
95 lines
3.4 KiB
Python
"""工具级人工审批管理器 — asyncio.Event 驱动的异步审批等待/唤醒"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
import uuid
|
||
from dataclasses import dataclass, field
|
||
from typing import Any, Dict, Optional
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@dataclass
|
||
class ApprovalRequest:
|
||
approval_id: str
|
||
tool_name: str
|
||
args: Dict[str, Any]
|
||
event: asyncio.Event = field(default_factory=asyncio.Event)
|
||
decision: str = "deny" # approved | denied | skip
|
||
|
||
|
||
class ApprovalManager:
|
||
"""全局单例 — 管理工具执行前的审批等待/唤醒。"""
|
||
|
||
_pending: Dict[str, ApprovalRequest] = {}
|
||
|
||
def create(self, tool_name: str, args: Dict[str, Any]) -> ApprovalRequest:
|
||
"""创建审批请求(不等待),返回 ApprovalRequest(含 approval_id)。
|
||
|
||
用于流式场景:先 create → yield SSE 事件(带 approval_id)→ wait_for_decision。
|
||
"""
|
||
approval_id = str(uuid.uuid4())[:8]
|
||
req = ApprovalRequest(
|
||
approval_id=approval_id,
|
||
tool_name=tool_name,
|
||
args=args,
|
||
)
|
||
self._pending[approval_id] = req
|
||
logger.info("审批请求已创建: id=%s tool=%s", approval_id, tool_name)
|
||
return req
|
||
|
||
async def wait_for_decision(self, approval_id: str, timeout_ms: int = 60000) -> str:
|
||
"""等待审批决定(带超时)。返回 "approved" | "denied" | "skip" """
|
||
req = self._pending.get(approval_id)
|
||
if not req:
|
||
return "deny"
|
||
try:
|
||
await asyncio.wait_for(req.event.wait(), timeout=timeout_ms / 1000.0)
|
||
except asyncio.TimeoutError:
|
||
logger.warning("审批请求超时: id=%s", approval_id)
|
||
req.decision = "deny"
|
||
self._pending.pop(approval_id, None)
|
||
return req.decision
|
||
|
||
async def submit(self, tool_name: str, args: Dict[str, Any], timeout_ms: int = 60000) -> ApprovalRequest:
|
||
"""提交审批请求并等待决策(带超时)— 非流式场景一步完成。
|
||
|
||
Returns:
|
||
ApprovalRequest(含 decision 字段,调用方读取即可)
|
||
"""
|
||
req = self.create(tool_name, args)
|
||
try:
|
||
await asyncio.wait_for(req.event.wait(), timeout=timeout_ms / 1000.0)
|
||
except asyncio.TimeoutError:
|
||
logger.warning("审批请求超时: id=%s tool=%s", req.approval_id, tool_name)
|
||
req.decision = "deny"
|
||
self._pending.pop(req.approval_id, None)
|
||
return req
|
||
|
||
def resolve(self, approval_id: str, decision: str) -> bool:
|
||
"""外部(API)调用,写入审批决定并唤醒等待方。
|
||
|
||
Returns:
|
||
True 表示成功唤醒,False 表示审批 ID 无效或已完成。
|
||
"""
|
||
req = self._pending.get(approval_id)
|
||
if not req:
|
||
logger.warning("审批 ID 无效或已完成: %s", approval_id)
|
||
return False
|
||
if decision not in ("approved", "denied", "skip"):
|
||
decision = "deny"
|
||
req.decision = decision
|
||
req.event.set()
|
||
logger.info("审批已解决: id=%s decision=%s", approval_id, decision)
|
||
return True
|
||
|
||
def get_pending(self, approval_id: str) -> Optional[ApprovalRequest]:
|
||
"""查询待审批请求(用于前端展示详情)。"""
|
||
return self._pending.get(approval_id)
|
||
|
||
|
||
# 全局单例
|
||
approval_manager = ApprovalManager()
|