ForcePilot/backend/package/yuxi/channel/streaming/directives.py

114 lines
3.5 KiB
Python
Raw Normal View History

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