"""Putting words into a conversation: a message, an inject, ``say``, ``schedule``.""" from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, cast from beaver_gateway.conversations.turns import Turns if TYPE_CHECKING: from datetime import datetime from beaver_gateway.conversations.injects import Priority from beaver_gateway.storage.models import Conversation, InjectQueueItem __all__ = ["Messaging"] _log = logging.getLogger(__name__) class Messaging(Turns): async def post( self, conv: Conversation, text: str, *, origin: str = "user", attachments: list[dict[str, Any]] | None = None, ) -> InjectQueueItem: item = await self._queue.push( conversation_id=cast("int", conv.id), priority="user", origin=origin, text=text, attachments=attachments, ) await self.touch_user(conv) self._bus.publish( "message.queued", conversation_id=conv.external_id, item=item.id, origin=origin, ) self._ensure_worker(cast("int", conv.id)) return item async def inject( self, conv: Conversation, text: str, *, urgency: Priority = "normal", origin: str = "system", interrupt: bool = True, ) -> InjectQueueItem: if conv.kind == "master" and conv.status != "open": live = await self.open_master() if live is None: _log.error( "inject (%s) for closed master %s: no open master, it stays there", origin, conv.external_id, ) else: _log.info( "inject (%s) for closed master %s goes to %s", origin, conv.external_id, live.external_id, ) conv = live item = await self._queue.push( conversation_id=cast("int", conv.id), priority=urgency, origin=origin, text=text, ) self._bus.publish( "inject.queued", conversation_id=conv.external_id, item=item.id, priority=urgency, origin=origin, ) if urgency == "urgent" and interrupt: backend = self._backend(conv.agent_name) if await backend.interrupt(conv.external_id): _log.info( "conversation %s: interrupted for urgent inject", conv.external_id ) await self._queue.mark_interrupting(item) self._ensure_worker(cast("int", conv.id)) return item async def interrupt(self, conv: Conversation) -> bool: """Cut the running turn; the reply so far still lands.""" try: backend = self._backend(conv.agent_name) except LookupError: return False return await backend.interrupt(conv.external_id) async def say(self, conv: Conversation, text: str) -> dict[str, Any]: runner = self._runners.get(cast("int", conv.id)) _log.info("say[%s]: %s", conv.external_id, text[:200]) return self._bus.publish( "say", conversation_id=conv.external_id, text=text, turn_id=runner.turn_id if runner is not None else None, ) async def schedule( self, conv: Conversation, at: str, text: str, *, urgency: Priority = "wake", dedupe_key: str | None = None, ) -> tuple[int | None, datetime]: if self.scheduler is None: msg = "no scheduler; `schedule` is unavailable" raise RuntimeError(msg) return await self.scheduler.schedule( conv, at, text, urgency=urgency, dedupe_key=dedupe_key ) async def schedules(self, conv: Conversation | None = None) -> list[dict[str, Any]]: return await self.scheduler.scheduled(conv) if self.scheduler else []