"""AskHumanTool — Human-in-the-Loop tool for Chat mode. When registered in a Chat-mode Agent, this tool allows the ReAct loop to pause and ask the user a question, then wait for a reply via the WebSocket connection. """ from __future__ import annotations import asyncio import logging import uuid from typing import Any from agentkit.tools.base import Tool logger = logging.getLogger(__name__) class AskHumanTool(Tool): """Tool that asks the human user a question and waits for a reply. Only functional in Chat mode where a WebSocket connection exists. In Task mode, this tool should not be registered. Usage in ReAct loop: The Agent calls this tool when it needs clarification or a decision from the user. The question is pushed to the client via WebSocket, and the tool blocks until the user replies or a timeout expires. """ def __init__(self, timeout: float = 60.0): super().__init__( name="ask_human", description="Ask the human user a question and wait for their reply. " "Use this when you need clarification, a decision, or " "confirmation from the user before proceeding.", ) self._timeout = timeout # Shared dict injected by the Chat WebSocket handler: # request_id -> asyncio.Future self._pending_replies: dict[str, asyncio.Future] | None = None # Callback to push question to client self._ask_callback: Any = None def configure( self, pending_replies: dict[str, asyncio.Future] | None = None, ask_callback: Any = None, ) -> None: """Configure the tool with WebSocket communication channels. Args: pending_replies: Dict mapping request_id to Future that will be resolved when the user replies. ask_callback: Async callable(request_id, question, options) that pushes the question to the client. """ self._pending_replies = pending_replies self._ask_callback = ask_callback @property def parameters(self) -> dict[str, Any]: return { "type": "object", "properties": { "question": { "type": "string", "description": "The question to ask the user", }, "options": { "type": "array", "items": {"type": "string"}, "description": "Optional list of choices for the user", }, }, "required": ["question"], } async def execute(self, **kwargs: Any) -> dict: """Ask the user a question and wait for their reply. Args: question: The question to ask. options: Optional list of choices. Returns: Dict with "reply" key containing the user's response. """ question = kwargs.get("question", "") options = kwargs.get("options") if self._pending_replies is None or self._ask_callback is None: # Not in Chat mode — return a default response logger.warning("AskHumanTool called outside Chat mode, returning default response") default = options[0] if options else "confirmed" return {"reply": default} request_id = str(uuid.uuid4())[:8] # Create and register future BEFORE calling callback so the # callback (or any concurrent task) can resolve it immediately. loop = asyncio.get_event_loop() future = loop.create_future() self._pending_replies[request_id] = future # Push question to client await self._ask_callback(request_id, question, options) try: reply = await asyncio.wait_for(future, timeout=self._timeout) return {"reply": str(reply)} except asyncio.TimeoutError: logger.warning(f"AskHumanTool timeout for request {request_id}") default = options[0] if options else "timeout — no response received" return {"reply": default} finally: self._pending_replies.pop(request_id, None)