from __future__ import annotations import os from collections.abc import AsyncIterator, Mapping import structlog from adapter.matrix.store import get_room_meta from core.chat import ChatManager from core.store import StateStore from sdk.interface import ( Attachment, MessageChunk, MessageResponse, PlatformClient, PlatformError, User, UserSettings, ) logger = structlog.get_logger(__name__) def _ws_debug_enabled() -> bool: value = os.environ.get("SURFACES_DEBUG_WS", "") return value.strip().lower() in {"1", "true", "yes", "on"} class RoutedPlatformClient(PlatformClient): def __init__( self, *, chat_mgr: ChatManager, store: StateStore, delegates: Mapping[str, PlatformClient], ) -> None: if not delegates: raise ValueError("RoutedPlatformClient requires at least one delegate") self._chat_mgr = chat_mgr self._store = store self._delegates = dict(delegates) self._default_client = next(iter(self._delegates.values())) self._prototype_state = getattr(self._default_client, "_prototype_state", None) async def get_or_create_user( self, external_id: str, platform: str, display_name: str | None = None, ) -> User: return await self._default_client.get_or_create_user( external_id=external_id, platform=platform, display_name=display_name, ) async def send_message( self, user_id: str, chat_id: str, text: str, attachments: list[Attachment] | None = None, ) -> MessageResponse: delegate, platform_chat_id = await self._resolve_delegate(user_id, chat_id) return await delegate.send_message(user_id, platform_chat_id, text, attachments) async def stream_message( self, user_id: str, chat_id: str, text: str, attachments: list[Attachment] | None = None, ) -> AsyncIterator[MessageChunk]: delegate, platform_chat_id = await self._resolve_delegate(user_id, chat_id) async for chunk in delegate.stream_message(user_id, platform_chat_id, text, attachments): yield chunk async def get_settings(self, user_id: str) -> UserSettings: return await self._default_client.get_settings(user_id) async def update_settings(self, user_id: str, action) -> None: await self._default_client.update_settings(user_id, action) async def close(self) -> None: for delegate in self._delegates.values(): close = getattr(delegate, "close", None) if callable(close): await close() async def _resolve_delegate( self, user_id: str, local_chat_id: str ) -> tuple[PlatformClient, str]: chat = await self._chat_mgr.get(local_chat_id, user_id) if chat is None: raise PlatformError( f"unknown matrix chat id: {local_chat_id}", code="MATRIX_CHAT_NOT_FOUND", ) room_meta = await get_room_meta(self._store, chat.surface_ref) if room_meta is None: raise PlatformError( f"matrix room is not bound: {chat.surface_ref}", code="MATRIX_ROOM_NOT_BOUND", ) agent_id = room_meta.get("agent_id") platform_chat_id = room_meta.get("platform_chat_id") if not agent_id or not platform_chat_id: raise PlatformError( f"matrix room routing is incomplete: {chat.surface_ref}", code="MATRIX_ROUTE_INCOMPLETE", ) delegate = self._delegates.get(str(agent_id)) if delegate is None: raise PlatformError( f"unknown matrix agent id: {agent_id}", code="MATRIX_AGENT_NOT_FOUND", ) if _ws_debug_enabled(): logger.warning( "matrix_route_resolved", user_id=user_id, local_chat_id=local_chat_id, surface_ref=chat.surface_ref, agent_id=str(agent_id), platform_chat_id=str(platform_chat_id), delegate_type=type(delegate).__name__, ) return delegate, str(platform_chat_id)