Files

81 lines
2.7 KiB
Python

"""Unified HITL (human-in-the-loop) approval flow.
Used by both the main agent loop and browser sub-agents.
"""
from __future__ import annotations
import asyncio
import logging
from datetime import datetime
from uuid import uuid4
from backend.apps.agents.models import AgentSession, ApprovalRequest
from backend.apps.agents.manager.ws_manager import ws_manager
from backend.apps.analytics.collector import record as _analytics
logger = logging.getLogger(__name__)
async def request_approval(
session: AgentSession,
tool_name: str,
tool_input: dict,
timeout: float | None = None,
track_analytics: bool = True,
) -> dict:
"""Unified HITL approval flow.
Creates an ApprovalRequest, sends it via WebSocket, waits for the user's
decision, cleans up, and returns the decision dict.
Returns: {"behavior": "allow"|"deny", "message": ..., "updated_input": ...}
"""
safe_input = tool_input if isinstance(tool_input, dict) else {}
request_id = uuid4().hex
approval_req = ApprovalRequest(
id=request_id, session_id=session.id,
tool_name=tool_name, tool_input=safe_input,
)
session.pending_approvals.append(approval_req)
session.status = "waiting_approval"
if track_analytics:
_analytics("approval.requested", {
"tool_name": tool_name,
"is_first_approval_in_session": len(session.pending_approvals) == 1,
"model": session.model,
}, session_id=session.id, dashboard_id=session.dashboard_id)
await ws_manager.emit_status(session.id, "waiting_approval")
if timeout is not None:
try:
decision = await asyncio.wait_for(
ws_manager.send_approval_request(session.id, request_id, tool_name, safe_input),
timeout=timeout,
)
except asyncio.TimeoutError:
decision = {"behavior": "deny", "message": "Approval timed out"}
else:
decision = await ws_manager.send_approval_request(
session.id, request_id, tool_name, safe_input,
)
if track_analytics:
latency_ms = int((datetime.now() - approval_req.created_at).total_seconds() * 1000)
_analytics("approval.resolved", {
"tool_name": tool_name,
"decision": decision.get("behavior", "unknown"),
"latency_ms": latency_ms,
"input_was_modified": decision.get("updated_input") is not None,
"model": session.model,
}, session_id=session.id, dashboard_id=session.dashboard_id)
session.pending_approvals = [a for a in session.pending_approvals if a.id != request_id]
session.status = "running"
await ws_manager.emit_status(session.id, "running")
return decision