102 lines
3.0 KiB
Python
102 lines
3.0 KiB
Python
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import asyncio
|
|||
|
|
import logging
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
ZOOM_CHUNK_MAX = 3800
|
|||
|
|
ZOOM_CHUNK_MIN = 500
|
|||
|
|
ZOOM_COALESCE_IDLE_MS = 1000
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ZoomStreamSession:
|
|||
|
|
"""
|
|||
|
|
Block Streaming 会话
|
|||
|
|
|
|||
|
|
收集 Agent 流式输出的文本 Token,达到 chunk_min 或 idle_ms 时
|
|||
|
|
自动发送当前累积的文本块。
|
|||
|
|
|
|||
|
|
Zoom 的限制:
|
|||
|
|
- 单条消息最大 4096 字符 → chunk_max = 3800 (留余量)
|
|||
|
|
- 不支持消息编辑 (无法实现 Preview Streaming)
|
|||
|
|
- 使用连续发送新消息的方式模拟流式效果
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(self, send_fn, target_id: str, chunk_max: int = ZOOM_CHUNK_MAX):
|
|||
|
|
self._send = send_fn
|
|||
|
|
self._target_id = target_id
|
|||
|
|
self._chunk_max = chunk_max
|
|||
|
|
self._buffer: str = ""
|
|||
|
|
self._sent_count: int = 0
|
|||
|
|
self._finished: bool = False
|
|||
|
|
self._task: asyncio.Task | None = None
|
|||
|
|
|
|||
|
|
async def start(self, initial_text: str = "") -> None:
|
|||
|
|
if initial_text:
|
|||
|
|
self._buffer += initial_text
|
|||
|
|
if len(self._buffer) >= ZOOM_CHUNK_MIN:
|
|||
|
|
await self._flush()
|
|||
|
|
|
|||
|
|
async def feed(self, text: str) -> None:
|
|||
|
|
if self._finished or not text:
|
|||
|
|
return
|
|||
|
|
self._buffer += text
|
|||
|
|
if len(self._buffer) >= ZOOM_CHUNK_MIN:
|
|||
|
|
await self._flush()
|
|||
|
|
|
|||
|
|
async def _flush(self) -> None:
|
|||
|
|
if not self._buffer:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
chunk = self._buffer[: self._chunk_max]
|
|||
|
|
remainder = self._buffer[self._chunk_max :] if len(self._buffer) > self._chunk_max else ""
|
|||
|
|
|
|||
|
|
if self._sent_count > 0:
|
|||
|
|
prefix = "...\n"
|
|||
|
|
chunk = prefix + chunk[: self._chunk_max - len(prefix)]
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
await self._send(self._target_id, chunk)
|
|||
|
|
self._sent_count += 1
|
|||
|
|
except Exception:
|
|||
|
|
logger.exception("Zoom streaming send failed")
|
|||
|
|
|
|||
|
|
self._buffer = remainder
|
|||
|
|
|
|||
|
|
async def finish(self) -> None:
|
|||
|
|
while self._buffer:
|
|||
|
|
await self._flush()
|
|||
|
|
self._finished = True
|
|||
|
|
|
|||
|
|
async def cancel(self) -> None:
|
|||
|
|
self._buffer = ""
|
|||
|
|
self._finished = True
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def sent_count(self) -> int:
|
|||
|
|
return self._sent_count
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def is_finished(self) -> bool:
|
|||
|
|
return self._finished
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ZoomStreaming:
|
|||
|
|
streaming_mode: str = "block"
|
|||
|
|
block_streaming_enabled: bool = True
|
|||
|
|
block_streaming_chunk_min_chars: int = ZOOM_CHUNK_MIN
|
|||
|
|
block_streaming_chunk_max_chars: int = ZOOM_CHUNK_MAX
|
|||
|
|
block_streaming_chunk_break_preference: str = "paragraph"
|
|||
|
|
block_streaming_coalesce_defaults: dict | None = {
|
|||
|
|
"min_chars": ZOOM_CHUNK_MIN,
|
|||
|
|
"max_chars": ZOOM_CHUNK_MAX,
|
|||
|
|
"idle_ms": ZOOM_COALESCE_IDLE_MS,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def create_draft_stream_session(self, target_id: str, send_fn=None) -> ZoomStreamSession:
|
|||
|
|
return ZoomStreamSession(send_fn, target_id)
|
|||
|
|
|
|||
|
|
def create_block_chunker(self) -> object:
|
|||
|
|
return {"mode": "length", "min_chars": ZOOM_CHUNK_MIN, "max_chars": ZOOM_CHUNK_MAX}
|