"""``AskUserQuestion`` from inside a turn: answered or timed out.""" from __future__ import annotations import asyncio import contextlib from typing import TYPE_CHECKING, Any, cast from uuid import uuid4 from beaver_gateway.conversations.rewinding import Rewinding from beaver_gateway.conversations.state import Question if TYPE_CHECKING: from beaver_gateway.storage.models import Conversation __all__ = ["Questions"] class Questions(Rewinding): async def ask(self, key: str, payload: dict[str, Any]) -> str | None: """Show the question and wait; ``None`` when nobody answered in time.""" conv = await self.get(key) if conv is None: return None runner = self._runners.get(cast("int", conv.id)) question_id = f"q_{uuid4().hex[:10]}" pending = Question( conversation_id=key, turn_id=runner.turn_id if runner is not None else None, questions=list(payload.get("questions") or []), answer=asyncio.get_running_loop().create_future(), ) self._questions[question_id] = pending await self._set_pending_question(conv, value=True) self._bus.publish( "question", conversation_id=key, turn_id=pending.turn_id, question_id=question_id, questions=pending.questions, timeout=self._question_timeout, ) try: async with asyncio.timeout(self._question_timeout): answer = await pending.answer except TimeoutError: self._bus.publish( "question.timeout", conversation_id=key, turn_id=pending.turn_id, question_id=question_id, ) return None finally: self._questions.pop(question_id, None) await self._set_pending_question(conv, value=False) self._bus.publish( "question.answered", conversation_id=key, turn_id=pending.turn_id, question_id=question_id, answer=answer, ) return answer def answer(self, question_id: str, answer: str) -> bool: pending = self._questions.get(question_id) if pending is None or pending.answer.done(): return False pending.answer.set_result(answer) return True def answer_text(self, answer: str | None) -> str: if answer is None: return self._texts.unanswered.format( minutes=round(self._question_timeout / 60) ) return self._texts.answered.format(answer=answer) async def _set_pending_question(self, conv: Conversation, *, value: bool) -> None: async def apply(row: Conversation) -> None: row.pending_question = value with contextlib.suppress(LookupError): await self._update(conv, apply)