Files
AgentLab/backend/app/api/ledger.py
T

103 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""账本(BE-3,只读):工作流版本被哪些应用钉住、生效计数;生效记录查询。设计文档 §2.9 L4。
不另建存储:ActionRecord 本身带着工作流、版本、graph_hash、能力版本、payload_hash、回执;「口径 → 应用」就是固定这一版的
应用版本。用在发布弹窗、退役确认、版本列表三处。
口径账本(docs/design/next-round/02-口径账本.md)在这里加两个只读接口:
- GET /api/runs/{run_id}/ledger(runs_router,和证据同一道门):一次运行里每个数的口径标签、整题判断、每条查询的比对、能做的
动作(app/ledger/view.py);
- GET /api/ledger/stats(router,搭建者):账本自身给出的统计 E1–E4,从比对工件、运行、审计现算(app/ledger/stats.py)。
"""
from __future__ import annotations
from typing import Any
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.base import get_session
from app.db.models import Workflow
from app.db.models_process import ActionRecord
router = APIRouter(prefix="/api/ledger", tags=["ledger"])
#: 口径账本里按运行读的那几个(一个答案里每个数的口径标签等,docs/design/next-round/02-口径账本.md):挂在
#: /api/runs/{run_id}/ledger… 下,和证据接口同一道门(main.py 挂 evidence_scope:使用者只读、而且要能读这次运行)
runs_router = APIRouter(prefix="/api/runs", tags=["ledger"])
#: 按口径正式出具、口径对账这类要写的(docs/design/next-round/02-口径账本.md 做法 5、6):使用者也能发起(总方案拍板
#: 账本-L5:对账都可以,正式出具只限有权使用的应用固定着的口径版本),所以 main.py 只挂登录即可,接口里按运行和应用授权再判
answers_router = APIRouter(prefix="/api/ledger/answers", tags=["ledger"])
@router.get("/workflow-versions/{workflow_id}/{version}")
async def workflow_version(workflow_id: str, version: int, session: AsyncSession = Depends(get_session)
) -> dict[str, Any]:
"""一个工作流版本被哪些应用固定、生效记录数、正式运行数。"""
from app.apps.versions import usage
if await session.get(Workflow, workflow_id) is None:
raise HTTPException(404, "工作流不存在,可能已被删除")
return await usage(session, workflow_id, version)
def effect_row(r: ActionRecord, *, receipt: bool = True) -> dict[str, Any]:
return {"id": r.id, "run_id": r.run_id, "node_id": r.node_id, "app_id": r.app_id, "workflow_id": r.workflow_id,
"workflow_version": r.workflow_version, "graph_hash": r.graph_hash, "capability": r.capability,
"capability_version": r.capability_version, "environment": r.environment, "state": r.state,
"attempt": r.attempt, "payload_hash": r.payload_hash, "preview": r.preview,
"receipt": r.receipt if receipt else None, "error": r.error, "resolution": r.resolution,
"created_at": r.created_at.isoformat() if r.created_at else None,
"updated_at": r.updated_at.isoformat() if r.updated_at else None}
@router.get("/effects")
async def effects(workflow_id: str | None = None, version: int | None = None, app_id: str | None = None,
state: str | None = None, capability: str | None = None,
limit: int = Query(default=100, ge=1, le=500), session: AsyncSession = Depends(get_session)
) -> dict[str, Any]:
"""生效记录,新的在前。演练(simulated)照样列出,状态写明是演练。"""
query = select(ActionRecord)
if workflow_id:
query = query.where(ActionRecord.workflow_id == workflow_id)
if version is not None:
query = query.where(ActionRecord.workflow_version == version)
if app_id:
query = query.where(ActionRecord.app_id == app_id)
if state:
query = query.where(ActionRecord.state == state)
if capability:
query = query.where(ActionRecord.capability == capability)
rows = (await session.execute(query.order_by(ActionRecord.created_at.desc()).limit(limit))).scalars()
return {"items": [effect_row(r) for r in rows]}
# --------------------------------------------------------------------------
# 口径账本(next-round 02)
# --------------------------------------------------------------------------
@runs_router.get("/{run_id}/ledger")
async def run_ledger(run_id: str) -> dict[str, Any]:
"""一次运行的口径账本:每个数来自哪一份口径(等价已证明 / 助手选用)、相关口径差在哪、这次的推理、没比对的原因;整题判断;
每条查询和哪几份口径比过。比对在封存之后做(note 写明不在封存范围内);没比对过、能比的运行这时排上按需比对,答复
state=pending。绑定的值是这次运行查到过的遮罩值时不给值,差异条目里的 SQL 片段按同一套遮罩换掉。"""
from app.ledger.view import run_view
return await run_view(run_id)
@router.get("/stats")
async def ledger_stats(since: datetime | None = Query(default=None, description="只算这之后发起的运行(ISO 时间)"),
until: datetime | None = Query(default=None, description="只算这之前发起的运行"),
origin: str | None = Query(default=None, description="逗号分隔的运行来源,缺省是探索运行的几种"),
session: AsyncSession = Depends(get_session)) -> dict[str, Any]:
"""账本自身给出的统计 E1–E4(app/ledger/stats.compute_stats):口径命中、对账、重复度、召回。全部现算,不另存;评测页
「口径命中与重复度」直接调同一个函数。"""
from app.api.deps import current_principal
from app.ledger.stats import compute_stats
origins = [o.strip() for o in (origin or "").split(",") if o.strip()] or None
return await compute_stats(session, since=since, until=until, origins=origins, principal=current_principal())