from __future__ import annotations import asyncio import logging from yuxi.channel.extensions.workplace.outbound import WorkplaceOutbound logger = logging.getLogger(__name__) class WorkplaceStreaming: streaming_mode: str = "block" block_streaming_enabled: bool = True block_streaming_break: str = "text_end" block_streaming_chunk_min_chars: int = 200 block_streaming_chunk_max_chars: int = 2000 block_streaming_chunk_break_preference: str = "paragraph" block_streaming_coalesce_defaults: dict = {"min_chars": 200, "max_chars": 2000, "idle_ms": 1000} def __init__(self, outbound: WorkplaceOutbound | None = None): self._outbound = outbound async def send_block_stream( self, target_id: str, full_text: str, *, thread_id: str | None = None, account_id: str | None = None, ) -> list[str]: chunks = self._chunk_text(full_text) outbound = self._outbound or WorkplaceOutbound() message_ids: list[str] = [] for i, chunk in enumerate(chunks): if not chunk.strip(): continue results = await outbound.send_text(target_id, chunk, thread_id=thread_id, account_id=account_id) for r in results: msg_id = r.get("message_id") if msg_id: message_ids.append(msg_id) if i < len(chunks) - 1: await asyncio.sleep(0.3) return message_ids def create_draft_stream_session(self, **kwargs) -> object | None: return None def _chunk_text(self, text: str) -> list[str]: if not text: return [""] if len(text) <= self.block_streaming_chunk_max_chars: return [text] chunks: list[str] = [] remaining = text while remaining: if len(remaining) <= self.block_streaming_chunk_max_chars: chunks.append(remaining) break cutoff = self.block_streaming_chunk_max_chars chunk_text = remaining[:cutoff] para_break = chunk_text.rfind("\n\n") if para_break > self.block_streaming_chunk_min_chars: cutoff = para_break + 2 else: line_break = chunk_text.rfind("\n") if line_break > self.block_streaming_chunk_min_chars: cutoff = line_break + 1 else: sentence_break = max( chunk_text.rfind("。"), chunk_text.rfind(". "), chunk_text.rfind("!"), chunk_text.rfind("?"), ) if sentence_break > self.block_streaming_chunk_min_chars: cutoff = sentence_break + 1 chunks.append(remaining[:cutoff].strip()) remaining = remaining[cutoff:].strip() return chunks