feat(v2): T10 轨迹回放与会议结果复盘——只读轨迹、unmatched 单列、更正追加
覆盖 AC-22/32/33(BR-13/21/31/32、E-05/23/26)。
后端:
- contracts/trajectory.py:TrajectoryStep 与 ReviewOutcome。
TrajectoryStep 失败必有错误码、成本未知时币种留空、observable() 给出
可读字段白名单(机器可读地排除任何推理过程字段);
ReviewOutcome 只在 matched 且有明确映射口径时才有二元标签,附条件/撤回/
未送审一律留空,unmatched 不得挂快照。
- services/trajectory.py:
· project_steps 把既有 run 的步骤投影成轨迹(顺序即 sequence,部分完成
如实标注,失败保留,成本缺省留空不填 0);
· replay 只读回放并按 sequence 稳定分页,不存在或跨项目抛 KeyError;
· record 追加会议结果:未知快照记 unmatched,更正要指向本项目既有记录,
链必须能终止;denominator 只数 matched 且有通过/否决口径的记录。
- api/prescreen.py:新增轨迹回放、会议结果列表与追加三个端点。
未做:轨迹写入仍由既有 DurableWorker.save_step 负责(本批不重做运行执行),
因此目前没有 judgment/rule 类步骤的产出方;会议标签映射(E-26)仍属 T11。
验证:backend 全量 unittest 494 项 OK(T09 时 469 项,无回归)。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ from api.project_assistant import enabled
|
||||
from api.risk_agent_access import authorize, check_project
|
||||
from contracts.prescreen import (
|
||||
EVIDENCE_MAX,
|
||||
ID_MAX,
|
||||
MAX_BATCH_FILES,
|
||||
NOTE_MAX,
|
||||
ErrorCode,
|
||||
@@ -41,7 +42,7 @@ from contracts.prescreen import (
|
||||
S1_ERROR_RETRYABLE,
|
||||
)
|
||||
from services import assistant_store as store
|
||||
from services import fact_conflict, material_import, prescreen_sandbox, prescreen_store
|
||||
from services import fact_conflict, material_import, prescreen_sandbox, prescreen_store, trajectory
|
||||
from services.assistant_settings import flags
|
||||
from services.fact_conflict import FactConflictUnresolved
|
||||
from services.project_assistant import ProjectAssistantService
|
||||
@@ -91,6 +92,20 @@ class SimulationInput(Input):
|
||||
expected_revision: int | None = Field(default=None, ge=1)
|
||||
|
||||
|
||||
class OutcomeInput(Input):
|
||||
"""T10 会议结果。原始结果码必填;未提供时不得推断成通过或否决。"""
|
||||
|
||||
raw_code: str = Field(min_length=1, max_length=100)
|
||||
meeting_id: str = Field(min_length=1, max_length=ID_MAX)
|
||||
normalized: str | None = Field(default=None, max_length=40)
|
||||
occurred_at: str | None = None
|
||||
reviewed_snapshot_id: str | None = Field(default=None, max_length=ID_MAX)
|
||||
supersedes_id: str | None = Field(default=None, max_length=ID_MAX)
|
||||
mapping_ref: dict | None = None
|
||||
conditions: list[str] = Field(default_factory=list, max_length=50)
|
||||
evidence_ids: list[str] = Field(default_factory=list, max_length=EVIDENCE_MAX)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 错误信封
|
||||
def _envelope(request: Request, code: ErrorCode, message: str) -> JSONResponse:
|
||||
request_id = request.headers.get("x-request-id") or store.uid("req")
|
||||
@@ -452,3 +467,54 @@ def create_simulation(project_id: str, run_id: str, payload: SimulationInput,
|
||||
return result
|
||||
|
||||
return run(request, action)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 轨迹与会议结果(T10)
|
||||
@router.get("/projects/{project_id}/runs/{run_id}/trajectory")
|
||||
def get_trajectory(project_id: str, run_id: str, request: Request,
|
||||
cursor: str | None = None, limit: int = 50):
|
||||
"""只读轨迹回放。失败的步骤照常返回,不做美化过滤。"""
|
||||
blocked = scope(request, project_id)
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
|
||||
def action():
|
||||
with _db() as db:
|
||||
return trajectory.replay(db, project_id, run_id, cursor, _limit(limit))
|
||||
|
||||
return run(request, action)
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/review-outcomes")
|
||||
def list_review_outcomes(project_id: str, request: Request, status: str | None = None):
|
||||
"""会议结果列表。unmatched 单列,不混进已观察通过率的分母。"""
|
||||
blocked = scope(request, project_id)
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
if status not in (None, "matched", "unmatched"):
|
||||
return _envelope(request, ErrorCode.INVALID_REQUEST,
|
||||
"状态取值须为 matched 或 unmatched")
|
||||
|
||||
def action():
|
||||
with _db() as db:
|
||||
return trajectory.listing(db, project_id, status)
|
||||
|
||||
return run(request, action)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/review-outcomes", status_code=201)
|
||||
def record_review_outcome(project_id: str, payload: OutcomeInput, request: Request):
|
||||
"""追加会议结果。找不到材料版本的一律记 unmatched;更正只能新增。"""
|
||||
blocked = scope(request, project_id)
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
|
||||
def action():
|
||||
with _db() as db:
|
||||
runs = store.listing(db, "run", project_id)
|
||||
snapshots = [item.get("input_snapshot_id") for item in runs
|
||||
if item.get("input_snapshot_id")]
|
||||
return trajectory.record(db, project_id, payload.model_dump(), _actor(request),
|
||||
known_snapshots=snapshots)
|
||||
|
||||
return run(request, action)
|
||||
|
||||
Reference in New Issue
Block a user