Files
beaver-gateway/src/beaver_gateway/conversations/questions.py
T

86 lines
2.9 KiB
Python

"""``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)