ForcePilot/backend/package/yuxi/channel/streaming/strategy_selector.py
Kris 1a7690aaa0 feat(streaming): 新增流式处理完整模块
新增了完整的流式响应处理模块,包含代码围栏解析、分块策略、指令处理、流式草稿循环等核心能力,支持按段落/句子等规则切割响应块,支持进度更新与指令回调,完善流式交互的全流程支持
2026-05-21 10:35:07 +08:00

127 lines
4.6 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from yuxi.channel.capabilities import ChannelCapabilities
STREAMING_MODE_CARD_KIT = "card_kit"
STREAMING_MODE_BLOCK = "block"
STREAMING_MODE_C2C_STREAM_API = "c2c_stream_api"
STREAMING_MODE_RAW = "raw"
STREAMING_MODE_PREVIEW = "preview"
_MODE_TO_STRATEGY: dict[str, str] = {
"card_kit": STREAMING_MODE_CARD_KIT,
"block": STREAMING_MODE_BLOCK,
"c2c_stream_api": STREAMING_MODE_C2C_STREAM_API,
"raw": STREAMING_MODE_RAW,
}
@dataclass
class StreamingStrategy:
mode: str
preview_throttle_ms: int = 160
preview_min_initial_chars: int = 18
block_chunk_min_chars: int = 800
block_chunk_max_chars: int = 1200
block_chunk_break_preference: str = "paragraph"
block_coalesce_min_chars: int | None = None
block_coalesce_max_chars: int | None = None
block_coalesce_idle_ms: int = 1000
@classmethod
def from_capabilities(
cls,
caps: ChannelCapabilities,
overrides: dict | None = None,
) -> StreamingStrategy:
mode = _MODE_TO_STRATEGY.get(caps.streaming_mode or "", STREAMING_MODE_PREVIEW)
strategy = cls(
mode=mode,
preview_throttle_ms=caps.preview_stream_throttle_ms,
preview_min_initial_chars=caps.preview_min_initial_chars,
block_chunk_min_chars=caps.block_streaming_chunk_min_chars,
block_chunk_max_chars=caps.block_streaming_chunk_max_chars,
block_chunk_break_preference=caps.block_streaming_chunk_break_preference,
block_coalesce_min_chars=caps.block_streaming_coalesce_min_chars,
block_coalesce_max_chars=caps.block_streaming_coalesce_max_chars,
block_coalesce_idle_ms=caps.block_streaming_coalesce_idle_ms,
)
if overrides:
for k, v in overrides.items():
if hasattr(strategy, k) and v is not None:
setattr(strategy, k, v)
return strategy
@property
def is_card_kit(self) -> bool:
return self.mode == STREAMING_MODE_CARD_KIT
@property
def is_block(self) -> bool:
return self.mode == STREAMING_MODE_BLOCK
@property
def is_raw(self) -> bool:
return self.mode == STREAMING_MODE_RAW
@property
def is_c2c_stream_api(self) -> bool:
return self.mode == STREAMING_MODE_C2C_STREAM_API
@property
def is_preview(self) -> bool:
return self.mode == STREAMING_MODE_PREVIEW
def to_dict(self) -> dict:
result = {"mode": self.mode, "preview_throttle_ms": self.preview_throttle_ms}
if self.preview_min_initial_chars != 18:
result["preview_min_initial_chars"] = self.preview_min_initial_chars
if self.mode in (STREAMING_MODE_BLOCK, STREAMING_MODE_CARD_KIT):
result.update({
"block_chunk_min_chars": self.block_chunk_min_chars,
"block_chunk_max_chars": self.block_chunk_max_chars,
"block_chunk_break_preference": self.block_chunk_break_preference,
"block_coalesce_idle_ms": self.block_coalesce_idle_ms,
})
if self.block_coalesce_min_chars is not None:
result["block_coalesce_min_chars"] = self.block_coalesce_min_chars
if self.block_coalesce_max_chars is not None:
result["block_coalesce_max_chars"] = self.block_coalesce_max_chars
return result
class StreamingStrategySelector:
@staticmethod
def select(capabilities: ChannelCapabilities) -> StreamingStrategy:
return StreamingStrategy.from_capabilities(capabilities)
@staticmethod
def supports_preview(capabilities: ChannelCapabilities) -> bool:
return capabilities.streaming and (
capabilities.streaming_mode is None or capabilities.streaming_mode == STREAMING_MODE_RAW
)
@staticmethod
def supports_block(capabilities: ChannelCapabilities) -> bool:
return bool(capabilities.block_streaming) and (
capabilities.streaming_mode == STREAMING_MODE_BLOCK
or (capabilities.streaming_mode is None and capabilities.block_streaming)
)
@staticmethod
def supports_card_kit(capabilities: ChannelCapabilities) -> bool:
return capabilities.streaming_mode == STREAMING_MODE_CARD_KIT
@staticmethod
def supports_c2c_stream(capabilities: ChannelCapabilities) -> bool:
return capabilities.streaming_mode == STREAMING_MODE_C2C_STREAM_API
@staticmethod
def get_config(capabilities: ChannelCapabilities) -> dict:
return StreamingStrategy.from_capabilities(capabilities).to_dict()