新增了完整的流式响应处理模块,包含代码围栏解析、分块策略、指令处理、流式草稿循环等核心能力,支持按段落/句子等规则切割响应块,支持进度更新与指令回调,完善流式交互的全流程支持
114 lines
3.5 KiB
Python
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
|