mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:41:57 +00:00
277982ead3
- 蓝图:bp_* 操作和逐卡白名单、蓝图检查、留痕、按蓝图判权限;五个场景模板加自由形(骨架、关节、应用骨架、定时任务),实例化与骨架比对;BlueprintApplier / TemplateApplier(只收模板声明过的关节)与自查;look 新类别口径卡;蓝图对话和填关节两个流式接口,不同组的关节并行填、出错的组串行重试;模板列表 - 试运行与发布单:四种起点(对话、模板、问数据的一轮、已发布版本);试运行是探索运行,草稿应用按这次运行解析、印章写试运行,流程类派给自己、不触发升级;发布单九步、预检只读、每步留痕、停下后接着做、核对卡片指纹和应用管理权、启动时整理中断的单;白话层覆盖全部问题编号 - 记录表与文件:平台托管只追加的记录表、按高水位固定数据、连接上的临时视图做行级可见、写入走动作(records_append)、表版本在发布时切换、草稿不影响线上、记录列表遮敏感列;文件按哈希存、两步上传、提交时绑定运行、下载按读权限、清理、file_text、按配方导入一期(table_import_period) - 个人应用:存成个人应用(问数据的约束)、刷新(遮罩、只读、每日上限)、只有拥有者看得见、提交审核与共享副本、开关下发到认人接口;页面编辑的候选;隐藏组件不下发;三条新检查
152 lines
8.3 KiB
Python
152 lines
8.3 KiB
Python
"""白话层(docs/design/normal-user-apps.md N10,app/plain.py)和反问收尾的服务端部分(N11、G6)。
|
||
|
||
- 覆盖:代码里出现的问题编号(工作流发布门禁 contract.* / governed.* / action.* / process.*、子工作流和口径卡固定的版本、
|
||
应用检查 app.*、蓝图 bp.*、模板 tpl.*、个人应用 personal.*、发布单 plan.*)都要有白话。B1、B3、B4 新增的编号由各包
|
||
自己补;这里扫的是代码里实际出现的全部编号,报出没有白话的那几个;
|
||
- 谁来改(who):要管理员的编号 → admin;自动修复给的是 choice → you,auto / assist → assistant;应用检查只有人能处理的
|
||
(APP_HUMAN_ONLY)→ you,其余 → assistant。白话里登记的那一档和这条规则一致(一种说法,P5);
|
||
- 文案:白话里不写「钉住」、不裸写配置键名(check-copy 同一套规则在 scripts/check-copy.mjs 里扫);
|
||
- 反问收尾:只问不改的一轮记进历史时,下一轮的上下文写「助手反问了什么」,不写「没有结果」;只问了一句的那一轮记的
|
||
空图不算「上一轮的图」。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from app.db import models # noqa: F401 - 建表要先登记全部模型(conftest 按 Base.metadata 建表)
|
||
from app.plain import ADMIN_CODES, PLAIN, PLAIN_PREFIX, plain_of, who_of, with_plain
|
||
|
||
APP = Path(__file__).resolve().parent.parent / "app"
|
||
#: 白话层要覆盖的编号前缀
|
||
PREFIXES = ("contract", "governed", "action", "process", "app", "bp", "tpl", "personal", "plan")
|
||
_CODE = r"((?:%s)\.[a-z_]+)" % "|".join(PREFIXES)
|
||
#: 问题编号在代码里出现的几种写法:code="x"、"code": "x"、out.error("x"、add("x"、flag 的元组写法、
|
||
#: 异常类的默认编号、模块级常量(PERSONAL_OFF = "personal.off")
|
||
PATTERNS = [
|
||
re.compile(r'\bcode\s*=\s*"%s"(?!\s*,\s*")' % _CODE),
|
||
re.compile(r'"code"\s*:\s*"%s"' % _CODE),
|
||
re.compile(r'\.(?:error|warning|info)\(\s*"%s"' % _CODE),
|
||
re.compile(r'\badd\(\s*"%s"' % _CODE),
|
||
re.compile(r'\bcode\s*=\s*"[^"]*",\s*"%s"' % _CODE),
|
||
re.compile(r'\bcode\s*:\s*str\s*=\s*"%s"' % _CODE),
|
||
re.compile(r'^[A-Z][A-Z0-9_]*\s*(?::\s*\w+\s*)?=\s*"%s"' % _CODE, re.M),
|
||
re.compile(r'Refused\([^)]*?"%s"\s*\)' % _CODE),
|
||
]
|
||
#: 不是问题编号的同名写法:运行事件的种类(core/events.py 的 action.sent 一类)
|
||
SKIP_FILES = {APP / "core" / "events.py"}
|
||
|
||
|
||
def codes_in_code() -> dict[str, str]:
|
||
"""代码里出现的问题编号 → 第一次出现在哪个文件。"""
|
||
out: dict[str, str] = {}
|
||
for path in sorted(APP.rglob("*.py")):
|
||
if path in SKIP_FILES:
|
||
continue
|
||
text = path.read_text(encoding="utf-8")
|
||
for pattern in PATTERNS:
|
||
for code in pattern.findall(text):
|
||
out.setdefault(code, str(path.relative_to(APP)))
|
||
return out
|
||
|
||
|
||
def test_the_scan_finds_codes_in_every_family():
|
||
found = codes_in_code()
|
||
for code in ("contract.required_missing", "contract.report_from_invalid", "governed.metric_untraced",
|
||
"action.prod_missing", "process.human_in_subgraph", "app.text_number", "app.acl_empty",
|
||
"app.polish_refused", "plan.graph_changed", "plan.preflight", "bp.refused"):
|
||
assert code in found, code
|
||
# 事件种类不是问题编号
|
||
assert "action.sent" not in found
|
||
|
||
|
||
def test_every_problem_code_in_the_code_has_a_plain_wording():
|
||
missing = {code: where for code, where in codes_in_code().items() if plain_of(code) is None}
|
||
assert not missing, "这些问题编号还没有白话(app/plain.py):" + ";".join(
|
||
f"{code}({where})" for code, where in sorted(missing.items()))
|
||
|
||
|
||
def test_subgraph_and_caliber_version_codes_have_plain_wording():
|
||
from app.apps.versions import CALIBER_RETIRED, CALIBER_UNPUBLISHED, SUBGRAPH_RETIRED, SUBGRAPH_UNPUBLISHED
|
||
|
||
for code in (SUBGRAPH_UNPUBLISHED, SUBGRAPH_RETIRED, CALIBER_UNPUBLISHED, CALIBER_RETIRED):
|
||
assert plain_of(code) is not None, code
|
||
|
||
|
||
#: 工作流发布门禁、应用检查、发布单的白话(包 B2 写的):一句话、为什么要紧、怎么办三样都要有
|
||
FULL = ("contract.", "governed.", "action.", "process.", "app.", "plan.", "subgraph_version_", "caliber_version_")
|
||
|
||
|
||
def test_every_plain_wording_is_complete_and_follows_the_copy_rules():
|
||
keys = ("report_from", "metrics_from", "caliber_from", "on_timeout", "assignee", "upgrade_policy",
|
||
"evidence_role", "output_schema", "cite_fields")
|
||
for code, plain in [*PLAIN.items(), *PLAIN_PREFIX.items()]:
|
||
assert plain.title and plain.action, code
|
||
assert plain.why or not code.startswith(FULL), code
|
||
assert plain.who in ("you", "assistant", "admin"), code
|
||
text = plain.title + plain.why + plain.action
|
||
assert "钉" not in text and "跑" not in text, code
|
||
assert not any(k in text for k in keys), code
|
||
# 中文句子用全角标点
|
||
assert not re.search(r"[一-鿿][,;:?!]|[,;:?!][一-鿿]", text), code
|
||
|
||
|
||
def test_who_follows_one_rule():
|
||
from app.api.copilot_gen import APP_HUMAN_ONLY
|
||
|
||
# 应用检查(apps/lint.py 报的)在白话里登记的那一档:只有人能处理的「你来定」,其余「助手可以修」(助手的自查修它们)
|
||
lint = {code for code, where in codes_in_code().items() if where == "apps/lint.py"}
|
||
for code in lint:
|
||
assert PLAIN[code].who == ("you" if code in APP_HUMAN_ONLY else "assistant"), code
|
||
assert who_of({"code": "app.slot_unbound"}) == "you"
|
||
assert who_of({"code": "app.text_number"}) == "assistant"
|
||
# 自动修复给的种类:要人选的你来定,能自动修、能交给助手的助手可以修
|
||
assert who_of({"code": "contract.required_missing", "fix_kind": "choice"}) == "you"
|
||
assert who_of({"code": "governed.subgraph_unpinned", "fix_kind": "auto"}) == "assistant"
|
||
assert who_of({"code": "governed.supervisor", "fix_kind": "assist"}) == "assistant"
|
||
# 要管理员的:端点、能力登记、受管级和降级的发布
|
||
assert {"action.prod_missing", "action.reconcile_unplanned", "plan.admin_required", "plan.downgrade_admin"} \
|
||
<= ADMIN_CODES
|
||
assert who_of({"code": "action.prod_missing", "fix_kind": "assist"}) == "admin"
|
||
# 没登记白话的:照旧显示技术说法(不补 plain)
|
||
issue = {"code": "no.such_code", "message": "x"}
|
||
assert with_plain(issue) is issue
|
||
out = with_plain({"code": "subgraph_version_retired", "message": "x"})
|
||
assert out["plain"]["title"] and out["plain"]["who"] == "you"
|
||
|
||
|
||
@pytest.fixture
|
||
async def conversation():
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import Conversation, ConversationTurn
|
||
|
||
async with SessionLocal() as session:
|
||
conv = Conversation(title="反问")
|
||
session.add(conv)
|
||
await session.flush()
|
||
session.add(ConversationTurn(conversation_id=conv.id, seq=1, question="订单金额", answer="", status="done",
|
||
graph={"nodes": [], "edges": []},
|
||
meta={"asked": {"question": "按下单时间还是付款时间算?",
|
||
"options": ["下单时间", "付款时间"]}}))
|
||
# 问数据页的写法:meta.asked 只记 true,问题写在这一轮的答案里
|
||
session.add(ConversationTurn(conversation_id=conv.id, seq=2, question="付款时间", status="done",
|
||
answer="要不要去掉已退款的订单?", meta={"asked": True}))
|
||
await session.commit()
|
||
return conv.id
|
||
|
||
|
||
async def test_history_says_what_the_assistant_asked(conversation):
|
||
from app.api.copilot import _history_section, _previous_graph
|
||
from app.db.base import SessionLocal
|
||
|
||
async with SessionLocal() as session:
|
||
text = await _history_section(session, conversation)
|
||
previous = await _previous_graph(session, conversation)
|
||
assert "助手反问" in text and "按下单时间还是付款时间算?" in text and "下单时间、付款时间" in text
|
||
assert "(助手反问,等用户回答:要不要去掉已退款的订单?)" in text
|
||
assert "(没有结果)" not in text
|
||
# 只问了一句的那一轮记的是空图:不算「上一轮的图」
|
||
assert previous is None and "上一轮用的工作流" not in text
|