mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:11:59 +00:00
出口契约认藏在代码块里的正文改为逐行标出代码:写明是文字的块不再因为一行 SQL、一行像 YAML 的键或 + / - 要点就整块当代码,公式、JSON、目录树这类像数据的行照代码看
上一轮先判整块:只要有一行以 SQL / Python 关键字或小写键开头,或者每行都以 + - 空格 @ 开头,整块就当代码,只认去掉 注释、字符串之后还成句的行。写成 markdown、text 的块里,+ / - 开头的正负面要点、正文后面另嵌的 ~~~sql、缩进的 select、 一行「- note: …」都让不带句读的结论认不出,藏进代码块反倒完整出具。反过来,写明是文字的块不去字符串,六字连写、结论词 又对公式、JSON、目录树、print、流程箭头这些行生效:结论句为 0 的报告贴一段带「占比」的公式就降档,缺口还说正文写在 代码块里。 - 逐行标出代码(_code_lines),不再整块判:块里再嵌、收得了的代码块(~~~sql、````,开了没收的那一行照普通的行看); SQL / Python 语句从开头那一行到这一段结束,续行也算;整段是 YAML 的(第一行是小写键,其余是键或缩进的行;写明是 文字的块里另要有值为空的键,一行一个「- note: 一句话」是正文里的标签);像数据、公式的行(前面有字的等号和箭头、 画目录树的制表符、JSON 的开头和「"键":」、函数调用、文件名、逗号隔开的表头)。代码的行只认去掉注释、字符串之后 成句的,别的行照旧:写明是文字的按 _written,信息串为空的按 _prose_line - diff:写明是文字的块里 + - 后面空一格的是列表项,紧挨着字写的才是 diff;信息串为空的块里第一个改动行是 + 的是 正负面要点(diff 先列删掉的行),有 @@ 块头的照旧是 diff - 一段到空行为止,只写着块注释的行不算空行;写明是文字的块里顶格的标题另起一段。块注释、三个引号括起的多行字符串 涂成空格,行数不变 - SQL 关键字只认全小写、全大写,def / class 要写成定义的样子:「From 数据看……」「class A 门店……」是正文。代码的行里 -- // 紧挨着前面的字也算注释 - 取舍写进 _fenced_body 的说明:一块里只要有一行在写正文就记缺口,拿不准的行宁可当代码,藏起来的正文总有别的行认得出 测试:复核里的写法逐一补上:+ / - 要点(markdown、text、信息串为空)、嵌 ~~~sql、缩进的 select、小写键的标签,正式 运行降档并只带这条缺口;带「占比」「增长率」的公式、写成 text 的 JSON 和目录树,在结论句为 0 的报告里照常完整出具。 单元参数另补 print、groupby、流程箭头、文件名、逗号表头、JSON 数组、多行 SQL 夹块注释、docstring、嵌套的 YAML、开了 没收和收尾更长的嵌套围栏、行首箭头、首字母大写的关键字、先列 + 的带 @@ diff,并钉住两条取舍:写成 text 又空一格的 diff 照列表看;信息串为空、整段都是小写键的照 YAML 看。长行耗时用例扩到七种长行、两种多行围栏、两种信息串。逐一去掉 36 处规则(diff、嵌套围栏、语句、关键字大小写、def / class、YAML、分段、像数据的七种写法、注释、块注释、多行字符串、 正文判法、结构片段、挂钩),各有用例变红。
This commit is contained in:
+134
-42
@@ -501,20 +501,37 @@ _CJK_RUN = re.compile(r"[\u4e00-\u9fff]{6,}")
|
||||
#: 英文的词串:单词之间只隔空格、逗号。五个以上的单词紧跟句末标点算一句(_en_sentence)
|
||||
_EN_RUN = re.compile(r"[A-Za-z]+(?:[ ,]+[A-Za-z]+)*")
|
||||
_EN_WORD = re.compile(r"[A-Za-z]+")
|
||||
#: Markdown 标题:正文里的标题切块时记为 heading,从不算结论句(evidence._scan_blocks),代码块里的也不算在写正文
|
||||
_HEADING = re.compile(r"\s*#{1,6}\s")
|
||||
#: 整行是注释(-- // # /* */ 开头;# 开头的 Markdown 标题本来也不是句子),和行尾隔着空白的注释:写着中文句子也是
|
||||
#: 代码的一部分
|
||||
#: 整行是注释(-- // # /* */ 开头),和行尾隔着空白的注释:写着中文句子也是代码的一部分。# 开头的 Markdown 标题本来
|
||||
#: 也不算结论句(evidence._scan_blocks 把它切成 heading)
|
||||
_COMMENT_LINE = re.compile(r"\s*(?:--|//|#|/\*|\*/)")
|
||||
_TRAILING_COMMENT = re.compile(r"\s(?:--|//|#).*")
|
||||
#: 看得出是代码的行里,-- // 紧挨着前面的字也是注释(SELECT SUM(amount)--按区域汇总):字符串已经先去掉了
|
||||
_CODE_COMMENT = re.compile(r"(?:--|//|\s#).*")
|
||||
#: 写明是文字的块里顶格写的 Markdown 标题:标题前后是两段(_code_lines)
|
||||
_MD_HEADING = re.compile(r"#{1,6}\s")
|
||||
#: 代码里的字符串字面量('…'、"…",可带 f、r、b、u、N 这类前缀):里面的中文标点是数据。引号前面紧挨着字母、数字的
|
||||
#: 不算开头(英文的 it's、don't)
|
||||
_STRING_LITERAL = re.compile(r"(?<![A-Za-z0-9_])[rRbBuUfFnN]{0,2}(?:'[^'\n]*'|\"[^\"\n]*\")")
|
||||
#: 看得出是代码的行:SQL、Python 的开头,YAML 的键(「columns:」「- name: 退款占比」)。YAML 的键只认小写开头的:
|
||||
#: 正文里的「Note:」「Q1:」是标签
|
||||
_CODE_LINE = re.compile(r"\s*(?:select|with|from|where|join|group\s+by|order\s+by|having|union|insert|update"
|
||||
r"|delete|create|def|class|import|return)\b", re.IGNORECASE)
|
||||
#: 可以跨行的块注释、多行字符串:开头的写法,和它对应的收尾
|
||||
_SPAN_OPEN = re.compile(r"/\*|\"\"\"|'''")
|
||||
_SPAN_CLOSE = {"/*": "*/", '"""': '"""', "'''": "'''"}
|
||||
#: SQL、Python 语句的开头。SQL 的关键字只认全小写、全大写的写法:「From 数据看……」「Update ……」这类首字母大写的是
|
||||
#: 正文里的英文词;def、class 要写成定义的样子(def 名字(、class 名字: ),「class A 门店……」说的是门店的分类
|
||||
_CODE_LINE = re.compile(r"\s*(?:(?:select|with|from|where|join|group\s+by|order\s+by|having|union|insert|update|delete"
|
||||
r"|create|import|return|SELECT|WITH|FROM|WHERE|JOIN|GROUP\s+BY|ORDER\s+BY|HAVING|UNION|INSERT"
|
||||
r"|UPDATE|DELETE|CREATE)\b|(?:def|class)\s+[A-Za-z_]\w*\s*[(:])")
|
||||
#: YAML 的键(「columns:」「- name: 退款占比」),只认小写开头的:正文里的「Note:」「Q1:」是标签。值为空、下面缩进着
|
||||
#: 列表或子键的另记一种(_YAML_NEST)
|
||||
_YAML_KEY = re.compile(r"\s*(?:-\s+)?[a-z_][\w.-]*:(?:\s|$)")
|
||||
_YAML_NEST = re.compile(r"\s*(?:-\s+)?[a-z_][\w.-]*:\s*$")
|
||||
#: 块里再嵌的代码块:~~~ 或三个以上反引号开头。compose_doc 只认整行三个反引号的围栏,那样的一行外层就收了,块里剩下的
|
||||
#: 嵌套写法是 ~~~sql、````sql 这些
|
||||
_NESTED_FENCE = re.compile(r"\s*(`{3,}|~{3,})(.*)")
|
||||
#: 像数据、公式的一行:前面有字的等号(公式、赋值、比较)和箭头(连起的流程),画目录树的制表符,JSON 的对象、数组开头
|
||||
#: ({"、[")和「"键":」,函数调用(print(、groupby(),带扩展名的文件名(周报.xlsx)。行首的「→ 」「=> 」是列表的
|
||||
#: 记号,不算
|
||||
_DATA_LINE = re.compile(r"\S\s*(?:[==→⟶⇒]|->)|[\u2500-\u257f]|^\s*[\[{]\s*[\"'\[{]|[\"']\s*:"
|
||||
r"|(?<![\w.])[A-Za-z_][\w.]*\(|(?<=[^\s.])\.[A-Za-z][A-Za-z0-9]{1,4}(?![\w.])")
|
||||
|
||||
|
||||
def _en_sentence(line: str) -> bool:
|
||||
@@ -543,41 +560,116 @@ def _concluding(line: str) -> bool:
|
||||
|
||||
|
||||
def _written(line: str) -> bool:
|
||||
"""信息串写明是文字(markdown、text 这类)、里面也看不出是代码的块里,这一行在写正文:成句、按正文的规矩会算结论,
|
||||
或者一行里连着六个以上汉字。不看列表标记:标记的写法太多(- 1. 1、 一、 (1) · ●,也常不带标记),补一种漏一种;
|
||||
也不当代码看,# -- 和半角引号都是正文里的符号。标题不算。"""
|
||||
if _HEADING.match(line):
|
||||
"""信息串写明是文字(markdown、text 这类)的块里,不是代码的这一行在写正文:成句、按正文的规矩会算结论,或者一行里
|
||||
连着六个以上汉字。不看列表标记:标记的写法太多(- + 1. 1、 一、 (1) · ●,也常不带标记),补一种漏一种;也不去
|
||||
字符串、行尾注释,# -- 和半角引号都是正文里的符号。整行是注释、标题的不算。"""
|
||||
if _COMMENT_LINE.match(line):
|
||||
return False
|
||||
return _sentence(line) or _CJK_RUN.search(line) is not None or _concluding(line)
|
||||
|
||||
|
||||
def _code_like(lines: list[str]) -> bool:
|
||||
"""块里看得出是代码:有 SQL、Python 开头的行(_CODE_LINE)、YAML 的键(_YAML_KEY),或者整块是 diff(有 + 开头的
|
||||
行也有 - 开头的行,其余是空格开头的上下文行、@@ 开头的块头)。"""
|
||||
if any(_CODE_LINE.match(line) or _YAML_KEY.match(line) for line in lines):
|
||||
return True
|
||||
heads = {line[:1] for line in lines if line.strip()}
|
||||
return {"+", "-"} <= heads <= {"+", "-", " ", "@"}
|
||||
|
||||
|
||||
def _prose_line(line: str, *, code: bool) -> bool:
|
||||
"""信息串为空、或者看得出是代码的块里,这一行在写正文:去掉字符串字面量和行尾注释之后成句;整块看不出是代码时,
|
||||
按正文的规矩会算结论的也算(不带句读的列表项、不带标记的行)。不带句读、也不像结论的不算:YAML 里的列名、diff 里
|
||||
改的口径和列表项没有分别。整行是注释的不算。"""
|
||||
"""代码的行(code),或者信息串为空的块里的行,在写正文:去掉字符串字面量和行尾注释之后成句;不是代码的行另认按正文
|
||||
的规矩会算结论的(不带句读的列表项、不带标记的行)。不带句读、也不像结论的不算:YAML 里的列名、diff 里改的口径和
|
||||
列表项没有分别。整行是注释的不算。"""
|
||||
if _COMMENT_LINE.match(line):
|
||||
return False
|
||||
line = _TRAILING_COMMENT.sub("", _STRING_LITERAL.sub(" ", line), count=1)
|
||||
line = (_CODE_COMMENT if code else _TRAILING_COMMENT).sub("", _STRING_LITERAL.sub(" ", line), count=1)
|
||||
return _sentence(line) or (not code and _concluding(line))
|
||||
|
||||
|
||||
def _drop_block_comments(text: str) -> str:
|
||||
"""去掉 /* … */(可以跨行):块注释里的中文句子也是代码的一部分。没收尾的不去,开头那一行照注释行跳过。"""
|
||||
kept: list[str] = []
|
||||
def _blank_spans(text: str) -> str:
|
||||
"""块注释(/* … */)、三个引号括起的多行字符串涂成空格,换行留着,块里还是原来那几行:里面写着中文句子也是代码的
|
||||
一部分。没收尾的不动,开头那一行照注释行跳过。"""
|
||||
out: list[str] = []
|
||||
at = 0
|
||||
while (start := text.find("/*", at)) != -1 and (end := text.find("*/", start + 2)) != -1:
|
||||
kept += [text[at:start], "\n"]
|
||||
at = end + 2
|
||||
return "".join(kept) + text[at:]
|
||||
while (start := _SPAN_OPEN.search(text, at)) is not None:
|
||||
end = text.find(_SPAN_CLOSE[start.group()], start.end())
|
||||
if end == -1:
|
||||
break
|
||||
stop = end + len(_SPAN_CLOSE[start.group()])
|
||||
out += [text[at:start.start()], "\n".join(" " * len(part) for part in text[start.start():stop].split("\n"))]
|
||||
at = stop
|
||||
return "".join(out) + text[at:]
|
||||
|
||||
|
||||
def _diff(lines: list[str], *, labelled: bool) -> bool:
|
||||
"""整块是 diff:有 - 开头的行也有 + 开头的行,其余是空格开头的上下文行、@@ 开头的块头。diff 里一处改动先列删掉的行、
|
||||
再列加上的行,第一个改动行就是 + 的,是 + / - 开头的正负面要点(有 @@ 块头的照旧算 diff)。信息串写明是文字的块里,
|
||||
+ - 后面空一格的是 Markdown 的列表项,只有紧挨着字写的(-按下单日期统计)才是 diff。"""
|
||||
heads = [line[:1] for line in lines if line.strip()]
|
||||
if not {"+", "-"} <= set(heads) <= {"+", "-", " ", "@"}:
|
||||
return False
|
||||
if labelled and any(line[:2] in ("+ ", "- ") for line in lines):
|
||||
return False
|
||||
return "@" in heads or next(h for h in heads if h in ("+", "-")) == "-"
|
||||
|
||||
|
||||
def _data_like(line: str) -> bool:
|
||||
"""像数据、公式的一行(_DATA_LINE),或者逗号隔开的一行表头、数据(两个以上半角逗号,逗号后面都不空格)。"""
|
||||
return _DATA_LINE.search(line) is not None or (line.count(",") >= 2 and ", " not in line)
|
||||
|
||||
|
||||
def _yaml(lines: list[str], *, labelled: bool) -> bool:
|
||||
"""一段是 YAML:第一行是小写开头的键,其余每一行都是键或者缩进的行(键下面的列表项、子键),注释行不看。写明是文字的
|
||||
块里另要有值为空、往下缩进的键:一行一个「键: 一句话」的(「- note: 华南销售额超过华北」)多半是正文里的标签。"""
|
||||
body = [line for line in lines if line.strip() and not _COMMENT_LINE.match(line)]
|
||||
if not body or not _YAML_KEY.match(body[0]):
|
||||
return False
|
||||
if not all(_YAML_KEY.match(line) or line[:1].isspace() for line in body[1:]):
|
||||
return False
|
||||
return not labelled or any(_YAML_NEST.match(line) for line in body)
|
||||
|
||||
|
||||
def _bare_fence(line: str) -> str:
|
||||
"""整行只写着围栏(``` 或 ~~~,不带信息串)的,返回这串围栏,能给块里再嵌的代码块收尾;别的行返回空串。"""
|
||||
fence = _NESTED_FENCE.match(line)
|
||||
return fence.group(1) if fence and not fence.group(2).strip() else ""
|
||||
|
||||
|
||||
def _code_lines(lines: list[str], raw: list[str], *, labelled: bool) -> list[bool]:
|
||||
"""逐行标出哪些是代码:块里再嵌的代码块(~~~sql),SQL、Python 的语句(从开头那一行到这一段结束,续行也算;前面的
|
||||
注释行本来就不算正文),整段是 YAML 的(_yaml),像数据、公式的行(_data_like)。
|
||||
|
||||
lines 是涂掉块注释、多行字符串之后的行,raw 是原样的行:一段到空行为止,只写着注释的行不算空行;写明是文字的块里,
|
||||
顶格的标题也另起一段。不把整块当代码:正文里另贴一段 SQL、嵌一个 ~~~sql,或者有一行像 YAML 的键,别的行照样按正文认。
|
||||
再嵌的代码块要收得了才算(后面有同一种符号、不比开头短的收尾),开了没收的那一行照普通的行看。"""
|
||||
code = [False] * len(lines)
|
||||
paragraph: list[int] = []
|
||||
fence = ""
|
||||
# 每一行后面最长的收尾围栏(整行只有 ``` 或 ~~~),按符号分开记:开了收不了的围栏不算,照普通的行看,免得后面的
|
||||
# 正文都被当成代码
|
||||
reach = {"`": [0] * (len(lines) + 1), "~": [0] * (len(lines) + 1)}
|
||||
for k in range(len(lines) - 1, -1, -1):
|
||||
for after in reach.values():
|
||||
after[k] = after[k + 1]
|
||||
if bare := _bare_fence(lines[k]):
|
||||
reach[bare[0]][k] = max(reach[bare[0]][k], len(bare))
|
||||
|
||||
def close() -> None:
|
||||
statement = _yaml([lines[k] for k in paragraph], labelled=labelled)
|
||||
for k in paragraph:
|
||||
statement = statement or _CODE_LINE.match(lines[k]) is not None
|
||||
code[k] = statement or _data_like(lines[k])
|
||||
paragraph.clear()
|
||||
|
||||
for k, line in enumerate(lines):
|
||||
if fence:
|
||||
code[k] = True
|
||||
closing = _bare_fence(line)
|
||||
if closing[:1] == fence[0] and len(closing) >= len(fence):
|
||||
fence = ""
|
||||
continue
|
||||
if (opening := _NESTED_FENCE.match(line)) and reach[opening.group(1)[0]][k + 1] >= len(opening.group(1)):
|
||||
close()
|
||||
fence, code[k] = opening.group(1), True
|
||||
continue
|
||||
if not raw[k].strip() or (labelled and _MD_HEADING.match(line)):
|
||||
close()
|
||||
continue
|
||||
paragraph.append(k)
|
||||
close()
|
||||
return code
|
||||
|
||||
|
||||
def _fenced_body(doc: dict[str, Any]) -> bool:
|
||||
@@ -588,9 +680,10 @@ def _fenced_body(doc: dict[str, Any]) -> bool:
|
||||
text 的原样交给 compose_doc:整段正文落在代码块里,切不出结论句,裁判没有句子可判、反方审稿记 no_claims,没挂依据
|
||||
的、证据相矛盾的结论句都数出 0 句,出具就成了完整出具。有一句结论的不算:正文没被整个藏起来,结论句照常核对。
|
||||
|
||||
在不在写正文逐行看:信息串写明是文字、里面也看不出是代码的按 _written。信息串为空的块里也常是 SQL、Python、YAML、
|
||||
diff,信息串写成 text 的也偶尔是代码,这些按 _prose_line:注释、字符串字面量里的中文句子不算;整块看得出是代码
|
||||
(_code_like)的只认成句的,以 * + - 开头的 SQL 续行、中文别名里的「最高」这类词都不算。"""
|
||||
在不在写正文逐行看。先标出哪些行是代码(_code_lines;信息串为空、整块是 diff 的全是代码),代码的行只认去掉注释、
|
||||
字符串之后成句的(_prose_line),中文别名、YAML 的值、公式里的「最高」「占比」这类词都不算。其余的行:信息串写明
|
||||
是文字的按 _written,信息串为空的块里也常是代码,去掉注释、字符串之后再看(_prose_line)。一块里只要有一行在写正文
|
||||
就算,所以宁可把拿不准的行当代码:藏起来的正文总有别的行认得出,贴的一段代码、公式里却一行认错就会记错缺口。"""
|
||||
from app.engine.evidence import iter_units
|
||||
|
||||
units = list(iter_units(doc))
|
||||
@@ -603,14 +696,13 @@ def _fenced_body(doc: dict[str, Any]) -> bool:
|
||||
segments = [s for s in unit.get("segments") or [] if isinstance(s, dict)]
|
||||
if unit.get("see") or any(s.get("ref") for s in segments):
|
||||
return True
|
||||
# 围栏那两行是挂在块上的结构片段,不是块里写的内容:整块是不是 diff 要看里面每一行的开头
|
||||
# 围栏那两行是挂在块上的结构片段,不是块里写的内容:算进来的话,开头那行 ```markdown 会被当成块里再嵌的代码块,
|
||||
# 整块是不是 diff 也看不准
|
||||
text = "".join(str(s.get("text") or "") for s in segments if s.get("kind") != "structural")
|
||||
lines = _drop_block_comments(text).split("\n")
|
||||
code = _code_like(lines)
|
||||
if lang and not code:
|
||||
if any(_written(line) for line in text.split("\n")):
|
||||
return True
|
||||
elif any(_prose_line(line, code=code) for line in lines):
|
||||
raw, lines = text.split("\n"), _blank_spans(text).split("\n")
|
||||
code = [True] * len(lines) if _diff(raw, labelled=bool(lang)) else _code_lines(lines, raw, labelled=bool(lang))
|
||||
if any(_written(line) if lang and not is_code else _prose_line(line, code=is_code)
|
||||
for line, is_code in zip(lines, code)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -7,11 +7,16 @@ K5 让报告节点去掉整篇外层的 markdown 围栏(report._unfenced),
|
||||
- 结论句策略为 judge 或 require_citation(报告节点的,或出具契约自己写的)、结论句 0 句、又有信息串为空 / markdown /
|
||||
md / text / plaintext / txt / plain 的代码块,里面写着引用(片段里的引用、只挂依据的 [[see:]])或者在写正文:记一条
|
||||
缺口,降档。缺口不带核查节点的前缀,界面上归在「校验未全部完成」下(run/decode.ts 的 splitIssuanceGaps)
|
||||
- 在写正文:带句读的中文、成句的英文,或者按正文的规矩会算一句结论(方向词、因果词);信息串写明是文字的,一行里连着
|
||||
六个以上汉字也算。列表不论用什么标记(- 1. 1、 一、 (1) · ●),也不论带不带标记,只看字
|
||||
- 行为不变:有正常结论句的;代码块是 sql、python 这类代码的;信息串为空、里面是代码的(注释、字符串里的中文标点不算
|
||||
句子;有 SQL / Python 开头的行、YAML 的键,或者整块是 diff 时,方向词也不算),信息串写成 text、markdown 而里面
|
||||
看得出是代码的同样;信息串为空的块里不带句读、也不像结论的标签和列名;结论句策略为 off 的
|
||||
- 逐行看,先标出代码的行:块里再嵌的代码块(~~~sql)、SQL / Python 的语句连同续行、整段是 YAML 的(写明是文字的块里
|
||||
要有值为空的键)、像数据的行(公式、JSON、目录树、流程箭头、函数调用、文件名、逗号隔开的表头),信息串为空、整块是
|
||||
diff 的全是代码。正文里另有一段代码、一行像 YAML 键的标签,不妨碍认出别的行
|
||||
- 在写正文:代码的行只认去掉注释、字符串之后成句的;别的行带句读的中文、成句的英文,或者按正文的规矩会算一句结论
|
||||
(方向词、因果词);信息串写明是文字的,一行里连着六个以上汉字也算。列表不论用什么标记(- + 1. 1、 一、 (1) · ●
|
||||
→),也不论带不带标记,只看字
|
||||
- 行为不变:有正常结论句的;代码块是 sql、python 这类代码的;写文字的块里只有代码、数据、公式的(注释、字符串里的
|
||||
中文句子不算,别名、公式里的方向词也不算);信息串为空的块里不带句读、也不像结论的标签和列名;结论句策略为 off 的
|
||||
- 取舍:写成 text、markdown 的 diff,+ - 后面又空一格的,和列表分不开,照列表看;信息串为空的块里先列 - 再列 + 的
|
||||
要点、整段都是「小写键: 值」的,和 diff、YAML 分不开,照代码看
|
||||
|
||||
模型一律是 MockChatModel 剧本,数据源是现造的 SQLite 文件,区域、金额都是虚构的。
|
||||
"""
|
||||
@@ -62,6 +67,15 @@ def listed(first: str, second: str) -> str:
|
||||
return f"## 本周结论\n\n{first}{LINES[0]}\n{second}{LINES[1]}"
|
||||
|
||||
|
||||
#: + / - 开头的正负面要点:不包代码块时两句都是结论(「增长」「回落」)
|
||||
PROS_CONS = "+ 华东销售额增长明显\n- 华北销售额回落"
|
||||
#: 正文后面另贴一段取数的 SQL:块里再嵌一个 ~~~sql 代码块,或者缩进四格
|
||||
WITH_TILDE = f"{BARE_LIST}\n\n## 取数\n\n~~~sql\nSELECT region, SUM(amount) AS gmv FROM orders GROUP BY region\n~~~"
|
||||
WITH_INDENTED = f"{BARE_LIST}\n\n## 取数\n\n select region, sum(amount) as gmv from orders group by region"
|
||||
#: 列表项前面写着小写的英文标签,看着像 YAML 的键
|
||||
WITH_LABEL = "## 本周结论\n\n- 华东销售额居各区域之首\n- note: 华南销售额超过华北"
|
||||
|
||||
|
||||
HIDDEN = [pytest.param(PREAMBLE, id="preamble"), pytest.param(UNCLOSED, id="unclosed"),
|
||||
pytest.param(AS_TEXT, id="text"), pytest.param(fenced(TEXT, "plaintext"), id="plaintext"),
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(SEE_LIST)}", id="see-only-list"),
|
||||
@@ -73,7 +87,15 @@ HIDDEN = [pytest.param(PREAMBLE, id="preamble"), pytest.param(UNCLOSED, id="uncl
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(listed('● ', '● '))}", id="black-circle"),
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(UNMARKED)}", id="lines-without-markers"),
|
||||
# 信息串为空:第二句按正文的规矩是一句结论
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(listed('1、', '2、'), '')}", id="enumerated-no-info")]
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(listed('1、', '2、'), '')}", id="enumerated-no-info"),
|
||||
# + / - 开头的正负面要点:写明是文字的块里是列表;信息串为空的块里先列 + 的也不是 diff
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(PROS_CONS)}", id="pros-and-cons"),
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(PROS_CONS, 'text')}", id="pros-and-cons-text"),
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(PROS_CONS, '')}", id="pros-and-cons-no-info"),
|
||||
# 正文里另有几行代码、像 YAML 键的标签:只有那几行当代码看,正文照样认得出
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(WITH_TILDE)}", id="tilde-block-inside"),
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(WITH_INDENTED)}", id="indented-sql-inside"),
|
||||
pytest.param(f"以下是本周周报:\n\n{fenced(WITH_LABEL)}", id="lowercase-label")]
|
||||
|
||||
|
||||
def doc_of(raw: str) -> dict:
|
||||
@@ -130,6 +152,43 @@ def doc_of(raw: str) -> dict:
|
||||
pytest.param("```\n华北销售额回落\n```", id="no-info-conclusion"),
|
||||
# 首字母大写的「Note:」是正文里的标签,不是 YAML 的键
|
||||
pytest.param("```markdown\nNote: 以下为初步结论\n- 华南销售额超过华北\n```", id="capitalised-label"),
|
||||
# + / - 开头的正负面要点:写明是文字的块里是列表,不论哪个在前;信息串为空的块里先列 + 的不是 diff(diff 里一处
|
||||
# 改动先列删掉的行),+ 下面缩进的子项也一样
|
||||
pytest.param(f"```markdown\n{PROS_CONS}\n```", id="plus-minus-markdown"),
|
||||
pytest.param("```text\n- 华北销售额回落\n+ 华东销售额增长明显\n```", id="minus-plus-text"),
|
||||
# 取舍:写成 text、markdown 的 diff,+ - 后面又空一格,和列表分不开,照列表看(diff 该写 diff 信息串)
|
||||
pytest.param("```text\n- 按下单日期统计\n+ 按支付日期统计\n```", id="spaced-diff-labelled-text"),
|
||||
pytest.param(f"```\n{PROS_CONS}\n```", id="plus-minus-no-info"),
|
||||
pytest.param("```\n+ 华东销售额增长明显\n - 主要来自新店\n- 华北销售额回落\n```", id="plus-minus-nested-no-info"),
|
||||
# 正文里另有几行代码:块里再嵌的 ~~~sql、缩进的 SQL 只是那几行,不把整块都当代码
|
||||
pytest.param(f"```markdown\n{WITH_TILDE}\n```", id="tilde-block-inside"),
|
||||
pytest.param(f"```\n{WITH_TILDE}\n```", id="tilde-block-inside-no-info"),
|
||||
pytest.param(f"```markdown\n{WITH_INDENTED}\n```", id="indented-sql-inside"),
|
||||
pytest.param(f"```\n{WITH_INDENTED}\n```", id="indented-sql-inside-no-info"),
|
||||
pytest.param("```text\n- 华南销售额超过华北\n\nselect region from orders\n```", id="sql-in-another-paragraph"),
|
||||
pytest.param("```markdown\n~~~sql\nSELECT region FROM orders\n~~~\n\n- 华南销售额超过华北\n```",
|
||||
id="prose-after-nested-block"),
|
||||
# 块里再嵌的代码块开了没收:不当围栏,后面的正文照样认
|
||||
pytest.param("```markdown\n## 取数\n\n~~~sql\nSELECT region FROM orders\n\n## 本周结论\n\n- 华南销售额超过华北\n```",
|
||||
id="nested-block-left-open"),
|
||||
pytest.param("```markdown\n~~~~sql\nSELECT region FROM orders\n~~~\n\n- 华南销售额超过华北\n```", id="nested-close-too-short"),
|
||||
# 收尾的围栏比开头长也收得了
|
||||
pytest.param("```markdown\n~~~~sql\nSELECT region FROM orders\n~~~~~\n\n- 华南销售额超过华北\n```", id="nested-close-longer"),
|
||||
# 写明是文字的块里,顶格的标题另起一段:上一段的 SQL 管不到标题下面的列表
|
||||
pytest.param("```markdown\n## 取数\nselect region from orders\n## 本周结论\n- 华南销售额超过华北\n```", id="heading-ends-sql"),
|
||||
# 行首的箭头是列表的记号,不是连起流程的箭头
|
||||
pytest.param("```markdown\n→ 华东销售额居各区域之首\n→ 华南销售额超过华北\n```", id="arrow-bullets"),
|
||||
# 小写英文键开头的行:一段里有不缩进、又不是键的行,不是 YAML;写明是文字的块里,没有值为空的键(下面缩进着
|
||||
# 列表、子键)的「键: 一句话」也是正文里的标签
|
||||
pytest.param(f"```markdown\n{WITH_LABEL}\n```", id="lowercase-label-after"),
|
||||
pytest.param("```\n- note: 华南销售额超过华北\n- 华东销售额居各区域之首\n```", id="lowercase-label-first-no-info"),
|
||||
pytest.param("```text\n- top: 华东销售额居各区域之首\n- note: 华南销售额超过华北\n```", id="lowercase-labels-only"),
|
||||
pytest.param("```markdown\nsummary: 华南销售额超过华北\n```", id="key-and-conclusion"),
|
||||
# 首字母大写的英文词开头是正文里的英文,不是 SQL 的关键字
|
||||
pytest.param("```markdown\nFrom 数据看 华南销售额超过华北\n```", id="capitalised-keyword"),
|
||||
pytest.param("```\nUpdate 华南销售额超过华北\n```", id="capitalised-keyword-no-info"),
|
||||
# def、class 不写成定义的样子,是正文里的词(A 类门店)
|
||||
pytest.param("```markdown\nclass A 门店销售额超过 class B 门店\n```", id="class-of-stores"),
|
||||
])
|
||||
def test_a_report_body_in_a_prose_code_block_is_recognised(raw):
|
||||
doc = doc_of(raw)
|
||||
@@ -174,12 +233,52 @@ def test_a_report_body_in_a_prose_code_block_is_recognised(raw):
|
||||
pytest.param("```\nSELECT MAX(amount) AS 最高金额, MIN(amount) AS 最低金额 FROM orders\n```", id="sql-aliases"),
|
||||
pytest.param("```\nmetrics:\n - 退款占比\n - 销售额增长率\n```", id="yaml-with-conclusion-words"),
|
||||
pytest.param("```\n- name: 退款占比\n unit: 百分比\n```", id="yaml-mappings"),
|
||||
# 取舍:信息串为空、整段都是「小写键: 值」的照 YAML 看,值里写着结论也一样(写明是文字的块里照正文看)
|
||||
pytest.param("```\n- top: 华东销售额居各区域之首\n- note: 华南销售额超过华北\n```", id="all-keys-no-info"),
|
||||
pytest.param("```\n- 退款率下降\n+ 退款率上升\n```", id="diff-with-conclusion-words"),
|
||||
pytest.param("```\n@@ -1,2 +1,2 @@\n 口径说明\n-销售额按下单日期统计后下降\n+销售额按支付日期统计后上升\n```", id="diff-hunk"),
|
||||
# 写成 text、markdown 的 diff:+ - 紧挨着字写,不是 Markdown 的列表项
|
||||
pytest.param("```text\n-退款率下降时提醒\n+退款率上升时提醒\n```", id="diff-labelled-text"),
|
||||
pytest.param("```markdown\n@@ -1 +1 @@\n-销售额按下单日期统计后下降\n+销售额按支付日期统计后上升\n```",
|
||||
id="diff-hunk-labelled-markdown"),
|
||||
# 有 @@ 块头的,先列加上的行也是 diff
|
||||
pytest.param("```\n@@ -1,2 +1,2 @@\n+销售额按支付日期统计后上升\n-销售额按下单日期统计后下降\n```", id="diff-hunk-plus-first"),
|
||||
# 代码的行里,-- 紧挨着前面的字也是注释
|
||||
pytest.param("```\nSELECT SUM(amount)--按区域汇总,排除退款\nFROM orders\n```", id="sql-comment-without-space"),
|
||||
# 信息串写成 text、markdown,里面却看得出是代码:照代码看,注释里的句子、别名里的方向词不算
|
||||
pytest.param("```text\nSELECT region AS 区域, MAX(amount) AS 最高金额 FROM orders -- 按区域汇总,排除退款\n```",
|
||||
id="sql-labelled-text"),
|
||||
pytest.param("```markdown\nmetrics:\n - 退款占比\n - 销售额增长率\n```", id="yaml-labelled-markdown"),
|
||||
pytest.param("```text\n# 指标\nmetrics:\n # 核心指标\n - 退款占比\n```", id="yaml-with-comments"),
|
||||
pytest.param("```\n# 核心指标\nmetrics:\n - 退款占比\n```", id="yaml-with-comments-no-info"),
|
||||
# 写成 text 的 SQL 跨好几行:以语句开头的一段连同续行都是代码,开头的注释行也是;块注释、多行字符串整段不算,
|
||||
# 只写着块注释的一行也不把一段断开
|
||||
pytest.param("```text\nSELECT region,\n amount AS 销售额增长率\n FROM orders\n```", id="sql-continuation-text"),
|
||||
pytest.param("```text\nSELECT region,\n/* 别名 */\n amount AS 销售额增长率\n FROM orders\n```",
|
||||
id="sql-with-block-comment-text"),
|
||||
pytest.param("```text\n-- 按区域汇总,排除退款。\nSELECT region, amount AS 最高金额 FROM orders\n```", id="sql-leading-comment"),
|
||||
pytest.param('```\ndef summary():\n """\n 按支付日统计,排除退款。\n """\n return 1\n```', id="docstring"),
|
||||
# 像数据、公式的行(等号、JSON、画目录树的制表符、箭头连起的流程、函数调用、文件名、逗号隔开的表头)照代码看:
|
||||
# 去掉字符串之后不成句的不算正文,结论词、六字连写只看写正文的行
|
||||
pytest.param("```\n退款占比 = 退款金额 / 销售额\n```", id="formula-no-info"),
|
||||
pytest.param("```text\n销售额增长率 = (本期销售额 - 上期销售额) / 上期销售额\n```", id="formula-text"),
|
||||
pytest.param('```text\n{"区域": "华东", "说明": "按支付日期统计"}\n```', id="json-text"),
|
||||
pytest.param('```markdown\n "说明": "按支付日期统计,排除退款",\n```', id="json-member"),
|
||||
pytest.param('```text\n["订单编号", "支付日期", "销售额增长率"]\n```', id="json-array"),
|
||||
pytest.param("```text\n报表/\n├── 华东门店销售周报.xlsx\n└── 华南门店销售周报.xlsx\n```", id="tree-text"),
|
||||
pytest.param("```text\n周报\n├── 华东门店销售周报\n│ └── 第四周销售明细\n└── 华南门店销售周报\n```", id="tree-of-folders"),
|
||||
pytest.param("```text\n上传表格导入 → 字段映射校验 → 生成周报\n```", id="flow-text"),
|
||||
pytest.param("```text\nprint('导入完成,请查收')\n```", id="call-with-string-text"),
|
||||
pytest.param("```markdown\nrows.groupby(区域).agg(销售额增长率)\n```", id="call-text"),
|
||||
pytest.param("```markdown\n华东门店销售周报.xlsx\n华南门店销售周报.xlsx\n```", id="file-names"),
|
||||
pytest.param("```text\n订单编号,支付日期,销售额增长率\n```", id="csv-header"),
|
||||
# 块里再嵌的代码块照代码看:注释里的句子、别名里的结论词、列着指标名的 YAML 都不算
|
||||
pytest.param("```markdown\n## 取数\n\n~~~sql\nSELECT MAX(amount) AS 最高金额 FROM orders -- 按区域汇总,排除退款\n~~~\n```",
|
||||
id="tilde-block-only"),
|
||||
pytest.param("```markdown\n## 口径\n\n~~~yaml\n- 退款占比\n- 销售额增长率\n~~~\n```", id="nested-yaml-block"),
|
||||
pytest.param("```markdown\n~~~~\n- 退款占比\n- 销售额增长率\n~~~~~\n```", id="nested-yaml-longer-close"),
|
||||
# 开了没收的那一行照普通的行看,里面的 SQL 照样认得出
|
||||
pytest.param("```markdown\n## 取数\n\n~~~sql\nSELECT MAX(amount) AS 最高金额 FROM orders\n```", id="nested-sql-left-open"),
|
||||
# 有正常的结论句:正文没被藏起来
|
||||
pytest.param("华东销售额超过华南。\n\n```markdown\n华东销售额 1500,居各区域之首。\n```", id="claims-outside"),
|
||||
# 没有代码块
|
||||
@@ -189,14 +288,27 @@ def test_code_or_a_report_with_claims_is_not_taken_for_a_hidden_body(raw):
|
||||
assert _fenced_body(doc_of(raw)) is False
|
||||
|
||||
|
||||
def test_a_long_line_without_a_full_stop_takes_linear_time():
|
||||
"""很长、不带句末标点的一行英文单词:逐串数单词,耗时随行长线性增长。按正则回溯认英文句子时,四万多字的一行要十几
|
||||
秒(行长翻倍、耗时翻四倍),出口复核在事件循环上同步执行,整个服务都会卡住。"""
|
||||
line = "alpha beta " * 4000
|
||||
doc = {"blocks": [{"type": "code", "lang": "", "units": [
|
||||
@pytest.mark.parametrize("lang", ["", "markdown"])
|
||||
@pytest.mark.parametrize("line", [
|
||||
pytest.param("alpha beta " * 4000, id="words"),
|
||||
# 逐行认代码、认数据的几条规则:一长串字母后面没有括号、冒号,一长串空格、横线,一长串汉字、逗号、引号
|
||||
pytest.param("a" * 40000, id="one-long-word"),
|
||||
pytest.param(" " * 20000 + "-" + " " * 20000 + "a", id="spaces-and-a-dash"),
|
||||
pytest.param("华" * 40000, id="cjk"),
|
||||
pytest.param("a," * 20000, id="commas"),
|
||||
pytest.param(' "a' * 13000, id="open-quotes"),
|
||||
pytest.param('"""' * 13000, id="triple-quotes"),
|
||||
# 好多行围栏:收得了的、开了没收的
|
||||
pytest.param("~~~\n" * 13000, id="bare-fences"),
|
||||
pytest.param("~~~sql\n" * 13000, id="fences-left-open"),
|
||||
])
|
||||
def test_a_long_line_without_a_full_stop_takes_linear_time(lang, line):
|
||||
"""很长、不带句末标点的一行:逐串数单词、逐行认代码,耗时随行长线性增长。按正则回溯认英文句子时,四万多字的一行要
|
||||
十几秒(行长翻倍、耗时翻四倍),出口复核在事件循环上同步执行,整个服务都会卡住。"""
|
||||
doc = {"blocks": [{"type": "code", "lang": lang, "units": [
|
||||
{"kind": "code", "see": [], "segments": [{"kind": "text", "text": line}]}]}]}
|
||||
started = time.perf_counter()
|
||||
assert _fenced_body(doc) is False
|
||||
_fenced_body(doc)
|
||||
assert time.perf_counter() - started < 1.0
|
||||
|
||||
|
||||
@@ -283,16 +395,21 @@ async def test_require_citation_records_the_gap_too(monkeypatch, claims, contrac
|
||||
assert issuance["claims"]["uncited_claims"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [
|
||||
pytest.param("SELECT region, SUM(amount) AS gmv FROM orders GROUP BY region -- 按区域汇总,排除退款",
|
||||
@pytest.mark.parametrize("info, code", [
|
||||
pytest.param("", "SELECT region, SUM(amount) AS gmv FROM orders GROUP BY region -- 按区域汇总,排除退款",
|
||||
id="sql-trailing-comment"),
|
||||
pytest.param("columns:\n - 订单编号\n - 支付日期", id="yaml-list"),
|
||||
pytest.param("- 按下单日期统计\n+ 按支付日期统计", id="diff"),
|
||||
pytest.param("", "columns:\n - 订单编号\n - 支付日期", id="yaml-list"),
|
||||
pytest.param("", "- 按下单日期统计\n+ 按支付日期统计", id="diff"),
|
||||
pytest.param("", "退款占比 = 退款金额 / 销售额", id="formula"),
|
||||
pytest.param("text", "销售额增长率 = (本期销售额 - 上期销售额) / 上期销售额", id="formula-text"),
|
||||
pytest.param("text", '{"区域": "华东", "说明": "按支付日期统计"}', id="json-text"),
|
||||
pytest.param("text", "报表/\n├── 华东门店销售周报.xlsx\n└── 华南门店销售周报.xlsx", id="tree-text"),
|
||||
])
|
||||
async def test_code_in_a_report_without_claims_is_issued_as_before(monkeypatch, code):
|
||||
"""一句结论都没有的报告(只有口径说明)贴了一段不带信息串的代码:行尾注释里写着中文句子的 SQL、列着中文列名的
|
||||
YAML、改口径的 diff。那是代码,不是藏起来的正文,照常完整出具,不记这条缺口。"""
|
||||
writer(monkeypatch, f"## 口径说明\n\n本周报告按支付日期统计。\n\n```\n{code}\n```")
|
||||
async def test_code_in_a_report_without_claims_is_issued_as_before(monkeypatch, info, code):
|
||||
"""一句结论都没有的报告(只有口径说明)贴了一段代码、公式或数据:行尾注释里写着中文句子的 SQL、列着中文列名的
|
||||
YAML、改口径的 diff(这三种不带信息串),带「占比」「增长率」的公式,写成 text 的 JSON、目录树。那不是藏起来的
|
||||
正文,照常完整出具,不记这条缺口。"""
|
||||
writer(monkeypatch, f"## 口径说明\n\n本周报告按支付日期统计。\n\n```{info}\n{code}\n```")
|
||||
row = await finish(weekly(claims="require_citation"))
|
||||
assert row.status == "succeeded", row.error
|
||||
issuance = row.output["_issuance"]
|
||||
|
||||
Reference in New Issue
Block a user