from __future__ import annotations import logging from yuxi.channel.domain.model.shared.channel_capabilities import ChannelCapabilities from yuxi.channel.domain.model.shared.content_chunker import chunk_content from yuxi.channel.domain.port.sse_push_port import SsePushPort logger = logging.getLogger(__name__) WEB_CAPABILITIES = ChannelCapabilities( media=True, group=False, dm=True, streaming=True, typing=False, # Web uses streaming instead of typing indicator reaction=False, thread=False, max_text_length=32768, ) class WebOutbound: def __init__(self, *, sse_push: SsePushPort | None = None) -> None: self._sse_push = sse_push async def send_text(self, session_id: str, text: str, metadata: dict | None = None) -> bool: if not self._sse_push: logger.warning("web send to %s failed: no SSE endpoint", session_id) return False chunks = chunk_content(text, WEB_CAPABILITIES.max_text_length) for chunk in chunks: ok = await self._sse_push.push_event( session_id, { "type": "assistant_message", "content": chunk, "channel_type": metadata.get("channel_type", "web") if metadata else "web", "trace_id": metadata.get("trace_id", "") if metadata else "", "message_id": metadata.get("message_id", "") if metadata else "", }, ) if not ok: return False return True async def send_typing(self, session_id: str) -> None: if self._sse_push: try: await self._sse_push.push_event(session_id, {"type": "typing", "session_id": session_id}) except Exception: pass async def send_media(self, session_id: str, *, url: str, media_type: str, metadata: dict | None = None) -> bool: if self._sse_push: try: return await self._sse_push.push_event( session_id, { "type": "media", "url": url, "media_type": media_type, "trace_id": metadata.get("trace_id", "") if metadata else "", }, ) except Exception: return False logger.warning("web send_media to %s failed: no SSE endpoint", session_id) return False