"""Gateway delivery loop for local triggers.""" from __future__ import annotations import asyncio import uuid from collections.abc import Awaitable, Callable from typing import Any from loguru import logger from nanobot.agent.automation_turns import AutomationTurnError from nanobot.bus.events import InboundMessage, OutboundMessage from nanobot.triggers.local_session_turns import LOCAL_TRIGGER_META from nanobot.triggers.local_store import LocalTriggerStore from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery from nanobot.webui.metadata import WEBUI_MESSAGE_SOURCE_METADATA_KEY, WEBUI_TURN_METADATA_KEY async def run_local_trigger_queue( *, store: LocalTriggerStore, submit_turn: Callable[[InboundMessage], Awaitable[OutboundMessage | None]] | None = None, is_channel_enabled: Callable[[str], bool], poll_interval_s: float = 0.5, batch_size: int = 20, ) -> None: """Poll local trigger deliveries and submit them as session turns.""" if submit_turn is None: raise ValueError("run_local_trigger_queue requires submit_turn") logger.info("Local trigger queue started") recovered = store.recover_processing_deliveries() if recovered: logger.warning( "Trigger: recovered {} interrupted delivery file(s) from processing", recovered, ) while True: deliveries = store.claim_deliveries(limit=batch_size) if not deliveries: await asyncio.sleep(poll_interval_s) continue for delivery in deliveries: try: await _deliver_delivery( store, delivery, submit_turn=submit_turn, is_channel_enabled=is_channel_enabled, ) store.complete_delivery(delivery) except asyncio.CancelledError as exc: store.retry_delivery(delivery, str(exc) or exc.__class__.__name__) _write_delivery_run_record( store, delivery, status="interrupted", error=str(exc) or exc.__class__.__name__, ) raise except _TerminalDeliveryError as exc: store.record_delivery( delivery.trigger_id, status="error", error=str(exc), run_at_ms=delivery.created_at_ms, ) _write_delivery_run_record( store, delivery, status="error", error=str(exc), ) store.complete_delivery(delivery) logger.warning( "Trigger: dropped delivery {} for {}: {}", delivery.id, delivery.trigger_id, exc, ) except AutomationTurnError as exc: error = str(exc) or exc.__class__.__name__ store.record_delivery( delivery.trigger_id, status="error", error=error, run_at_ms=delivery.created_at_ms, ) _write_delivery_run_record( store, delivery, status="error", error=error, ) store.complete_delivery(delivery) logger.warning( "Trigger: delivery {} for {} reached the agent but failed: {}", delivery.id, delivery.trigger_id, error, ) except Exception as exc: error = str(exc) or exc.__class__.__name__ retried = store.retry_delivery(delivery, error) _write_delivery_run_record( store, delivery, status="retrying" if retried else "error", error=error, ) store.record_delivery( delivery.trigger_id, status="error", error=error, run_at_ms=delivery.created_at_ms, ) logger.exception( "Trigger: failed delivery {} for {}{}", delivery.id, delivery.trigger_id, "; queued retry" if retried else "; moved to failed queue", ) class _TerminalDeliveryError(RuntimeError): pass async def _deliver_delivery( store: LocalTriggerStore, delivery: TriggerDelivery, *, submit_turn: Callable[[InboundMessage], Awaitable[OutboundMessage | None]], is_channel_enabled: Callable[[str], bool], ) -> None: trigger = store.get(delivery.trigger_id) if trigger is None: raise _TerminalDeliveryError("trigger not found") if not trigger.enabled: raise _TerminalDeliveryError("trigger is disabled") if not is_channel_enabled(trigger.channel): raise _TerminalDeliveryError(f"target channel is not enabled: {trigger.channel}") store.write_delivery_run_record(delivery, trigger=trigger, status="processing") msg = InboundMessage( channel=trigger.channel, sender_id=trigger.sender_id, chat_id=trigger.chat_id, content=delivery.content, metadata=_delivery_metadata(trigger, delivery), session_key_override=trigger.session_key, ) response = await submit_turn(msg) store.record_delivery( trigger.id, status="ok", run_at_ms=delivery.created_at_ms, ) _write_delivery_run_record( store, delivery, trigger=trigger, status="ok", response=response.content if response else "", ) def _write_delivery_run_record( store: LocalTriggerStore, delivery: TriggerDelivery, *, status: str, trigger: LocalTrigger | None = None, error: str | None = None, response: str | None = None, ) -> None: try: store.write_delivery_run_record( delivery, trigger=trigger, status=status, error=error, response=response, ) except Exception: logger.exception( "Trigger: failed to write run record for delivery {}", delivery.id, ) def _delivery_metadata(trigger: LocalTrigger, delivery: TriggerDelivery) -> dict[str, Any]: metadata = dict(trigger.origin_metadata or {}) metadata[LOCAL_TRIGGER_META] = { "trigger_id": trigger.id, "trigger_name": trigger.name, "delivery_id": delivery.id, "created_at_ms": delivery.created_at_ms, "persist_content": _history_content(trigger, delivery), } if trigger.channel == "websocket": metadata.pop(WEBUI_TURN_METADATA_KEY, None) metadata[WEBUI_TURN_METADATA_KEY] = f"trigger:{trigger.id}:{uuid.uuid4().hex}" source: dict[str, str] = {"kind": "local_trigger"} if trigger.name: source["label"] = trigger.name metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] = source return metadata def _history_content(trigger: LocalTrigger, delivery: TriggerDelivery) -> str: label = trigger.name.strip() if trigger.name else trigger.id return f"Local trigger received: {label}\n\n{delivery.content}"