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

114 lines
3.5 KiB
Python

from __future__ import annotations
import re
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from enum import StrEnum
DEFAULT_SILENT_TOKEN = "🔇"
_REPLY_TARGET_RE = re.compile(r"\[\[([^\[\]]+)\]\]")
_MEDIA_RE = re.compile(r"MEDIA:(\S+)")
_MULTI_SPACE_RE = re.compile(r" {2,}")
class DirectiveType(StrEnum):
REPLY_TARGET = "reply_target"
MEDIA = "media"
SILENT = "silent"
@dataclass
class StreamingDirective:
type: DirectiveType
value: str
raw: str
index: int = 0
DirectiveHandler = Callable[[StreamingDirective], Awaitable[None]]
class StreamingDirectiveParser:
def __init__(self, silent_token: str = DEFAULT_SILENT_TOKEN):
self._silent_re = re.compile(re.escape(silent_token))
self._emitted: set[tuple[str, str]] = set()
def feed(self, text: str) -> tuple[str, list[StreamingDirective]]:
"""Parse directives from a single text chunk.
Designed for incremental chunk-by-chunk processing in streaming scenarios.
Each call scans only the provided *text* (a stream delta), not the
accumulated full content. Directives are deduplicated across calls by
(type, value) — the same directive value of the same type is emitted
at most once per parser lifetime.
"""
directives = self._find_all(text)
new_directives: list[StreamingDirective] = []
for d in directives:
key = (d.type.value, d.value)
if key not in self._emitted:
self._emitted.add(key)
new_directives.append(d)
clean = self._remove_directives(text, directives)
clean = _MULTI_SPACE_RE.sub(" ", clean).strip()
return clean, new_directives
def reset(self) -> None:
self._emitted.clear()
@property
def emitted_count(self) -> int:
return len(self._emitted)
def _find_all(self, text: str) -> list[StreamingDirective]:
directives: list[StreamingDirective] = []
for match in _REPLY_TARGET_RE.finditer(text):
value = match.group(1).strip()
if value:
directives.append(
StreamingDirective(
type=DirectiveType.REPLY_TARGET,
value=value,
raw=match.group(0),
index=match.start(),
)
)
for match in _MEDIA_RE.finditer(text):
directives.append(
StreamingDirective(
type=DirectiveType.MEDIA,
value=match.group(1),
raw=match.group(0),
index=match.start(),
)
)
for match in self._silent_re.finditer(text):
directives.append(
StreamingDirective(
type=DirectiveType.SILENT,
value="",
raw=match.group(0),
index=match.start(),
)
)
return directives
@staticmethod
def _remove_directives(text: str, directives: list[StreamingDirective]) -> str:
if not directives:
return text
result = text
for d in sorted(directives, key=lambda x: -x.index):
if d.index < 0 or d.index > len(result):
continue
if d.index + len(d.raw) > len(result):
continue
result = result[: d.index] + result[d.index + len(d.raw):]
return result