mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 05:31:59 +00:00
b3dcddb52e
- W4R-01:召回只查新版图里会不会写外部系统(apps/backfill 抽出 graph_external_write,graph_side_effect 复用它)。召回图只有两版
的取数和口径卡,图里别处的人工节点、按流程使用的问题碰不到它;口径卡上游的人工节点 derive 本来就认作数据部分不能复用。
「按新版重新出具」走完整的正式运行,仍按 graph_side_effect 全套拒绝。原来带审批的口径升版后一组都不召回,L6 对它们失效。
- W4R-09:收尾时召回卡读不出、或缺了某个比较的旧值 / 新值,照实记重算失败(读不出召回卡),原答案一个不动。原来两边都是空,
按渲染比被当成「没变」,给每个原答案永久盖上「已核对,不影响」,收过尾之后再也不核对。
- W4R-12:这一版已经不是当前发布版(之后又发布了更新的版本)时不再发起召回、不给接着算,批次里写 continue_reason,接着算的
接口 409 带同一句。同启动时接着做只认当前发布版:新版本的召回覆盖更早的所有版本,再按旧版开「口径升到 vN」的待办只是添乱。
- W4R-13:开登录时一组答案几个主人就几条待办,全局渠道却按这一批(recall.batch_of_key:同一次召回、同一个来由)只在最早开的
那条上推一次,答案、待办按这一批汇总,几条的链到待办箱;别的几条连 task.opened 也不再推全局渠道。个人渠道照旧每人一条。
- W4R-11:目录审阅之后的核对按数据源排队、批量审阅的几张表合成一遍(catalog_pass,同一数据源一把锁);查询快照按数据源在库里
筛、按运行结束时间分页、每张表凑够 BASIS_MAX_RUNS 次运行就停;冻结的表结构快照这一遍里按工件 id 只读一次。原来批量审阅
几十张表就同时起几十个后台任务,每个都全表扫查询快照、反复读同一份几 MB 的表结构快照。
- W4R-03(本包那一半):GET /api/runs/{id}/ledger 加 recall:checked(已按 vN 核对,不影响)、basis(依据有变:哪一种、几项、
新出现几条错误级问题)、tasks(看的这个人手上说的是这个答案、还没处理的 recall 待办)。不放数、不放表名。几种状态都带。
- W4R-10(本包那一半):新事件 ledger.feedback(对账运行上的封存后事件),POST /api/ledger/answers/{run}/reconcile/{rid}/feedback
{verdict: useful | unclear} 记反馈,同一个人改主意只算最后一次(app/ledger/feedback.py);账本接口的 reconciled 每一项带
feedback {mine, useful, unclear};统计 E2 的 feedback 数到它,去掉「第 4 波接上」的占位说法。离线核对页、封存后事件摘要
都认得这个事件。
测试:test_ledger_recall 新增七个用例、改了人工节点那一个(召回照算、重新出具照实拒绝);新增 test_ledger_feedback;
test_ledger_api 的 E2 断言跟上。每个修复都先写失败的测试、去掉修复回跑确认抓得住。后端全量 14237 通过。
109 lines
5.7 KiB
Python
109 lines
5.7 KiB
Python
"""对账结果上的「说明有用 / 看不懂」(docs/design/next-round/02-口径账本.md 做法 10 的 E2)。
|
||
|
||
对账(ledger/reconcile)把这次的答案和一份相关口径在同一份数据上各算一次,结果卡片上写差在哪、差多少。看的人觉得这段
|
||
说明有没有帮他看懂,点一下「说明有用」或「看不懂」——这是 E2 里唯一要人给的数,衡量差异说明写得好不好。
|
||
|
||
- 记在哪:对账运行上的封存后事件 ledger.feedback({verdict, by, verified, source_run},engine/run_append,post_seal)。
|
||
运行本身就是记录,不另建反馈表(同统计「全部现算、不另存」);批注不是证据,封存核对照旧一致。
|
||
- 谁能给:能读这个答案的人(同发起对账,ledger/reconcile:使用者对自己的答案也能对账);对账要是这个答案的(origin.source_run),
|
||
而且已经跑完封存。
|
||
- 怎么数:同一个人(开登录时按用户名;关登录时署名是自报的,按署名、没署名的算同一个)改主意再记一条,只算他最后一次。
|
||
账本接口 reconciled 每一项带上 {mine, useful, unclear}(feedback_of),统计 E2 数整个窗口(counts)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Collection, Iterable
|
||
from typing import Any
|
||
|
||
from sqlalchemy import select
|
||
|
||
from app.core.events import EventType
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import Run, RunEvent
|
||
|
||
#: 两种反馈
|
||
VERDICTS = ("useful", "unclear")
|
||
#: 被拒的机器码
|
||
REFUSED = "ledger_feedback_refused"
|
||
NOT_FOUND = "这次对账不存在,或者不是这个答案的对账"
|
||
NO_ACCESS = "你没有权限查看这个答案"
|
||
NOT_SEALED = "对账还没运行完,等结果出来再评说明有没有用"
|
||
BAD_VERDICT = "反馈只有两种:说明有用、看不懂"
|
||
|
||
|
||
class FeedbackRefused(Exception):
|
||
def __init__(self, status_code: int, message: str, code: str = REFUSED) -> None:
|
||
super().__init__(message)
|
||
self.status_code = status_code
|
||
self.message = message
|
||
self.code = code
|
||
|
||
|
||
def _who(principal: Any) -> str:
|
||
return str(getattr(principal, "username", None) or "")
|
||
|
||
|
||
async def give(answer_id: str, reconcile_id: str, verdict: str, *, principal: Any) -> dict[str, Any]:
|
||
"""记一条反馈(见模块说明),交回这次对账现在的反馈 {mine, useful, unclear}。不能记的抛 FeedbackRefused。"""
|
||
from app.api.deps import can_read_run
|
||
from app.engine.run_append import append_events
|
||
from app.ledger.reconcile import ORIGIN as RECONCILE
|
||
|
||
if verdict not in VERDICTS:
|
||
raise FeedbackRefused(422, BAD_VERDICT)
|
||
async with SessionLocal() as session:
|
||
answer = await session.get(Run, answer_id)
|
||
rec = await session.get(Run, reconcile_id)
|
||
origin = rec.origin if rec is not None and isinstance(rec.origin, dict) else {}
|
||
if answer is None or rec is None or rec.origin_kind != RECONCILE or origin.get("source_run") != answer_id:
|
||
raise FeedbackRefused(404, NOT_FOUND)
|
||
if getattr(principal, "mode", "off") != "off" and not await can_read_run(principal, answer):
|
||
raise FeedbackRefused(403, NO_ACCESS)
|
||
data = {"verdict": verdict, "by": _who(principal) or None, "verified": bool(getattr(principal, "verified", False)),
|
||
"source_run": answer_id}
|
||
if not await append_events(reconcile_id, [(EventType.LEDGER_FEEDBACK, None, data)], require="sealed"):
|
||
raise FeedbackRefused(409, NOT_SEALED)
|
||
async with SessionLocal() as session:
|
||
return (await feedback_of(session, [reconcile_id], principal))[reconcile_id]
|
||
|
||
|
||
async def _latest(session: Any, run_ids: Collection[str]) -> dict[str, dict[str, str]]:
|
||
"""{对账运行: {谁: 他最后一次给的}}。"""
|
||
out: dict[str, dict[str, str]] = {}
|
||
if not run_ids:
|
||
return out
|
||
rows = (await session.execute(select(RunEvent.run_id, RunEvent.data).where(
|
||
RunEvent.run_id.in_(list(run_ids)), RunEvent.type == EventType.LEDGER_FEEDBACK).order_by(RunEvent.seq))).all()
|
||
for run_id, data in rows:
|
||
if isinstance(data, dict) and data.get("verdict") in VERDICTS:
|
||
out.setdefault(run_id, {})[str(data.get("by") or "")] = str(data["verdict"])
|
||
return out
|
||
|
||
|
||
async def feedback_of(session: Any, run_ids: Iterable[str], principal: Any) -> dict[str, dict[str, Any]]:
|
||
"""每次对账的反馈:{对账运行: {mine: 看的这个人最后一次给的(没给过为 None), useful: 几个人, unclear: 几个人}}。"""
|
||
ids = list(dict.fromkeys(run_ids))
|
||
latest = await _latest(session, ids)
|
||
me = _who(principal)
|
||
out: dict[str, dict[str, Any]] = {}
|
||
for run_id in ids:
|
||
votes = latest.get(run_id, {})
|
||
out[run_id] = {"mine": votes.get(me), "useful": sum(v == "useful" for v in votes.values()),
|
||
"unclear": sum(v == "unclear" for v in votes.values())}
|
||
return out
|
||
|
||
|
||
async def counts(session: Any, run_ids: Collection[str]) -> dict[str, Any]:
|
||
"""统计 E2 的反馈(ledger/stats._e2):这些对账运行上,说明有用、看不懂各几个(每人只算最后一次),给过反馈的对账几次,
|
||
有用的比率(分母为 0 时为 None)。"""
|
||
latest = await _latest(session, run_ids)
|
||
useful = sum(v == "useful" for votes in latest.values() for v in votes.values())
|
||
unclear = sum(v == "unclear" for votes in latest.values() for v in votes.values())
|
||
total = useful + unclear
|
||
return {"available": True, "useful": useful, "unclear": unclear, "runs": sum(1 for v in latest.values() if v),
|
||
"useful_rate": round(useful / total, 4) if total else None}
|
||
|
||
|
||
__all__ = ["BAD_VERDICT", "NOT_FOUND", "NOT_SEALED", "REFUSED", "VERDICTS", "FeedbackRefused", "counts", "feedback_of",
|
||
"give"]
|