mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 10:01:58 +00:00
03c18c4d78
一、文案(前端约 1600 处,后端传到界面的约 880 处) - 术语统一:工作流层面用「运行」(继续运行、重新运行、试运行),节点和工具层面用「执行」,不再用「跑」; 总时长 / 执行时长 / 等待审批;服务端;助手;Agent;token;工作流;固定版本;模型接入;系统提示;批准 / 驳回;受管级别 - 口语改书面:取不到、没取回来、读不懂、连不上、多半、照样、兜底、揉成这类说法改成「无法 / 未能 / 失败 / 请」;去掉拟人写法 - 不再露出实现细节:界面上不写 checkpoint、同一拍、哈希(凭证处除外),不写改版历史(「以前…」「老数据」); 报错、校验、门禁、自动修复里裸写的配置键名和枚举值换成界面上的中文标签,由新的 backend/app/engine/labels.py 统一提供; 发布弹窗里结论句的选项不再是 off / require_citation - 同一件事只留一种说法:生成阶段文案、出具档位说明、画布锁定提示各合成一份 - 去掉界面上的工业场景示例(MES、ERP、「张工」「王工」)和真实产品名,换成通用示例 二、说错的提示(都有测试证明) - 数据库测连接失败时的专用提示以前从不出现(按报错文字全等比较,永远比不上):改为按错误类型判断,不再拼出重复的前缀 - 「设置 → 供应商」改指「设置 → 模型接入」;「在设置里建数据源」改指「数据」页 - 缺少 API Key、模型接入已停用的报错不再夹英文,写明去哪里填 - 两处原因说错(「这种引用在后续版本支持」「这一版还不支持」)改成真实原因 - 节点说明里不准确的说法改正:代码节点在 microVM 或系统沙箱中执行;报告撰写、口径卡的说明不再说过头 - 导出的证据 CSV 里两列同名,第三列改为「片段编号」 三、前后端对齐 - 前端按后端原文识别日志、报错的地方,新旧两种写法都认得(历史运行里存的是旧文字),逐条补了新旧原文的核对 - 后端交回模型的原指令放进单独的 for_model 字段,前端在解析响应时去掉,不会显示到界面上 四、检查 - 检查脚本里按原文匹配的断言改成新文案;改完会变成永远成立的反向断言逐条修正,并做了变异验证 - 新增 check-copy(静态扫描前端界面字符串和后端上界面的文案):再出现「跑」「后端」「多半」「令牌」、 界面上的 Copilot、裸写的配置键名、半角标点等就报错;确需保留的写进 scripts/copy-allow.json 并注明理由
126 lines
5.9 KiB
Python
126 lines
5.9 KiB
Python
"""报告撰写节点的类型登记,以及搭图时就能认出来的两种错接。
|
||
|
||
报告节点从哪几张口径卡取指标(metrics_from)、出口契约核对哪个报告节点
|
||
(report_from),都是按节点 id 写死的引用。指到一个不是口径卡的节点、或者指到
|
||
下游,运行时只会得到一个空目录、一份无从核对的报告——而这在画图时就看得出来。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from app.core.events import EventType
|
||
from app.engine.schema import TYPE_LABEL, GraphSpec, NodeType, type_label, validate_graph
|
||
from app.engine.variables import analyze
|
||
|
||
|
||
def node(nid, ntype, **config):
|
||
return {"id": nid, "type": ntype, "position": {"x": 0, "y": 0},
|
||
"data": {"label": nid, "config": config}}
|
||
|
||
|
||
def spec(nodes, edges):
|
||
return GraphSpec.model_validate({
|
||
"nodes": nodes, "edges": [{"source": a, "target": b} for a, b in edges]})
|
||
|
||
|
||
def issues(graph: GraphSpec, field: str) -> list[tuple[str, str]]:
|
||
return [(i.level, i.message) for i in validate_graph(graph).issues if i.field == field]
|
||
|
||
|
||
def test_report_type_is_registered():
|
||
assert NodeType("report") is NodeType.REPORT
|
||
assert TYPE_LABEL["report"] == "报告撰写" == type_label(NodeType.REPORT)
|
||
assert EventType("report.checked") is EventType.REPORT_CHECKED
|
||
|
||
|
||
def report_graph(metrics_from=None, *, report_from="write", extra=(), edges=None, **contract):
|
||
cfg = {"instructions": "写周报", "model": "mock"}
|
||
if metrics_from is not None:
|
||
cfg["metrics_from"] = metrics_from
|
||
nodes = [
|
||
node("start", "input"),
|
||
node("fetch", "transform", mode="expression", expression="{'gmv': 1}", assign_to="kpi"),
|
||
node("caliber", "metrics", metrics=[{"id": "gmv", "expression": "vars.kpi.gmv"}]),
|
||
node("write", "report", **cfg),
|
||
node("out", "output", fields=[{"name": "周报", "value": "{{ nodes.write.text }}"}],
|
||
contract={"metrics_from": ["caliber"], "report_from": report_from, **contract}),
|
||
*extra,
|
||
]
|
||
return spec(nodes, edges or [("start", "fetch"), ("fetch", "caliber"), ("caliber", "write"),
|
||
("write", "out")])
|
||
|
||
|
||
def test_a_well_formed_report_graph_has_no_new_errors():
|
||
graph = report_graph(["caliber"])
|
||
assert issues(graph, "metrics_from") == []
|
||
assert issues(graph, "contract.report_from") == []
|
||
assert issues(report_graph("caliber"), "metrics_from") == [] # 单个 id 写成字符串也认
|
||
|
||
|
||
@pytest.mark.parametrize("metrics_from, words", [
|
||
(["nope"], ["nope", "不存在"]),
|
||
(["fetch"], ["「fetch」", "不是口径卡"]),
|
||
])
|
||
def test_metrics_from_must_name_a_caliber(metrics_from, words):
|
||
found = issues(report_graph(metrics_from), "metrics_from")
|
||
assert len(found) == 1 and found[0][0] == "error", found
|
||
assert all(w in found[0][1] for w in words), found
|
||
|
||
|
||
def test_metrics_from_must_be_upstream():
|
||
"""口径卡接在报告后面:报告跑的时候它还没算,目录里一个指标都没有。"""
|
||
graph = report_graph(["late"], extra=[node("late", "metrics", metrics=[{"id": "x", "expression": "1"}])],
|
||
edges=[("start", "fetch"), ("fetch", "caliber"), ("caliber", "write"),
|
||
("write", "late"), ("late", "out")])
|
||
found = issues(graph, "metrics_from")
|
||
assert len(found) == 1 and found[0][0] == "error" and "上游" in found[0][1], found
|
||
|
||
|
||
@pytest.mark.parametrize("report_from, words", [
|
||
("nope", ["nope", "不存在"]),
|
||
("caliber", ["caliber", "不是「报告撰写」"]),
|
||
])
|
||
def test_report_from_must_name_a_report_node(report_from, words):
|
||
found = issues(report_graph(["caliber"], report_from=report_from), "contract.report_from")
|
||
assert len(found) == 1 and found[0][0] == "error", found
|
||
assert all(w in found[0][1] for w in words), found
|
||
|
||
|
||
def test_report_from_must_be_upstream_of_the_output():
|
||
graph = report_graph(["caliber"], report_from="side", extra=[node("side", "report", instructions="x")],
|
||
edges=[("start", "fetch"), ("fetch", "caliber"), ("caliber", "write"),
|
||
("write", "out"), ("caliber", "side")])
|
||
found = issues(graph, "contract.report_from")
|
||
assert len(found) == 1 and found[0][0] == "error" and "上游" in found[0][1], found
|
||
|
||
|
||
def test_old_contracts_are_untouched():
|
||
"""没写 report_from 的旧契约:不多一条报错,缺 metrics_from 照旧报。"""
|
||
graph = spec([node("start", "input"),
|
||
node("out", "output", contract={"narrative": "x"})], [("start", "out")])
|
||
assert issues(graph, "contract.report_from") == []
|
||
assert [lv for lv, _ in issues(graph, "contract.metrics_from")] == ["error"]
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 变量分析扫口径卡的表达式
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
def test_caliber_expressions_count_as_references():
|
||
"""口径卡对上游的引用以前在变量表里看不见:vars.kpi 被报成「产出了但没有任何地方引用」,
|
||
画布上也画不出口径卡依赖谁。"""
|
||
graph = spec([
|
||
node("start", "input"),
|
||
node("fetch", "transform", mode="expression", expression="{'gmv': 1, 'prev': 1}", assign_to="kpi"),
|
||
node("caliber", "metrics", metrics=[
|
||
{"id": "gmv", "expression": "vars.kpi.gmv"},
|
||
{"id": "wow", "expression": "(vars.kpi.gmv - vars.kpi.prev) / vars.kpi.prev"}]),
|
||
node("out", "output", fields=[{"name": "r", "value": "{{ nodes.caliber.text }}"}]),
|
||
], [("start", "fetch"), ("fetch", "caliber"), ("caliber", "out")])
|
||
report = analyze(graph)
|
||
kpi = next(v for v in report.variables if v.path == "vars.kpi")
|
||
assert {(r.node_id, r.field) for r in kpi.refs} == {("caliber", "metrics[0].expression"),
|
||
("caliber", "metrics[1].expression")}
|
||
assert not [i for i in report.issues if i.path == "vars.kpi"], report.issues
|