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

99 lines
2.6 KiB
Python

from __future__ import annotations
import re
from dataclasses import dataclass
from typing import TypedDict
_FENCE_LINE_RE = re.compile(r"^( {0,3})(`{3,}|~{3,})(.*)$")
class _OpenFence(TypedDict):
start: int
marker_char: str
marker_len: int
open_line: str
marker: str
indent: str
@dataclass
class FenceSpan:
start: int
end: int
open_line: str
marker: str
indent: str
def parse_fence_spans(buffer: str) -> list[FenceSpan]:
spans: list[FenceSpan] = []
open_fence: _OpenFence | None = None
offset = 0
while offset <= len(buffer):
next_newline = buffer.find("\n", offset)
line_end = next_newline if next_newline != -1 else len(buffer)
line = buffer[offset:line_end]
match = _FENCE_LINE_RE.match(line)
if match:
indent = match.group(1)
marker = match.group(2)
marker_char = marker[0]
marker_len = len(marker)
if open_fence is None:
open_fence = {
"start": offset,
"marker_char": marker_char,
"marker_len": marker_len,
"open_line": line,
"marker": marker,
"indent": indent,
}
elif open_fence["marker_char"] == marker_char and marker_len >= open_fence["marker_len"]:
spans.append(
FenceSpan(
start=open_fence["start"],
end=line_end,
open_line=open_fence["open_line"],
marker=open_fence["marker"],
indent=open_fence["indent"],
)
)
open_fence = None
if next_newline == -1:
break
offset = next_newline + 1
if open_fence is not None:
spans.append(
FenceSpan(
start=open_fence["start"],
end=len(buffer),
open_line=open_fence["open_line"],
marker=open_fence["marker"],
indent=open_fence["indent"],
)
)
return spans
def find_fence_span_at(spans: list[FenceSpan], index: int) -> FenceSpan | None:
low, high = 0, len(spans) - 1
while low <= high:
mid = (low + high) // 2
span = spans[mid]
if index < span.start:
high = mid - 1
elif index > span.end:
low = mid + 1
else:
return span
return None
def is_safe_fence_break(spans: list[FenceSpan], index: int) -> bool:
return find_fence_span_at(spans, index) is None