mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 04:31:58 +00:00
M1 复核修正(第 2 轮):去掉依据之后按真正挨上的两个字判粘连——两个只装着依据的括号连着写、数字包着粗体时,两边的数不再并成一个;依据标记的「]」不当数字;一片里只补一个空格
渲染(evidence._tidy):
- 上一轮判粘连只看原文里紧挨着括号的两个字(往后只越过 [[see:]])。两个只装着依据的括号连着写(「[[v:Q1.r0.销售额]]
([[see:Q1]])([[see:m:gmv]])2 月」)、数字包着粗体(「**[[v:…]]**([[see:Q1]])2 月」,组装文档时星号又被去掉)
时,去掉之后真正挨上的是两边的数,读者看到「4,919.52 月」「212212 笔」,compose_doc、verify_doc 都不报。现在先把依据
标记和只装着依据的括号(套着的、几个连着写的都算)认成渲染出来什么都没有的一段(_voids),连着写的几个并成一组,外面的
停顿标点、空白照原规矩收拾;所有切口去掉之后,再往两边越过去掉的字、依据、粗体斜体的星号和下划线,找真正挨上的两个看得
见的字,会读成一串的补回一个空格(_glue)
- 依据标记的「]」越过去、不当数字:「华南居首。[[see:Q1]]([[see:Q2]])2 月」不再多出空格,「[[see:Q1]]([[see:Q1]])
2 月」不再以空格开头
- 同一片里最多补一个空格:「212,[[see:Q1]],[[see:m:gmv]],5」→「212, 5」(上一轮是两个空格);挨上的是写作者自己留着的
空白的,不补
- 半角标点后面补空格并进同一条规则:只在下文是数字、字母或渲染出字的标记时补(「a,[[see:Q1]],b」→「a, b」),下文是中文
的照写作者原样
- 流式渲染把末尾的星号、下划线也攒到后面的字来了再放,和整篇渲染判得一样
- 粗体把括号包在里面时(「**[[v:…]]([[see:Q1]])**2 月」)空格落在收尾的星号前面:前端行内 Markdown 照样认这对星号,
组装出的文档去掉了包着标记的星号,读者看到的一样;挪到星号外面得先知道这对星号从哪开始,流式渲染的一块里不一定有。
测试里用严格的 xfail 写明这条已知限制
测试:两个括号连着写(全角半角混用、套着的)、粗体在括号前、后、里面、下划线和单个星号、依据的「]」、一片只补一个的用例;
结构化组合(数、依据、只装依据的括号、空白、星号、标点)逐份查写作者隔开了的两边没有挨上、没多出连着的两个空格,逐字和
每 4 个字分块渲染都等于整篇渲染;审批质询、核查 agent、反方审稿、双盲复算各补一个端到端用例。11 处改动逐一去掉实现,
相关测试都变红。另在测试之外抽查:结构化组合 15.9 万份和基线 58a6759 逐份比数字串,只剩复核说可以不管的「),5」这类半角
标点紧贴数字的写法;随机 8000 份不含依据的文档,内容哈希、渲染结果和基线逐份一致;随机 1.6 万份含依据的文档流式和整篇
一致。
This commit is contained in:
+144
-87
@@ -1818,23 +1818,31 @@ _OPEN, _CLOSE = frozenset("(("), frozenset("))")
|
||||
#: 依据和括号、依据和依据之间的分隔标点(「([[see:Q3]],原报告…)」「[[see:Q1]]、[[see:Q2]]」)
|
||||
_SEPS = ",、;,;::"
|
||||
_SEP = frozenset(_SEPS)
|
||||
#: 其中的半角标点:按排版习惯后面隔一个空格。两个夹着依据的去掉后一个时,前一个不能直接贴着下文(「212,5」读成一个数)
|
||||
_HALF = frozenset(",;:")
|
||||
#: 其中的停顿标点(不含冒号):依据挂在它后面、紧接着就到句末时,它悬在句末(「华南居首,。」)。冒号后面另起一行
|
||||
#: 常常是它引出的表格、列表,不动
|
||||
_PAUSE = _SEP - frozenset("::")
|
||||
#: 句末标点:停顿标点悬在它前面时去掉(还有全文结尾)。半角句点不算:后面跟着数字时它是小数点。换行不算:列表项末尾
|
||||
#: 的「;」、软换行处连着两个分句的「,」是写作者自己的,依据挂在它后面正合「挂在句末」的规矩
|
||||
_ENDING = frozenset("。!?!?")
|
||||
#: 去掉一对括号之后会粘成一串的字:前面是数字、字母(或者标记的结尾、百分号、半角的逗号和小数点),后面是数字、字母
|
||||
#: (或者一个渲染出字的标记)。中文和数字之间不加空格也读得对,不算
|
||||
_NUMBERISH = frozenset("]%‰.,")
|
||||
#: 依据周围、渲染后会挤在一起的字:空白和分隔标点
|
||||
_GAP = frozenset(_BLANK) | _SEP
|
||||
#: 连着写的几个依据:中间只隔着空白、分隔标点
|
||||
_SEE_GROUP = re.compile(_SEE_MARK + r"(?:[" + _BLANK + _SEPS + r"]*" + _SEE_MARK + r")*")
|
||||
#: 其中只隔着空白的一段(两边都是字时,依据之间的分隔标点可能就是分句的标点,按它断开各算各的)
|
||||
_SEE_RUN = re.compile(_SEE_MARK + r"(?:[" + _BLANK + r"]*" + _SEE_MARK + r")*")
|
||||
#: 去掉依据周围的字之后,挨上的两个字会读成一串(_glue):左边是数字、字母,或者这几个——标记的结尾(渲染出一个数)、
|
||||
#: 百分号、小数点、半角标点(「212,5」读成一个数;半角标点按排版习惯后面本来就隔一个空格);右边是数字、字母,或者
|
||||
#: 一个渲染出字的标记。中文和数字之间不加空格也读得对,不算
|
||||
_STICKY = frozenset("]%‰.,;:")
|
||||
#: 渲染出来什么都没有的东西(依据标记、只装着依据的括号)在遮盖文本里换成这个字,位置和原文一一对应(_voids)
|
||||
_VOID = "\x00"
|
||||
#: 只装着依据的括号:里面除了依据(和已经认出来的、套在里面的这种括号)只有空白、分隔标点。头一个依据前面只认空白、
|
||||
#: 标点,免得一长串依据后面没有右括号时来回试
|
||||
_VOID_BRACKET = re.compile("[((][" + _BLANK + _SEPS + "]*" + _VOID + "[" + _BLANK + _SEPS + _VOID + "]*[))]")
|
||||
#: 连着写的几个:中间只隔着空白、分隔标点
|
||||
_VOID_GROUP = re.compile(_VOID + "+(?:[" + _BLANK + _SEPS + "]*" + _VOID + "+)*")
|
||||
#: 其中只隔着空白的一段(两边都是字时,中间的分隔标点可能就是分句的标点,按它断开各算各的)
|
||||
_VOID_RUN = re.compile(_VOID + "+(?:[" + _BLANK + "]*" + _VOID + "+)*")
|
||||
#: 遮盖文本里看得见的一段:一组里夹着的空白、标点,括号连同里面的空白、标点
|
||||
_SOLID = re.compile("[^" + _VOID + "]+")
|
||||
#: 粗体、斜体的星号和粗体的两个下划线:前端按 Markdown 渲染,不显示(组装文档时包着标记的粗体也去掉了星号)
|
||||
_EMPHASIS = re.compile(r"\*+|__+")
|
||||
|
||||
|
||||
def _rendered_unit(marker: dict[str, Any], catalog: dict[str, Any], snaps: _Snapshots,
|
||||
@@ -1852,13 +1860,13 @@ def _tidy(text: str, catalog: dict[str, Any], snaps: _Snapshots, cells_allowed:
|
||||
- 数字标记后面重复写的单位去掉。[[m:visits]] 渲染出来已经带单位(12,345人次),写作者常常
|
||||
还跟着写一遍([[m:visits]] 人次),读者看到的是「12,345人次 人次」。单字的单位后面紧跟着字时
|
||||
不动:写作者写的可能是另一个单位(口径卡是「人」,正文写「人次」),硬删会把不一致藏起来;
|
||||
- [[see:…]] 渲染成空,它周围留下的空白、括号、标点去掉(_see_cuts):「最高月份 [[see:Q2]]。」会剩下
|
||||
「最高月份 。」,「结果一致([[see:Q3]],原报告…)」会剩下「结果一致(,原报告…)」。去掉之后两边的数字、字母
|
||||
会粘成一串的,换成一个空格(_glued)。
|
||||
- [[see:…]] 渲染成空,它周围留下的空白、括号、标点去掉(_voids、_see_cuts):「最高月份 [[see:Q2]]。」会剩下
|
||||
「最高月份 。」,「结果一致([[see:Q3]],原报告…)」会剩下「结果一致(,原报告…)」。去掉之后两边挨上的字会读成
|
||||
一串的,补回一个空格(_glue)。
|
||||
|
||||
prev 是这段文字前面那个字(流式渲染分块时用),判断空白是不是在行首。
|
||||
"""
|
||||
cuts: list[tuple[int, ...]] = []
|
||||
cuts: list[tuple[int, int]] = []
|
||||
for marker in parse_markers(text):
|
||||
if marker["kind"] not in _UNIT_KINDS:
|
||||
continue
|
||||
@@ -1870,48 +1878,67 @@ def _tidy(text: str, catalog: dict[str, Any], snaps: _Snapshots, cells_allowed:
|
||||
after = text[hit.end():hit.end() + 1]
|
||||
if len(unit) > 1 or not after.isalnum():
|
||||
cuts.append((marker["end"], hit.end()))
|
||||
for group in _SEE_GROUP.finditer(text):
|
||||
cuts.extend(_see_cuts(text, group.start(), group.end(), prev))
|
||||
if not cuts:
|
||||
see: list[tuple[int, int]] = []
|
||||
spaces: set[int] = set()
|
||||
if "see" in text: # 没有依据标记就没有要收拾的(流式渲染的每一块都走这里,先挡掉)
|
||||
masked, see = _voids(text)
|
||||
for group in _VOID_GROUP.finditer(masked):
|
||||
see += _see_cuts(text, masked, group.start(), group.end(), prev)
|
||||
spaces = _glue(text, masked, see, prev)
|
||||
if not cuts and not see:
|
||||
return text
|
||||
# 几处要去掉的字可能重叠(依据前面的空白、它和括号之间的标点):按并集去掉。带着替换的(第三项,去掉之后两边会
|
||||
# 粘在一起,换成一个空格)连成一片时整片换成它
|
||||
merged: list[list[Any]] = []
|
||||
for a, b, *rep in sorted(cuts):
|
||||
if merged and a < merged[-1][1]:
|
||||
# 几处要去掉的字可能重叠、相接(依据前面的空白、它和括号之间的标点):按并集去掉,要补空格的那一片换成一个空格
|
||||
merged: list[list[int]] = []
|
||||
for a, b in sorted(cuts + see):
|
||||
if merged and a <= merged[-1][1]:
|
||||
merged[-1][1] = max(merged[-1][1], b)
|
||||
merged[-1][2] = merged[-1][2] or "".join(rep)
|
||||
else:
|
||||
merged.append([a, b, "".join(rep)])
|
||||
merged.append([a, b])
|
||||
out, last = [], 0
|
||||
for a, b, rep in merged:
|
||||
out += [text[last:a], rep]
|
||||
for a, b in merged:
|
||||
out += [text[last:a], " " if any(a <= x < b for x in spaces) else ""]
|
||||
last = b
|
||||
out.append(text[last:])
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _see_cuts(text: str, start: int, end: int, prev: str, *, whole: bool = False) -> list[tuple[int, ...]]:
|
||||
"""一组依据(_SEE_GROUP,text[start:end])渲染成空之后,周围要一起去掉的字。标记本身留着:组装文档时句子靠它挂
|
||||
依据(unit.see),渲染时它本来就不显示。
|
||||
def _voids(text: str) -> tuple[str, list[tuple[int, int]]]:
|
||||
"""渲染出来什么都没有的东西:依据标记,和只装着依据的括号(里面可以隔着空白、分隔标点,套着的也算)。返回遮盖文本
|
||||
(这些地方换成 _VOID,位置和原文一一对应)和这种括号连同里面的空白、标点:要一起去掉的字(「合计 6,635.68
|
||||
([[see:Q1]])。」不剩「()」)。依据标记本身留着:组装文档时句子靠它挂依据(unit.see),渲染时它本来就不显示。
|
||||
|
||||
- 括号里只有依据的(「合计 6,635.68([[see:Q1]])。」),连括号一起去掉;去掉之后整对括号当一个依据,外面照下面
|
||||
几条再收拾一遍(「华南居首,([[see:Q1]])。」的逗号)。括号两边的数字、字母会粘成一串的,括号换成一个空格
|
||||
(「4,919.5([[see:Q1]])2 月」不剩「4,919.52 月」,_glued)
|
||||
- 依据在括号开头(「([[see:Q3]],原报告…)」):它和括号之间、它后面的标点和空白;在括号末尾(「(同 SQL,
|
||||
认出来的括号和依据一样当一个不显示的东西:连着写的几个(「([[see:Q1]])([[see:m:gmv]])」「[[see:Q1]]
|
||||
([[see:Q2]])」)由 _VOID_GROUP 并成一组,外面照 _see_cuts 收拾一遍(「华南居首,([[see:Q1]])。」的逗号)。"""
|
||||
chars = list(text.replace(_VOID, "\x01"))
|
||||
for m in MARKER_RE.finditer(text):
|
||||
if m.group("kind") == "see":
|
||||
chars[m.start():m.end()] = _VOID * (m.end() - m.start())
|
||||
masked, cuts = "".join(chars), []
|
||||
while hits := list(_VOID_BRACKET.finditer(masked)):
|
||||
# 一轮认出最里面的一层:套在外面的括号下一轮才只剩依据
|
||||
for hit in hits:
|
||||
cuts += [(s.start(), s.end()) for s in _SOLID.finditer(masked, hit.start(), hit.end())]
|
||||
chars[hit.start():hit.end()] = _VOID * (hit.end() - hit.start())
|
||||
masked = "".join(chars)
|
||||
return masked, cuts
|
||||
|
||||
|
||||
def _see_cuts(text: str, masked: str, start: int, end: int, prev: str) -> list[tuple[int, int]]:
|
||||
"""一组不显示的东西(_VOID_GROUP:依据、只装着依据的括号,连着写的几个,masked[start:end])渲染成空之后,周围要
|
||||
一起去掉的字:
|
||||
|
||||
- 在括号开头(「([[see:Q3]],原报告…)」):它和括号之间、它后面的标点和空白;在括号末尾(「(同 SQL,
|
||||
[[see:Q2]])」)同理
|
||||
- 后面就到句末(句末标点、全文结尾),依据前面、几个依据之间有停顿标点的(「华南居首,[[see:Q1]]。」):
|
||||
这些停顿标点(前面是冒号的留着:它引出的可能是下一行的表格、列表);没有停顿标点、到句末只隔着空白的,空白去掉
|
||||
(「华南居首 [[see:Q1]] 。」)
|
||||
- 以上几种里,几个依据之间的空白、分隔标点一并去掉(「([[see:Q1]]、[[see:Q2]])」)
|
||||
- 两边都是字的,依据之间的分隔标点可能就是分句的标点:按它断开,每段照 _run_cuts
|
||||
- 后面就到句末(句末标点、全文结尾),前面、中间有停顿标点的(「华南居首,[[see:Q1]]。」「华南居首,
|
||||
([[see:Q1]])。」):这些停顿标点(前面是冒号的留着:它引出的可能是下一行的表格、列表);没有停顿标点、到句末
|
||||
只隔着空白的,空白去掉(「华南居首 [[see:Q1]] 。」)
|
||||
- 以上几种里,中间的空白、分隔标点一并去掉(「(同 SQL;[[see:Q1]]、[[see:Q2]])」)
|
||||
- 两边都是字的,中间的分隔标点可能就是分句的标点:按它断开,每段照 _run_cuts
|
||||
|
||||
不含依据的括号、标点一个字不动。要看的只有依据前后连着的空白、括号、分隔标点,外加再往外的一个字:流式渲染把
|
||||
这些字攒到后面的字来了才放(_TRAILING_SEE),分块渲染和整篇渲染判得一样。prev 是这段文字前面那个字。
|
||||
whole:text[start:end] 是一对只装着依据的括号(括号本身已经记了去掉),当一个依据看它外面。
|
||||
不含依据的括号、标点一个字不动。要看的只有这组前后连着的空白、分隔标点,外加再往外的一个字:流式渲染把这些字攒到
|
||||
后面的字来了才放(_TRAILING_SEE),分块渲染和整篇渲染判得一样。prev 是这段文字前面那个字。
|
||||
"""
|
||||
marks = [] if whole else list(MARKER_RE.finditer(text, start, end))
|
||||
gaps = [(m.end(), n.start()) for m, n in zip(marks, marks[1:]) if m.end() < n.start()]
|
||||
gaps = [(s.start(), s.end()) for s in _SOLID.finditer(masked, start, end)]
|
||||
lo, hi = start, end
|
||||
while lo > 0 and text[lo - 1] in _GAP:
|
||||
lo -= 1
|
||||
@@ -1920,16 +1947,12 @@ def _see_cuts(text: str, start: int, end: int, prev: str, *, whole: bool = False
|
||||
opener = text[lo - 1] if lo > 0 else prev
|
||||
closer = text[hi] if hi < len(text) else ""
|
||||
left, right = text[lo:start], text[end:hi]
|
||||
if lo > 0 and opener in _OPEN and closer in _CLOSE:
|
||||
# 括号里只有依据:连括号一起去掉
|
||||
glue = " " if _glued(text, lo - 1, hi + 1, prev) else ""
|
||||
return [(lo - 1, start), *gaps, (end, hi + 1, glue), *_see_cuts(text, lo - 1, hi + 1, prev, whole=True)]
|
||||
if opener in _OPEN:
|
||||
return [(lo, start), *gaps, (end, hi)]
|
||||
if closer in _CLOSE:
|
||||
# 和括号之间只有空白的,照依据前的空白处理(行首的缩进不动)
|
||||
return [*gaps, (end, hi), *([(lo, start)] if _SEP & set(left) else _blank_cut(text, start, end, prev))]
|
||||
tail: list[tuple[int, ...]] = []
|
||||
tail: list[tuple[int, int]] = []
|
||||
if closer in _ENDING or closer == "":
|
||||
if _PAUSE & set(left + "".join(text[a:b] for a, b in gaps)):
|
||||
# 依据后面、句末前面的停顿标点是写作者自己的(「增长 [[see:Q1]],」),不是依据留下的,不动;只剩空白的去掉
|
||||
@@ -1937,41 +1960,19 @@ def _see_cuts(text: str, start: int, end: int, prev: str, *, whole: bool = False
|
||||
*([(lo, start)] if _PAUSE & set(left) else _blank_cut(text, start, end, prev))]
|
||||
if not _SEP & set(right):
|
||||
tail = [(end, hi)]
|
||||
runs = [(start, end)] if whole else [(run.start(), run.end()) for run in _SEE_RUN.finditer(text, start, end)]
|
||||
return [*tail, *(cut for a, b in runs for cut in _run_cuts(text, a, b, prev))]
|
||||
return [*tail, *(cut for run in _VOID_RUN.finditer(masked, start, end)
|
||||
for cut in _run_cuts(text, masked, run.start(), run.end(), prev))]
|
||||
|
||||
|
||||
def _glued(text: str, start: int, end: int, prev: str) -> bool:
|
||||
"""去掉 text[start:end](一对只装着依据的括号)之后,两边的字会不会粘成一串:前面是数字、字母(或者标记的结尾、
|
||||
百分号、半角的逗号和小数点,_NUMBERISH),后面紧跟着数字、字母,或者一个渲染出字的标记。「[[v:Q1.r0.销售额]]
|
||||
([[see:Q1]])2 月」去掉括号读成「4,919.52 月」,是另一个数,而且哪项核对都查不出来。括号前有空白的不会粘:
|
||||
空白照 _blank_cut 留着。紧跟在括号后面的依据也不显示,越过它看下一个字(流式渲染把它和括号一起攒着)。"""
|
||||
before = text[start - 1] if start > 0 else prev
|
||||
if not (_wordish(before) or before in _NUMBERISH):
|
||||
return False
|
||||
while (marker := MARKER_RE.match(text, end)) is not None and marker.group("kind") == "see":
|
||||
end = marker.end()
|
||||
after = text[end:end + 1]
|
||||
if after == "[":
|
||||
return MARKER_RE.match(text, end) is not None
|
||||
return _wordish(after)
|
||||
def _run_cuts(text: str, masked: str, start: int, end: int, prev: str) -> list[tuple[int, int]]:
|
||||
"""两边都是字的一段(_VOID_RUN,中间只隔着空白):
|
||||
|
||||
|
||||
def _wordish(ch: str) -> bool:
|
||||
"""数字(全角的也算)或者半角字母:两个挨在一起读成一个数、一个词。"""
|
||||
return ch.isdecimal() or (ch.isascii() and ch.isalnum())
|
||||
|
||||
|
||||
def _run_cuts(text: str, start: int, end: int, prev: str) -> list[tuple[int, ...]]:
|
||||
"""两边都是字的一段依据(_SEE_RUN,中间只隔着空白;或者一对只装着依据的括号):
|
||||
|
||||
- 几个依据之间的空白,和依据前面的空白(_blank_cut)
|
||||
- 中间的空白,和前面的空白(_blank_cut)
|
||||
- 夹在两个分隔标点中间(「结果一致,[[see:Q1]],原报告…」):去掉后面那个(连同它前面的空白,后面的空白留着
|
||||
隔开下文:「a, [[see:Q1]], b」→「a, b」)。前面那个是半角标点、后面紧跟着字的,换成一个空格:「212,[[see:Q1]],5」
|
||||
不剩「212,5」
|
||||
隔开下文:「a, [[see:Q1]], b」→「a, b」)。去掉之后前一个贴着下文、读成一串的(「212,[[see:Q1]],5」),_glue
|
||||
补回一个空格
|
||||
"""
|
||||
marks = list(MARKER_RE.finditer(text, start, end))
|
||||
cuts: list[tuple[int, ...]] = [(m.end(), n.start()) for m, n in zip(marks, marks[1:]) if m.end() < n.start()]
|
||||
cuts = [(s.start(), s.end()) for s in _SOLID.finditer(masked, start, end)]
|
||||
cuts += _blank_cut(text, start, end, prev)
|
||||
lo, hi = start, end
|
||||
while lo > 0 and text[lo - 1] in _GAP:
|
||||
@@ -1980,10 +1981,7 @@ def _run_cuts(text: str, start: int, end: int, prev: str) -> list[tuple[int, ...
|
||||
hi += 1
|
||||
left, right = text[lo:start], text[end:hi]
|
||||
if _SEP & set(left) and _SEP & set(right):
|
||||
cut = end + max(i for i, ch in enumerate(right) if ch in _SEP) + 1
|
||||
after = text[cut:cut + 1]
|
||||
spaced = left.rstrip(_BLANK)[-1] in _HALF and after not in ("", "\n") and after not in _BLANK
|
||||
cuts.append((end, cut, " " if spaced else ""))
|
||||
cuts.append((end, end + max(i for i, ch in enumerate(right) if ch in _SEP) + 1))
|
||||
return cuts
|
||||
|
||||
|
||||
@@ -2000,6 +1998,65 @@ def _blank_cut(text: str, start: int, end: int, prev: str) -> list[tuple[int, in
|
||||
return []
|
||||
|
||||
|
||||
def _glue(text: str, masked: str, see: list[tuple[int, int]], prev: str) -> set[int]:
|
||||
"""依据周围的字(see)去掉之后,两边真正挨上的两个看得见的字会读成一串的(_STICKY):返回这一片里头一个去掉的字
|
||||
的位置,_tidy 在那里补回一个空格。「[[v:Q1.r0.销售额]]([[see:Q1]])2 月」去掉括号读成「4,919.52 月」,是另一个
|
||||
数,而且哪项核对都查不出来。
|
||||
|
||||
往两边找挨上的字时越过一切不显示的:去掉的字、依据标记、粗体斜体的星号和下划线。两个括号连着写(「([[see:Q1]])
|
||||
([[see:m:gmv]])」)、数字包着粗体(「**[[v:…]]**([[see:Q1]])」)都照挨上的字判;依据标记的「]」越过去,不当
|
||||
数字(「华南居首。[[see:Q1]]([[see:Q2]])2 月」不补)。一片里只补一个(「212,[[see:Q1]],[[see:m:gmv]],5」→
|
||||
「212, 5」);挨上的是写作者留着的空白的,不补。
|
||||
|
||||
空格补在头一个去掉的字那里。粗体把括号包在里面时(「**[[v:…]]([[see:Q1]])**2 月」)它落在收尾的星号前面:
|
||||
前端的行内 Markdown 照样认这对星号,组装文档时包着标记的星号也去掉了,读者看到的一样。要挪到星号外面得先知道
|
||||
这对星号从哪开始,流式渲染的一块里不一定有。
|
||||
|
||||
越过的这些字(空白、括号、分隔标点、依据、星号、下划线)流式渲染都攒到后面的字来了才放(_TRAILING_SEE):一片总
|
||||
落在同一块里,往左越出这一块就是 prev(它不会是这些字),分块渲染和整篇渲染判得一样。"""
|
||||
gone = bytearray(len(text))
|
||||
for a, b in see:
|
||||
gone[a:b] = b"\x01" * (b - a)
|
||||
|
||||
def hidden(i: int) -> bool:
|
||||
return bool(gone[i]) or masked[i] == _VOID
|
||||
|
||||
spaces: set[int] = set()
|
||||
seen: set[tuple[int, int]] = set()
|
||||
for run in re.finditer(rb"\x01+", gone):
|
||||
p, q = run.start(), run.end()
|
||||
while p > 0:
|
||||
if hidden(p - 1):
|
||||
p -= 1
|
||||
elif text[p - 1] == "*" or (p > 1 and text[p - 2:p] == "__"):
|
||||
mark = text[p - 1]
|
||||
while p > 0 and text[p - 1] == mark:
|
||||
p -= 1
|
||||
else:
|
||||
break
|
||||
while q < len(text):
|
||||
if hidden(q):
|
||||
q += 1
|
||||
elif emphasis := _EMPHASIS.match(text, q):
|
||||
q = emphasis.end()
|
||||
else:
|
||||
break
|
||||
if (p, q) in seen:
|
||||
continue
|
||||
seen.add((p, q))
|
||||
left = text[p - 1] if p > 0 else prev
|
||||
right = text[q:q + 1]
|
||||
sticks = (right == "[" and MARKER_RE.match(text, q) is not None) or _wordish(right)
|
||||
if sticks and (_wordish(left) or left in _STICKY):
|
||||
spaces.add(run.start())
|
||||
return spaces
|
||||
|
||||
|
||||
def _wordish(ch: str) -> bool:
|
||||
"""数字(全角的也算)或者半角字母:两个挨在一起读成一个数、一个词。"""
|
||||
return ch.isdecimal() or (ch.isascii() and ch.isalnum())
|
||||
|
||||
|
||||
def _render(text: str, catalog: dict[str, Any], snaps: _Snapshots, cells_allowed: bool, prev: str = "") -> str:
|
||||
text = _tidy(_expand_tables(text, catalog, snaps, cells_allowed, prev), catalog, snaps, cells_allowed, prev)
|
||||
out: list[str] = []
|
||||
@@ -2023,9 +2080,9 @@ def render_markers(text: str, catalog: dict[str, Any], *, cells_allowed: bool =
|
||||
# 流式渲染:让 llm.token 里出现的就是最终的数字
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
#: 文字末尾的空白、括号、分隔标点和 [[see:…]]:流式渲染先攒着,等后面的字来了再一起交给 _tidy(依据周围的这些字
|
||||
#: 去不去掉,要看再往后的那个字,见 _see_cuts)
|
||||
_TRAILING_SEE = re.compile(r"(?:[" + _BLANK + "(())" + _SEPS + r"]|" + _SEE_MARK + r")+$")
|
||||
#: 文字末尾的空白、括号、分隔标点、星号、下划线和 [[see:…]]:流式渲染先攒着,等后面的字来了再一起交给 _tidy(依据
|
||||
#: 周围的这些字去不去掉、去掉之后补不补空格,要看再往后的那个字,见 _see_cuts、_glue)
|
||||
_TRAILING_SEE = re.compile(r"(?:[" + _BLANK + "(())" + _SEPS + "*_" + r"]|" + _SEE_MARK + r")+$")
|
||||
|
||||
|
||||
def _safe_cut(text: str, has_unit: Callable[[dict[str, Any]], bool] | None = None) -> int:
|
||||
@@ -2036,9 +2093,9 @@ def _safe_cut(text: str, has_unit: Callable[[dict[str, Any]], bool] | None = Non
|
||||
开头可能是另一个 [。
|
||||
|
||||
整表标记闭合了也先不放:它展开时要看紧跟在后面的那个字(是不是换行),那个字还没到。
|
||||
_tidy 要往后看的也先不放:末尾的空白、括号、分隔标点和 [[see:…]](后面来的字决定它们去不去掉),
|
||||
以及后面还没攒够 UNIT_LOOKAHEAD 个字的数字标记(后面可能是重复的单位)。has_unit 判断一个
|
||||
标记渲染出来带不带单位,不带的(运行输入里的周次、没有单位的格)不用攒;不给就都攒。
|
||||
_tidy 要往后看的也先不放:末尾的空白、括号、分隔标点、星号、下划线和 [[see:…]](后面来的字决定它们去不
|
||||
去掉、要不要补空格),以及后面还没攒够 UNIT_LOOKAHEAD 个字的数字标记(后面可能是重复的单位)。has_unit 判断
|
||||
一个标记渲染出来带不带单位,不带的(运行输入里的周次、没有单位的格)不用攒;不给就都攒。
|
||||
"""
|
||||
cut = len(text)
|
||||
start = text.rfind("[[")
|
||||
|
||||
@@ -51,6 +51,21 @@ async def test_see_in_brackets_does_not_break_the_rationale(monkeypatch, shop):
|
||||
assert second["verdict"] == "refuted" and issuance["tier"] == "degraded"
|
||||
|
||||
|
||||
async def test_see_brackets_written_together_do_not_glue_two_numbers(monkeypatch, shop):
|
||||
"""两个只装着依据的括号连着写、数字包着粗体(复核第 2 轮):去掉之后两个数不能挨成一个(「9001,200」),句末前悬着的
|
||||
逗号也去掉。"""
|
||||
plays = {
|
||||
"华南销售额超过华东": Play([NET], "refuted", "扣除退款后华南、华东分别为 **[[v:Q1.r1.净销售额]]**([[see:Q1]])"
|
||||
"([[see:Q1]])[[v:Q1.r0.净销售额]],华南仍低于华东,([[see:Q1]])([[see:Q1]])。"),
|
||||
"居各区域之首": Play([REFUNDS], "stands", "华东退款 [[v:Q1.r0.退款笔数]] 笔,扣除后仍居首。[[see:Q1]]"),
|
||||
}
|
||||
record, _ = await _record(monkeypatch, shop, plays)
|
||||
refuted = next(claim for claim in record["claims"] if claim["verdict"] == "refuted")
|
||||
assert refuted["rationale"] == "扣除退款后华南、华东分别为 900 1,200,华南仍低于华东。"
|
||||
assert refuted["rationale_doc"]["markdown"] == refuted["rationale"]
|
||||
assert refuted["rationale_doc"]["violations"] == []
|
||||
|
||||
|
||||
async def test_a_placeholder_rationale_is_recorded_as_no_rationale(monkeypatch, shop):
|
||||
"""反方交回的理由只是占位词:记录里理由为空(审批卡据此写「反方没有给出理由」),判「被推翻」的照常不予采信。"""
|
||||
plays = {"华南销售额超过华东": Play([NET], "refuted", "placeholder"),
|
||||
|
||||
@@ -36,6 +36,16 @@ async def test_see_in_brackets_does_not_break_the_blind_rationale(monkeypatch, s
|
||||
assert doc["markdown"] == mine["rationale"] and doc["violations"] == []
|
||||
|
||||
|
||||
async def test_see_brackets_written_together_do_not_glue_the_number(monkeypatch, shop):
|
||||
"""两个只装着依据的括号连着写、数字包着粗体(复核第 2 轮):去掉之后数字不能和后面的字挨成一个数(「3302 月」),
|
||||
句末前悬着的逗号也去掉。"""
|
||||
mine = await _blind_input(monkeypatch, shop, "结果是 **{ref}**([[see:Q1]])([[see:Q1]])2 月起按支付日统计,"
|
||||
"已排除退款,([[see:Q1]])([[see:Q1]])。")
|
||||
assert mine["rationale"] == "结果是 330 2 月起按支付日统计,已排除退款。"
|
||||
doc = mine["rationale_doc"]
|
||||
assert doc["markdown"] == mine["rationale"] and doc["violations"] == []
|
||||
|
||||
|
||||
async def test_a_placeholder_rationale_is_not_shown_as_the_explanation(monkeypatch, shop):
|
||||
mine = await _blind_input(monkeypatch, shop, "placeholder")
|
||||
assert mine["value"] == 330.0, "数照样采用"
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
[[v:Q1.r1.金额合计]]([[see:Q1]])」,渲染后剩下「(,原报告…)」「()」,正式运行里随封存永久留着。
|
||||
|
||||
- 括号(全角、半角,一对里混用也算)里只有依据的,连括号一起去掉;去掉之后整对括号当一个依据,外面照下面几条再收拾
|
||||
一遍(括号前悬着的停顿标点、括号前后的空白)
|
||||
- 去掉括号之后两边的数字、字母会粘成一串的(「4,919.5([[see:Q1]])2 月」),括号换成一个空格:粘起来读成另一个数,
|
||||
哪项核对都查不出来;半角逗号夹着依据、去掉后一个之后同理(「212,[[see:Q1]],5」→「212, 5」)
|
||||
一遍(括号前悬着的停顿标点、括号前后的空白)。连着写的几个(「([[see:Q1]])([[see:m:gmv]])」)并成一组收拾
|
||||
- 去掉之后两边真正挨上的两个字会粘成一串的(「4,919.5([[see:Q1]])2 月」),补回一个空格:粘起来读成另一个数,
|
||||
哪项核对都查不出来。找挨上的字时越过一切不显示的:依据、只装着依据的括号、粗体斜体的星号和下划线;依据标记的「]」
|
||||
不当数字。一片里最多补一个,挨上的是写作者留着的空白的不补;半角逗号夹着依据、去掉后一个之后同理
|
||||
(「212,[[see:Q1]],5」→「212, 5」)
|
||||
- 依据在括号开头、后面跟着「,」「、」「;」「:」的,标点一起去掉;在括号末尾、前面紧挨着标点的同理
|
||||
- 依据夹在两个分隔标点中间的,去掉后一个;后面就到句末(句末标点、全文结尾)、前面或几个依据之间有「,」「、」
|
||||
「;」的,去掉这些标点,到句末只隔着空白的去掉空白;依据后面、句末前面的标点是写作者自己的,不动
|
||||
@@ -187,6 +189,132 @@ def test_a_glued_number_would_have_slipped_through_the_checks(catalog):
|
||||
assert [s["alias"] for s in unit["see"]] == ["Q1"]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 粘不粘看去掉之后真正挨上的两个字:越过依据、只装着依据的括号、粗体斜体的星号和下划线(复核第 2 轮)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
#: 读者看到的字:前端按 Markdown 渲染,星号、两个下划线不显示
|
||||
_EMPHASIS = re.compile(r"\*+|__+")
|
||||
|
||||
|
||||
def _visible(text: str) -> str:
|
||||
return _EMPHASIS.sub("", text)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("raw", "shown"), [
|
||||
# 两个只装着依据的括号连着写(全角半角混用、套着的也一样):中间什么都不剩,两边会粘的照样隔开
|
||||
("华东 [[v:Q1.r0.销售额]]([[see:Q1]])([[see:m:gmv]])2 月起持续下滑。", "华东 4,919.5 2 月起持续下滑。"),
|
||||
("订单 [[v:Q1.r0.订单数]]([[see:Q1]])([[see:m:gmv]])[[v:Q1.r0.订单数]] 笔。", "订单 212 212 笔。"),
|
||||
("增幅 [[m:yoy]]([[see:m:yoy]])([[see:Q1]])3 个区域。", "增幅 12.5% 3 个区域。"),
|
||||
("增幅 [[m:yoy]]([[see:m:yoy]])([[see:Q1]])3 个区域。", "增幅 12.5% 3 个区域。"),
|
||||
("增幅 [[m:yoy]]([[see:m:yoy]])(([[see:Q1]]))3 个区域。", "增幅 12.5% 3 个区域。"),
|
||||
("合计 [[v:Q1.r0.销售额]]([[see:Q1]]) ([[see:m:gmv]])2 月", "合计 4,919.5 2 月"),
|
||||
# 依据和括号连着写、几个依据之间隔着空白:去掉之后两边会粘的,同样隔开
|
||||
("合计 [[v:Q1.r0.销售额]][[see:Q1]]([[see:m:gmv]])2 月。", "合计 4,919.5 2 月。"),
|
||||
("合计 [[v:Q1.r0.销售额]][[see:Q1]] [[see:m:gmv]]2 月。", "合计 4,919.5 2 月。"),
|
||||
# 不会粘的照旧:中文和数字之间
|
||||
("华南居首([[see:Q1]])([[see:m:gmv]])2 月起回升。", "华南居首2 月起回升。"),
|
||||
])
|
||||
def test_see_only_brackets_written_together_do_not_glue_numbers(catalog, raw, shown):
|
||||
assert render(raw, catalog) == shown
|
||||
assert compose(raw, catalog)["markdown"] == shown
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("raw", "shown"), [
|
||||
# 连着写的几个只装着依据的括号当一组收拾:括号前悬着的停顿标点、中间的标点一并去掉
|
||||
("华南居首,([[see:Q1]])([[see:m:gmv]])。", "华南居首。"),
|
||||
("华南居首,[[see:Q1]]([[see:m:gmv]])。", "华南居首。"),
|
||||
("华南居首,([[see:Q1]])、([[see:m:gmv]])。", "华南居首。"),
|
||||
("结果一致([[see:Q1]])([[see:m:gmv]]),原报告未过滤。", "结果一致,原报告未过滤。"),
|
||||
("结果一致([[see:Q1]])([[see:m:gmv]],原报告未过滤)。", "结果一致(原报告未过滤)。"),
|
||||
])
|
||||
def test_see_only_brackets_written_together_are_tidied_as_one(catalog, raw, shown):
|
||||
assert render(raw, catalog) == shown
|
||||
assert compose(raw, catalog)["markdown"] == shown
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("raw", "shown"), [
|
||||
# 粗体在括号前、后、把括号包在里面:星号不显示,挨上的是两边的数
|
||||
("销售额 **[[v:Q1.r0.销售额]]**([[see:Q1]])2 月起持续下滑。", "销售额 4,919.5 2 月起持续下滑。"),
|
||||
("华东 **[[v:Q1.r0.销售额]]**([[see:Q1]])[[v:Q1.r0.订单数]] 笔。", "华东 4,919.5 212 笔。"),
|
||||
("华东 [[v:Q1.r0.销售额]]([[see:Q1]])**[[v:Q1.r0.订单数]] 笔**", "华东 4,919.5 212 笔"),
|
||||
("**[[v:Q1.r0.销售额]]([[see:Q1]])**2 月", "4,919.5 2 月"),
|
||||
("华东 **[[v:Q1.r0.销售额]]**([[see:Q1]])([[see:m:gmv]])**[[v:Q1.r0.订单数]]** 笔", "华东 4,919.5 212 笔"),
|
||||
("__[[v:Q1.r0.销售额]]__([[see:Q1]])2 月", "4,919.5 2 月"),
|
||||
("*[[v:Q1.r0.销售额]]*([[see:Q1]])2 月", "4,919.5 2 月"),
|
||||
# 依据前的空白:后面紧跟着的是星号,越过它看
|
||||
("合计 [[v:Q1.r0.销售额]] [[see:Q1]]**2 月**起", "合计 4,919.5 2 月起"),
|
||||
# 不会粘的照旧
|
||||
("合计 **[[v:Q1.r0.销售额]]**([[see:Q1]])元", "合计 4,919.5元"),
|
||||
])
|
||||
def test_bold_around_a_number_does_not_hide_the_glue(catalog, raw, shown):
|
||||
assert _visible(render(raw, catalog)) == shown
|
||||
assert _visible(compose(raw, catalog)["markdown"]) == shown
|
||||
|
||||
|
||||
def test_bold_still_wraps_the_number_after_the_bracket_is_dropped(catalog):
|
||||
doc = compose("销售额 **[[v:Q1.r0.销售额]]**([[see:Q1]])2 月起持续下滑。", catalog)
|
||||
[number] = [seg for _, unit in iter_units(doc) for seg in unit["segments"] if seg.get("ref")]
|
||||
assert number["text"] == "4,919.5" and number.get("strong") is True
|
||||
assert render("销售额 **[[v:Q1.r0.销售额]]**([[see:Q1]])2 月", catalog) == "销售额 **4,919.5** 2 月"
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason="已知限制:粗体把括号包在里面时,补的空格落在收尾的星号前面(「**4,919.5 **2 月」)。"
|
||||
"前端的行内 Markdown 照样认这对星号,组装出的文档去掉了包着标记的星号,读者看到的一样;"
|
||||
"挪到星号外面得先知道这对星号从哪开始,流式渲染的一块里不一定有")
|
||||
def test_the_space_lands_outside_a_bold_that_wraps_the_bracket(catalog):
|
||||
assert render("销售额 **[[v:Q1.r0.销售额]]([[see:Q1]])**2 月", catalog) == "销售额 **4,919.5** 2 月"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("raw", "shown"), [
|
||||
# 依据标记的「]」不显示,不当数字:左边真正挨上的是句号、文首
|
||||
("华南居首。[[see:Q1]]([[see:m:gmv]])2 月起回升。", "华南居首。2 月起回升。"),
|
||||
("[[see:Q1]]([[see:Q1]])2 月", "2 月"),
|
||||
("华南居首。[[see:Q1]] ([[see:m:gmv]])2 月起回升。", "华南居首。2 月起回升。"), # 中间的空白同几个依据之间的
|
||||
# 一片里最多补一个空格;挨上的本来就隔着空白的不补
|
||||
("212,[[see:Q1]],[[see:m:gmv]],5 个区域", "212, 5 个区域"),
|
||||
("212,([[see:Q1]]),[[see:m:gmv]],5 个区域", "212, 5 个区域"),
|
||||
("合计 [[v:Q1.r0.销售额]]([[see:Q1]])([[see:m:gmv]]) 2 月", "合计 4,919.5 2 月"),
|
||||
])
|
||||
def test_a_space_is_put_back_once_and_only_where_it_sticks(catalog, raw, shown):
|
||||
assert render(raw, catalog) == shown
|
||||
assert compose(raw, catalog)["markdown"] == shown
|
||||
|
||||
|
||||
#: 结构化组合:左边一个渲染出数的标记(可能包着粗体),中间 1–3 段依据、只装着依据的括号、空白、星号,右边以数字、
|
||||
#: 字母或者渲染出数的标记开头
|
||||
_LEFTS = ["合计 [[v:Q1.r0.销售额]]", "合计 **[[v:Q1.r0.销售额]]**", "合计 __[[v:Q1.r0.销售额]]__", "增幅 [[m:yoy]]",
|
||||
"编号 Q1"]
|
||||
_MIDDLES = ["([[see:Q1]])", "([[see:m:gmv]])", "(([[see:Q1]]))", "[[see:Q1]]", "**", " ", ","]
|
||||
_RIGHTS = ["2 月", "[[v:Q1.r0.订单数]] 笔", "**[[v:Q1.r0.订单数]] 笔**", "x 区"]
|
||||
#: 写作者原本就隔开了两边的中间段:括号(去掉依据之后还看得见)、空白、标点
|
||||
_SEPARATING = {"([[see:Q1]])", "([[see:m:gmv]])", "(([[see:Q1]]))", " ", ","}
|
||||
|
||||
|
||||
def _combos() -> list[tuple[str, str, str]]:
|
||||
middles = [[m] for m in _MIDDLES]
|
||||
middles += [[a, b] for a in _MIDDLES for b in _MIDDLES]
|
||||
middles += [[a, b, c] for a in _MIDDLES for b in _MIDDLES for c in _MIDDLES]
|
||||
return [(left, "".join(middle), right) for left in _LEFTS for right in _RIGHTS for middle in middles
|
||||
if any(m in _SEPARATING for m in middle)]
|
||||
|
||||
|
||||
#: 左边那个数渲染出来的字,后面紧跟着数字、字母,或者半角的逗号、句点再跟数字:读成了另一个数、另一个词
|
||||
_GLUED = re.compile(r"(?:4,919\.5|12\.5%|Q1)(?=[\dA-Za-z]|[,.]\d)")
|
||||
|
||||
|
||||
def test_a_separator_the_writer_wrote_never_disappears_between_two_numbers(catalog):
|
||||
"""写作者原本隔开了的两边(括号、空白、标点),收拾依据之后不能挨成一串;也不会多出两个连着的空格。"""
|
||||
bad = []
|
||||
for left, middle, right in _combos():
|
||||
raw = f"{left}{middle}{right}。"
|
||||
shown = _visible(render(raw, catalog))
|
||||
writer = _visible(re.sub(r"\[\[see:[^\]]*\]\]", "", raw))
|
||||
if _GLUED.search(shown) or (" " in shown and " " not in writer):
|
||||
bad.append((raw, shown))
|
||||
assert not bad, bad[:10]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("raw", "shown"), [
|
||||
("华南居首。[[see:Q1]]", "华南居首。"),
|
||||
("华南居首 [[see:Q1]],华东第二。", "华南居首,华东第二。"),
|
||||
@@ -299,6 +427,12 @@ def _stream(catalog, raw: str, sizes: list[int]) -> list[str]:
|
||||
"华东 [[v:Q1.r0.销售额]]([[see:Q1]])2 月,合计 [[v:Q1.r0.销售额]]([[see:Q1]])[[see:m:gmv]][[v:Q1.r0.订单数]] 笔"
|
||||
",212,[[see:Q1]],5 个",
|
||||
"华南居首,([[see:Q1]])。华南居首 [[see:Q1]] 。1. 华东居首;[[see:Q1]]\n2. 华南第二,([[see:Q1]]),华东第二",
|
||||
# 粗体、连着写的括号:往两边找挨上的字要越过星号、下划线,流式渲染得把它们和依据一起攒着
|
||||
"销售额 **[[v:Q1.r0.销售额]]**([[see:Q1]])2 月,华东 [[v:Q1.r0.销售额]]([[see:Q1]])([[see:m:gmv]])"
|
||||
"**[[v:Q1.r0.订单数]] 笔**",
|
||||
"华南居首。[[see:Q1]]([[see:m:gmv]])2 月,212,[[see:Q1]],[[see:m:gmv]],5 个,合计 [[v:Q1.r0.销售额]] [[see:Q1]]"
|
||||
"__2 月__",
|
||||
"**[[v:Q1.r0.销售额]]([[see:Q1]])**2 月;华南居首,([[see:Q1]])([[see:m:gmv]])。order_([[see:Q1]])_x",
|
||||
])
|
||||
@pytest.mark.parametrize("size", range(1, 9))
|
||||
def test_stream_tidies_see_in_brackets_like_the_full_render(catalog, raw, size):
|
||||
@@ -310,7 +444,8 @@ def test_stream_tidies_see_in_brackets_like_the_full_render(catalog, raw, size):
|
||||
def _corpus(n: int) -> list[str]:
|
||||
rng = random.Random(20261006)
|
||||
pieces = ["[[see:Q1]]", "[[see:m:gmv]]", "[[v:Q1.r0.销售额]]", "[[m:gmv]]", "(", ")", "(", ")", ",", "、",
|
||||
";", ":", ",", ";", " ", " ", "。", "\n", "文字", "元", "[[", "]]", "[", "2", "x", "%", "!"]
|
||||
";", ":", ",", ";", " ", " ", "。", "\n", "文字", "元", "[[", "]]", "[", "2", "x", "%", "!",
|
||||
"**", "*", "__", "_", "([[see:Q1]])"]
|
||||
return ["".join(rng.choice(pieces) for _ in range(rng.randint(1, 18))) for _ in range(n)]
|
||||
|
||||
|
||||
@@ -321,3 +456,13 @@ def test_stream_matches_the_full_render_on_random_brackets(catalog, raw):
|
||||
for _ in range(3):
|
||||
sizes = [rng.randint(1, 12) for _ in range(len(raw))]
|
||||
assert "".join(_stream(catalog, raw, sizes)) == full
|
||||
|
||||
|
||||
def test_stream_matches_the_full_render_on_the_structured_combos(catalog):
|
||||
"""结构化组合(数、依据、只装着依据的括号、空白、星号、下划线)逐字、每 4 个字一块地送进去,拼起来和整篇渲染一样。"""
|
||||
bad = []
|
||||
for left, middle, right in _combos():
|
||||
raw = f"{left}{middle}{right}。"
|
||||
full = render(raw, catalog)
|
||||
bad += [(raw, out) for size in (1, 4) if "".join(out := _stream(catalog, raw, [size] * len(raw))) != full]
|
||||
assert not bad, bad[:5]
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core import artifact_store
|
||||
from app.engine import verify_agent
|
||||
from app.engine.inquiry import NO_ANSWER
|
||||
@@ -38,6 +40,11 @@ from tests.test_verify_rationale_rules import slow_final
|
||||
BRACKETED = ("仍居各区域之首([[see:Q1]],已排除测试账号):华东为 [[v:Q1.r0.销售额]]([[see:Q1]]),"
|
||||
"华南为 [[v:Q1.r1.销售额]](同口径,[[see:Q1]])。")
|
||||
BRACKETED_TEXT = "仍居各区域之首(已排除测试账号):华东为 1,200,华南为 900(同口径)。"
|
||||
#: 两个只装着依据的括号连着写、数字包着粗体(复核第 2 轮):去掉括号之后两个数不能挨成一个(「1,200900」),句末前悬着的
|
||||
#: 逗号也去掉
|
||||
TOGETHER = ("华东、华南分别为 **[[v:Q1.r0.销售额]]**([[see:Q1]])([[see:Q1]])[[v:Q1.r1.销售额]],"
|
||||
"华东仍居各区域之首,([[see:Q1]])([[see:Q1]])。")
|
||||
TOGETHER_TEXT = "华东、华南分别为 1,200 900,华东仍居各区域之首。"
|
||||
|
||||
|
||||
async def _asked(client, shop) -> tuple[dict, list]:
|
||||
@@ -48,15 +55,17 @@ async def _asked(client, shop) -> tuple[dict, list]:
|
||||
return r.json()["inquiry"], await events(paused.id, "approval.inquiry")
|
||||
|
||||
|
||||
async def test_see_in_brackets_does_not_break_the_answer(monkeypatch, shop, client):
|
||||
Models(monkeypatch, shop, plays={"排除测试账号": Play([NO_TEST], BRACKETED)})
|
||||
@pytest.mark.parametrize(("said", "shown"), [(BRACKETED, BRACKETED_TEXT), (TOGETHER, TOGETHER_TEXT)],
|
||||
ids=["bracketed", "together"])
|
||||
async def test_see_in_brackets_does_not_break_the_answer(monkeypatch, shop, client, said, shown):
|
||||
Models(monkeypatch, shop, plays={"排除测试账号": Play([NO_TEST], said)})
|
||||
item, [event] = await _asked(client, shop)
|
||||
answer = item["answer"]
|
||||
assert answer["ok"] is True and answer["text"] == BRACKETED_TEXT
|
||||
assert answer["ok"] is True and answer["text"] == shown
|
||||
doc = artifact_store.load(answer["doc_artifact"])
|
||||
assert doc["markdown"] == BRACKETED_TEXT and doc["source"] == BRACKETED
|
||||
assert doc["markdown"] == shown and doc["source"] == said
|
||||
assert doc["violations"] == []
|
||||
assert event.data["answer_text"] == BRACKETED_TEXT
|
||||
assert event.data["answer_text"] == shown
|
||||
|
||||
|
||||
async def test_a_final_answer_that_runs_out_of_time_reports_the_time_limit(monkeypatch, shop, client):
|
||||
|
||||
@@ -90,6 +90,24 @@ async def test_see_written_in_brackets_renders_clean_in_the_rationale(monkeypatc
|
||||
assert out.rationale["violations"] == []
|
||||
|
||||
|
||||
async def test_see_brackets_written_together_do_not_glue_two_numbers(monkeypatch, shop):
|
||||
"""两个只装着依据的括号连着写、数字包着粗体(复核第 2 轮):去掉括号之后两个数不能挨成一个(「2303 笔」),句末前
|
||||
悬着的逗号也去掉。"""
|
||||
name, _ = shop
|
||||
said = ("未作废订单合计 **[[v:Q1.r0.total]]**([[see:Q1]])([[see:Q2]])[[v:Q2.r0.n]] 笔都已核对,口径一致,"
|
||||
"([[see:Q1]])([[see:Q2]])。")
|
||||
count = "SELECT COUNT(*) AS n FROM orders WHERE is_deleted = 0"
|
||||
Script(monkeypatch, [q(name, VALID_SUM, "c1"), q(name, count, "c2")], {**FOUND, "rationale": said})
|
||||
out = await run(name)
|
||||
shown = "未作废订单合计 230 3 笔都已核对,口径一致。"
|
||||
assert out.result["rationale"] == shown and out.rationale["markdown"] == shown
|
||||
[unit] = [unit for _, unit in iter_units(out.rationale)]
|
||||
assert [s["alias"] for s in unit["see"]] == ["Q1", "Q2", "Q1", "Q2"]
|
||||
numbers = [seg for seg in unit["segments"] if seg.get("ref")]
|
||||
assert [(seg["text"], bool(seg.get("strong"))) for seg in numbers] == [("230", True), ("3", False)]
|
||||
assert out.rationale["violations"] == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 收尾超时
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user