mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 05:01:57 +00:00
6d9d31395b
上一轮先判整块:只要有一行以 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、分段、像数据的七种写法、注释、块注释、多行字符串、 正文判法、结构片段、挂钩),各有用例变红。
427 lines
33 KiB
Python
427 lines
33 KiB
Python
"""出口契约:报告的正文仍写在代码块里、一句结论都没切出来时记出具缺口(nodes/io.py 的 _fenced_body)。
|
||
|
||
K5 让报告节点去掉整篇外层的 markdown 围栏(report._unfenced),规格有意不去的写法仍会让正文落在代码块里:围栏外有
|
||
前言、只有开头没有收尾、信息串写成 text。这时结论句是 0 句:裁判没有句子可判、反方审稿记 no_claims,以前正式运行
|
||
照样完整出具,读的人以为结论都核对过。
|
||
|
||
- 结论句策略为 judge 或 require_citation(报告节点的,或出具契约自己写的)、结论句 0 句、又有信息串为空 / markdown /
|
||
md / text / plaintext / txt / plain 的代码块,里面写着引用(片段里的引用、只挂依据的 [[see:]])或者在写正文:记一条
|
||
缺口,降档。缺口不带核查节点的前缀,界面上归在「校验未全部完成」下(run/decode.ts 的 splitIssuanceGaps)
|
||
- 逐行看,先标出代码的行:块里再嵌的代码块(~~~sql)、SQL / Python 的语句连同续行、整段是 YAML 的(写明是文字的块里
|
||
要有值为空的键)、像数据的行(公式、JSON、目录树、流程箭头、函数调用、文件名、逗号隔开的表头),信息串为空、整块是
|
||
diff 的全是代码。正文里另有一段代码、一行像 YAML 键的标签,不妨碍认出别的行
|
||
- 在写正文:代码的行只认去掉注释、字符串之后成句的;别的行带句读的中文、成句的英文,或者按正文的规矩会算一句结论
|
||
(方向词、因果词);信息串写明是文字的,一行里连着六个以上汉字也算。列表不论用什么标记(- + 1. 1、 一、 (1) · ●
|
||
→),也不论带不带标记,只看字
|
||
- 行为不变:有正常结论句的;代码块是 sql、python 这类代码的;写文字的块里只有代码、数据、公式的(注释、字符串里的
|
||
中文句子不算,别名、公式里的方向词也不算);信息串为空的块里不带句读、也不像结论的标签和列名;结论句策略为 off 的
|
||
- 取舍:写成 text、markdown 的 diff,+ - 后面又空一格的,和列表分不开,照列表看;信息串为空的块里先列 - 再列 + 的
|
||
要点、整段都是「小写键: 值」的,和 diff、YAML 分不开,照代码看
|
||
|
||
模型一律是 MockChatModel 剧本,数据源是现造的 SQLite 文件,区域、金额都是虚构的。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
|
||
import pytest
|
||
from langchain_core.messages import AIMessage
|
||
|
||
from app.engine.evidence import compose_doc
|
||
from app.engine.nodes.io import GAP_FENCED_BODY, _fenced_body
|
||
from app.providers.mock_model import MockChatModel
|
||
from tests.test_report_fenced import ( # noqa: F401 - engine_up、shop 是夹具,导入即可用
|
||
EAST,
|
||
SOUTH,
|
||
SQL,
|
||
TEXT,
|
||
Models,
|
||
audited,
|
||
chain,
|
||
engine_up,
|
||
fenced,
|
||
finish,
|
||
logs,
|
||
node,
|
||
output_of,
|
||
report_of,
|
||
shop,
|
||
)
|
||
|
||
#: 写作者把正文包进代码块、又不是整篇只有一个代码块(报告节点不去围栏)的几种写法
|
||
PREAMBLE = f"以下是本周的销售报告:\n\n{fenced(TEXT)}"
|
||
UNCLOSED = f"```markdown\n{TEXT}"
|
||
AS_TEXT = fenced(TEXT, "text")
|
||
#: 只用 [[see:]] 挂依据的列表:依据记在句子上(unit 的 see),片段里没有引用;列表项也不带句读
|
||
SEE_LIST = "## 本周结论\n\n- 华东销售额居各区域之首 [[see:Q1]]\n- 华南销售额超过华北 [[see:Q1]]"
|
||
#: 不带句读、也没挂依据的列表:中文报告的列表项常这样写
|
||
BARE_LIST = "## 本周结论\n\n- 华东销售额居各区域之首\n- 华南销售额超过华北"
|
||
#: 同样两句,换成中文报告常见的几种编号、项目符号,或者干脆不带标记。不包代码块时第二句是一句结论(「超过」),
|
||
#: 第一句是连接性的话:两种句子都要认得出
|
||
LINES = ("华东销售额居各区域之首", "华南销售额超过华北")
|
||
#: 不带标记、一行一句
|
||
UNMARKED = f"## 本周结论\n\n{LINES[0]}\n\n{LINES[1]}"
|
||
|
||
|
||
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"),
|
||
pytest.param(f"以下是本周周报:\n\n{fenced(BARE_LIST)}", id="list-without-punctuation"),
|
||
pytest.param(f"以下是本周周报:\n\n{fenced(listed('1、', '2、'))}", id="digit-and-comma"),
|
||
pytest.param(f"以下是本周周报:\n\n{fenced(listed('一、', '二、'))}", id="chinese-numeral"),
|
||
pytest.param(f"以下是本周周报:\n\n{fenced(listed('(1)', '(2)'))}", id="fullwidth-parens"),
|
||
pytest.param(f"以下是本周周报:\n\n{fenced(listed('· ', '· '))}", id="middle-dot"),
|
||
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"),
|
||
# + / - 开头的正负面要点:写明是文字的块里是列表;信息串为空的块里先列 + 的也不是 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:
|
||
return compose_doc(raw, {}, node_id="write")
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 认得出哪些代码块里写的是正文
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.parametrize("raw", [
|
||
pytest.param("以下是本周的销售报告:\n\n```markdown\n华东销售额 1500,居各区域之首。\n```", id="preamble"),
|
||
pytest.param("```markdown\n## 本周概览\n\n华东销售额 1500,居各区域之首。", id="unclosed"),
|
||
pytest.param("```text\n华东销售额 1500,居各区域之首。\n```", id="text"),
|
||
pytest.param("```\n华东销售额 1500,居各区域之首。\n```", id="no-info"),
|
||
pytest.param("```MD\n华东销售额 1500,居各区域之首。\n```", id="upper-md"),
|
||
pytest.param("```markdown\nEast region sales rose sharply this week.\n```", id="english"),
|
||
pytest.param("```markdown\nEast region's sales rose sharply this week.\n```", id="english-apostrophe"),
|
||
# 正文里用半角引号括起一个词:去掉代码字符串的那一步不能把句子吃掉
|
||
pytest.param('```markdown\n华东的"销售额"居各区域之首,华南次之。\n```', id="quoted-word"),
|
||
# 纯文字的另几种写法
|
||
pytest.param("```plaintext\n华东销售额 1500,居各区域之首。\n```", id="plaintext"),
|
||
pytest.param("```txt\n华东销售额 1500,居各区域之首。\n```", id="txt"),
|
||
pytest.param("```Plain\n华东销售额 1500,居各区域之首。\n```", id="plain"),
|
||
# 只有列表、没有句读,可写着报告的引用:代码里不会这样写
|
||
pytest.param("以下是周报:\n\n```markdown\n- 华东销售额 [[m:gmv]]\n- 华南销售额 [[m:gmv_s]]\n```", id="markers"),
|
||
# 只挂了依据([[see:]] 记在句子上,片段里没有引用),列表项短得不成句也认
|
||
pytest.param("以下是周报:\n\n```markdown\n- East [[see:m:gmv]]\n- South [[see:m:gmv]]\n```", id="see-only"),
|
||
# 不带句读的列表项:一行里连着六个以上汉字,或者按正文的规矩是一句结论
|
||
pytest.param("以下是周报:\n\n```markdown\n## 本周结论\n\n- 华东销售额居各区域之首\n- 华南销售额超过华北\n```",
|
||
id="bullets"),
|
||
pytest.param("```text\n1. 华东销售额居各区域之首\n2) 华南销售额超过华北\n```", id="numbered"),
|
||
pytest.param("```\n* 华东销售额居各区域之首\n+ 华南销售额超过华北\n• 华北销售额持平\n```", id="bare-bullets"),
|
||
# 中文报告常见的编号、项目符号,或者不带标记:不看标记,只看字
|
||
pytest.param("以下是周报:\n\n```markdown\n1、华东销售额居各区域之首\n2、华南销售额超过华北\n```", id="digit-and-comma"),
|
||
pytest.param("```markdown\n一、华东销售额居各区域之首\n二、华南销售额超过华北\n```", id="chinese-numeral"),
|
||
pytest.param("```text\n(1)华东销售额居各区域之首\n(2) 华南销售额超过华北\n```", id="parens"),
|
||
pytest.param("```markdown\n· 华东销售额居各区域之首\n● 华南销售额超过华北\n▪ 华北销售额持平\n```", id="dots-circles-squares"),
|
||
pytest.param("```markdown\n华东销售额居各区域之首\n\n华南销售额超过华北\n```", id="no-markers"),
|
||
pytest.param("```markdown\n> 华东销售额居各区域之首\n```", id="blockquote"),
|
||
pytest.param("```markdown\n| 区域 | 结论 |\n|---|---|\n| 华东 | 销售额居各区域之首 |\n```", id="table-row"),
|
||
pytest.param("```markdown\n**华东**销售额居各区域之首\n```", id="bold"),
|
||
# 写明是文字的块:一行里连着六个以上汉字就是在写中文,不像结论的也算
|
||
pytest.param("```markdown\n华东销售额居各区域之首\n```", id="six-or-more-in-a-row"),
|
||
# 不到六个字的短句:按正文的规矩是一句结论(方向词)就算
|
||
pytest.param("```text\n- 华北回落\n- 华南领先\n```", id="short-conclusions"),
|
||
# 写明是文字的块不当代码看:# -- 和半角引号都是正文里的符号
|
||
pytest.param("```markdown\n华东排名 #1,销售额居各区域之首。\n```", id="hash-in-prose"),
|
||
pytest.param('```markdown\n负责人说"销售额创新高,下周继续推进。"\n```', id="quoted-sentence"),
|
||
pytest.param("```markdown\n华东领跑 -- 华南次之,华北第三。\n```", id="dash-in-prose"),
|
||
# 信息串为空、看不出是代码:按正文的规矩会算结论的行也算
|
||
pytest.param("```\n1、华东销售额居各区域之首\n2、华南销售额超过华北\n```", id="no-info-enumerated"),
|
||
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)
|
||
assert doc["stats"]["claims"] == 0, "背景:正文在代码块里,一句结论都切不出来"
|
||
assert _fenced_body(doc) is True
|
||
|
||
|
||
@pytest.mark.parametrize("raw", [
|
||
# 代码:信息串写明了语言
|
||
pytest.param("```sql\nSELECT region, SUM(amount) AS gmv FROM orders WHERE amount > 100 GROUP BY region\n```",
|
||
id="sql"),
|
||
pytest.param("```python\nprint('华东销售额最高,华南次之。')\n```", id="python"),
|
||
pytest.param('```json\n{"区域": "华东,华南"}\n```', id="json"),
|
||
# 信息串为空、里面是代码:没有成句的文字;代码注释里的中文句子不算
|
||
pytest.param("```\nSELECT region, SUM(amount) FROM orders GROUP BY region\n```", id="bare-sql"),
|
||
pytest.param("```\n-- 按支付日统计,排除退款。\nSELECT SUM(amount) FROM orders\n```", id="sql-comment"),
|
||
pytest.param("```\n/* 按支付日统计,排除退款。 */\nSELECT SUM(amount) FROM orders\n```", id="block-comment"),
|
||
pytest.param("```\n/*\n * 按支付日统计,排除退款。\n 已退款的单不计入:见口径说明。\n */\nSELECT SUM(amount) FROM orders\n```",
|
||
id="multi-line-comment"),
|
||
# 行尾注释(-- // #)、# 开头的注释行,里面写着中文句子也是代码的一部分
|
||
pytest.param("```\nSELECT region, SUM(amount) AS gmv FROM orders GROUP BY region -- 按区域汇总,排除退款\n```",
|
||
id="trailing-sql-comment"),
|
||
pytest.param("```\n# 按区域汇总,排除退款。\ntotal = sum(rows) # 单位:元,不含税\n```", id="hash-comments"),
|
||
pytest.param("```\nconst total = sum(rows); // 按区域汇总,排除退款\n```", id="trailing-slash-comment"),
|
||
# 字符串字面量里的中文标点是数据(带 f、N 这类前缀的也是)
|
||
pytest.param('```\nprint("销售额:", total)\nprint(f"订单数:{n},客单价:{avg}")\n```', id="string-literals"),
|
||
pytest.param("```\nSELECT * FROM orders WHERE note = '已退款,不计入:测试' OR memo = N'作废,重开'\n```",
|
||
id="sql-strings"),
|
||
# 信息串为空、看得出是 SQL 的块:以 * + - 开头的是运算的续行,后面的中文列名不算正文
|
||
pytest.param("```\nSELECT 单价\n * 数量 AS 金额\n + 运费 AS 合计\n - 折扣金额 AS 实付\nFROM 订单\n```",
|
||
id="sql-continuation"),
|
||
pytest.param("```text\n区域 销售额\n华东 1500\n华南 900\n```", id="table-of-numbers"),
|
||
# 写明是文字的块里,表格的列名、门店名这类标签不到六个字,又不像结论
|
||
pytest.param("```text\n门店名称 累计销售额\n华东一店 1500\n```", id="labels-under-six"),
|
||
# 标题不算正文:不包代码块时,标题也从不算结论句
|
||
pytest.param("```markdown\n## 华南销售额超过华北\n```", id="heading-only"),
|
||
# 信息串为空、不带句读的行:不像结论的不算(列名、改了的口径)
|
||
pytest.param("## 口径说明\n\n本周报告按支付日期统计。\n\n```\ncolumns:\n - 订单编号\n - 支付日期\n```", id="yaml-list"),
|
||
pytest.param("## 口径说明\n\n本周报告按支付日期统计。\n\n```\n- 按下单日期统计\n+ 按支付日期统计\n```", id="diff"),
|
||
pytest.param("```\n- 订单编号\n- 支付日期\n```", id="labels"),
|
||
# 信息串为空、看得出是代码(SQL、YAML 的键、diff):值里、别名里有方向词也不是结论
|
||
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"),
|
||
# 没有代码块
|
||
pytest.param("以下是本周的销售报告。", id="no-code"),
|
||
])
|
||
def test_code_or_a_report_with_claims_is_not_taken_for_a_hidden_body(raw):
|
||
assert _fenced_body(doc_of(raw)) is False
|
||
|
||
|
||
@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()
|
||
_fenced_body(doc)
|
||
assert time.perf_counter() - started < 1.0
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 端到端:正式运行(裁判 + 反方审稿 + 出具契约)
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
def gap(title: str = "写周报") -> str:
|
||
return GAP_FENCED_BODY.format(title=title)
|
||
|
||
|
||
@pytest.mark.parametrize("raw", HIDDEN)
|
||
async def test_a_body_left_in_a_code_block_degrades_the_formal_issuance(monkeypatch, shop, raw):
|
||
"""正文落在代码块里:裁判没有句子可判(不调用)、反方审稿记 no_claims,出口契约不再完整出具,记一条缺口说清原因。"""
|
||
models = Models(monkeypatch, shop, raw)
|
||
row = await finish(audited(shop), run_class="formal")
|
||
assert row.status == "succeeded", row.error
|
||
_, doc = await report_of(row.id)
|
||
assert doc["source"] == raw and doc["stats"]["claims"] == 0, "背景:报告节点不去这几种围栏"
|
||
assert "code" in [b["type"] for b in doc["blocks"]]
|
||
assert models.judges == [] and models.reviewed == []
|
||
assert [d["reason"] for d in await logs(row.id, "challenge_skipped")] == ["no_claims"]
|
||
issuance = row.output["_issuance"]
|
||
assert issuance["tier"] == "degraded", issuance
|
||
assert issuance["gaps"] == [gap()], issuance["gaps"]
|
||
|
||
|
||
@pytest.mark.parametrize("raw", [
|
||
pytest.param(TEXT, id="plain"),
|
||
pytest.param(fenced(TEXT), id="whole-report-unwrapped"),
|
||
pytest.param(f"华东销售额 [[v:Q1.r0.销售额]],居各区域之首。[[see:Q1]]\n\n{SQL}\n\n华南销售额超过华北。[[see:Q1]]",
|
||
id="sql-block"),
|
||
])
|
||
async def test_a_report_with_claims_is_issued_as_before(monkeypatch, shop, raw):
|
||
"""有正常的结论句(整篇外层围栏已由报告节点去掉的也是)、代码块是 SQL 的:判过、审过,照常完整出具,不记这条缺口。"""
|
||
models = Models(monkeypatch, shop, raw)
|
||
row = await finish(audited(shop), run_class="formal")
|
||
assert row.status == "succeeded", row.error
|
||
assert sorted(models.reviewed) == sorted([EAST, SOUTH])
|
||
issuance = row.output["_issuance"]
|
||
assert issuance["tier"] == "formal" and issuance["gaps"] == [], issuance
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 结论句策略:require_citation 同样记,off 不管
|
||
# --------------------------------------------------------------------------
|
||
|
||
KPI = "{'gmv': 45678.5, 'orders': 1234}"
|
||
METRICS = [{"id": "gmv", "name": "销售额", "unit": "元", "decimals": 1, "expression": "vars.kpi.gmv"}]
|
||
#: 前言 + 代码块包住正文,正文里的引用都解析得了
|
||
CITED = "以下是本周周报:\n\n```markdown\n本周销售额 [[m:gmv]],较上周有所增长。[[see:m:gmv]]\n```"
|
||
|
||
|
||
def weekly(*, claims: str | None = None, contract_claims: str | None = None) -> dict:
|
||
"""输入 → 取数 → 口径卡 → 报告(claims 是报告节点的结论句策略)→ 成果(引用模式的出具契约)。"""
|
||
return chain(
|
||
node("start", "input"),
|
||
node("fetch", "transform", mode="expression", expression=KPI, assign_to="kpi"),
|
||
node("caliber", "metrics", caliber="周报口径", caliber_version="v1", metrics=METRICS),
|
||
node("write", "report", "写周报", instructions="写周报", max_repairs=0,
|
||
**({"claims": claims} if claims else {})),
|
||
node("out", "output", fields=[{"name": "周报", "value": "{{ nodes.write.text }}"}],
|
||
contract={"report_from": "write", "metrics_from": ["caliber"],
|
||
**({"claims": contract_claims} if contract_claims else {})}),
|
||
)
|
||
|
||
|
||
def writer(monkeypatch, text: str) -> None:
|
||
monkeypatch.setattr(MockChatModel, "_decide", lambda self, messages: AIMessage(content=text))
|
||
|
||
|
||
@pytest.mark.parametrize("claims, contract_claims", [
|
||
pytest.param("require_citation", None, id="report-node"),
|
||
pytest.param(None, "require_citation", id="contract"),
|
||
])
|
||
async def test_require_citation_records_the_gap_too(monkeypatch, claims, contract_claims):
|
||
"""要求结论句挂依据(报告节点或出具契约写的):同样数不出结论句,记同一条缺口。"""
|
||
writer(monkeypatch, CITED)
|
||
row = await finish(weekly(claims=claims, contract_claims=contract_claims))
|
||
assert row.status == "succeeded", row.error
|
||
issuance = row.output["_issuance"]
|
||
assert issuance["tier"] == "degraded" and issuance["gaps"] == [gap()], issuance
|
||
assert issuance["claims"]["uncited_claims"] == 0
|
||
|
||
|
||
@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("", "退款占比 = 退款金额 / 销售额", 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, 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"]
|
||
assert issuance["tier"] == "formal" and issuance["gaps"] == [], issuance
|
||
|
||
|
||
async def test_without_a_claims_policy_nothing_changes(monkeypatch):
|
||
"""结论句策略为 off:出口不核对结论句,同样的报告照旧完整出具(数字都在代码块里、也都有出处)。"""
|
||
writer(monkeypatch, CITED)
|
||
row = await finish(weekly())
|
||
assert row.status == "succeeded", row.error
|
||
issuance = row.output["_issuance"]
|
||
assert issuance["tier"] == "formal" and issuance["gaps"] == [], issuance
|
||
assert "claims" not in issuance
|