feat(awooop): filter runs by remediation evidence
This commit is contained in:
@@ -97,7 +97,7 @@ class DecideApprovalResponse(BaseModel):
|
||||
response_model=ListRunsResponse,
|
||||
summary="列出 Runs",
|
||||
description=(
|
||||
"返回 awooop_run_state 記錄,支援 project_id / state filter 與分頁。\n\n"
|
||||
"返回 awooop_run_state 記錄,支援 project_id / state / remediation_status filter 與分頁。\n\n"
|
||||
"- 按 created_at DESC 排序\n"
|
||||
"- 注意:此路徑為 /runs/list 以避免與 runs.py 的 /runs/{run_id} 衝突"
|
||||
),
|
||||
@@ -105,11 +105,19 @@ class DecideApprovalResponse(BaseModel):
|
||||
async def list_runs(
|
||||
project_id: str | None = Query(None, description="租戶 ID(可選)"),
|
||||
state: str | None = Query(None, description="Run 狀態 filter(可選)"),
|
||||
remediation_status: str | None = Query(
|
||||
None,
|
||||
description="AI 補救證據狀態 filter(no_evidence/read_only_dry_run/write_observed/blocked/observed)",
|
||||
),
|
||||
page: int = Query(1, ge=1, description="頁碼,從 1 開始"),
|
||||
per_page: int = Query(_DEFAULT_PER_PAGE, ge=1, le=_MAX_PER_PAGE, description="每頁筆數"),
|
||||
) -> dict[str, Any]:
|
||||
return await list_runs_svc(
|
||||
project_id=project_id, state=state, page=page, per_page=per_page
|
||||
project_id=project_id,
|
||||
state=state,
|
||||
remediation_status=remediation_status,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
)
|
||||
|
||||
|
||||
@@ -140,8 +148,16 @@ async def get_run_detail(
|
||||
async def list_approvals(
|
||||
project_id: str | None = Query(None, description="租戶 ID(可選)"),
|
||||
run_id: str | None = Query(None, description="Run ID(可選,M8 詳情頁查單筆)"),
|
||||
remediation_status: str | None = Query(
|
||||
None,
|
||||
description="AI 補救證據狀態 filter(no_evidence/read_only_dry_run/write_observed/blocked/observed)",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
return await list_approvals_svc(project_id=project_id, run_id=run_id)
|
||||
return await list_approvals_svc(
|
||||
project_id=project_id,
|
||||
run_id=run_id,
|
||||
remediation_status=remediation_status,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
|
||||
@@ -45,6 +45,13 @@ _MAX_LIST_CONTEXT_ROWS = 500
|
||||
_MAX_STEP_SUMMARY_CHARS = 128
|
||||
_REMEDIATION_HISTORY_LIMIT = 20
|
||||
_INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b")
|
||||
_REMEDIATION_STATUS_FILTERS = {
|
||||
"no_evidence",
|
||||
"read_only_dry_run",
|
||||
"write_observed",
|
||||
"blocked",
|
||||
"observed",
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Tenants
|
||||
@@ -133,10 +140,13 @@ async def list_contracts(
|
||||
async def list_runs(
|
||||
project_id: str | None,
|
||||
state: str | None,
|
||||
remediation_status: str | None,
|
||||
page: int,
|
||||
per_page: int,
|
||||
) -> dict[str, Any]:
|
||||
"""列出 runs,支援 project_id、state filter 與分頁。"""
|
||||
"""列出 runs,支援 project_id、state、remediation_status filter 與分頁。"""
|
||||
_validate_remediation_status_filter(remediation_status)
|
||||
|
||||
async with get_db_context("awoooi") as db:
|
||||
stmt = select(AwoooPRunState).order_by(AwoooPRunState.created_at.desc())
|
||||
if project_id is not None:
|
||||
@@ -144,22 +154,40 @@ async def list_runs(
|
||||
if state is not None:
|
||||
stmt = stmt.where(AwoooPRunState.state == state)
|
||||
|
||||
count_stmt = select(func.count()).select_from(stmt.subquery())
|
||||
total_result = await db.execute(count_stmt)
|
||||
total = total_result.scalar_one()
|
||||
|
||||
offset = (page - 1) * per_page
|
||||
stmt = stmt.offset(offset).limit(per_page)
|
||||
result = await db.execute(stmt)
|
||||
rows = list(result.scalars().all())
|
||||
if remediation_status:
|
||||
result = await db.execute(stmt)
|
||||
candidate_rows = list(result.scalars().all())
|
||||
inbound_by_run, outbound_by_run = await _load_run_message_context(db, candidate_rows)
|
||||
remediation_summaries = await _build_run_remediation_summaries(
|
||||
runs=candidate_rows,
|
||||
inbound_by_run=inbound_by_run,
|
||||
outbound_by_run=outbound_by_run,
|
||||
)
|
||||
filtered_rows = [
|
||||
row
|
||||
for row in candidate_rows
|
||||
if _remediation_summary_matches_status(
|
||||
remediation_summaries.get(row.run_id),
|
||||
remediation_status,
|
||||
)
|
||||
]
|
||||
total = len(filtered_rows)
|
||||
rows = filtered_rows[offset : offset + per_page]
|
||||
else:
|
||||
count_stmt = select(func.count()).select_from(stmt.subquery())
|
||||
total_result = await db.execute(count_stmt)
|
||||
total = total_result.scalar_one()
|
||||
|
||||
inbound_by_run, outbound_by_run = await _load_run_message_context(db, rows)
|
||||
|
||||
remediation_summaries = await _build_run_remediation_summaries(
|
||||
runs=rows,
|
||||
inbound_by_run=inbound_by_run,
|
||||
outbound_by_run=outbound_by_run,
|
||||
)
|
||||
stmt = stmt.offset(offset).limit(per_page)
|
||||
result = await db.execute(stmt)
|
||||
rows = list(result.scalars().all())
|
||||
inbound_by_run, outbound_by_run = await _load_run_message_context(db, rows)
|
||||
remediation_summaries = await _build_run_remediation_summaries(
|
||||
runs=rows,
|
||||
inbound_by_run=inbound_by_run,
|
||||
outbound_by_run=outbound_by_run,
|
||||
)
|
||||
|
||||
runs = [
|
||||
{
|
||||
@@ -471,6 +499,25 @@ def _run_remediation_list_summary(
|
||||
}
|
||||
|
||||
|
||||
def _validate_remediation_status_filter(value: str | None) -> None:
|
||||
if value is None:
|
||||
return
|
||||
if value not in _REMEDIATION_STATUS_FILTERS:
|
||||
allowed = ", ".join(sorted(_REMEDIATION_STATUS_FILTERS))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"remediation_status 必須是: {allowed}",
|
||||
)
|
||||
|
||||
|
||||
def _remediation_summary_matches_status(
|
||||
summary: dict[str, Any] | None,
|
||||
remediation_status: str,
|
||||
) -> bool:
|
||||
status_value = str((summary or {}).get("status") or "no_evidence")
|
||||
return status_value == remediation_status
|
||||
|
||||
|
||||
async def _build_run_remediation_summaries(
|
||||
*,
|
||||
runs: list[AwoooPRunState],
|
||||
@@ -984,8 +1031,11 @@ async def list_recent_channel_events(
|
||||
async def list_approvals(
|
||||
project_id: str | None,
|
||||
run_id: str | None = None,
|
||||
remediation_status: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""列出 waiting_approval runs,可依 project_id 或 run_id 篩選。"""
|
||||
"""列出 waiting_approval runs,可依 project_id / run_id / remediation_status 篩選。"""
|
||||
_validate_remediation_status_filter(remediation_status)
|
||||
|
||||
run_uuid: UUID | None = None
|
||||
if run_id:
|
||||
try:
|
||||
@@ -1021,6 +1071,16 @@ async def list_approvals(
|
||||
inbound_by_run=inbound_by_run,
|
||||
outbound_by_run=outbound_by_run,
|
||||
)
|
||||
if remediation_status:
|
||||
rows = [
|
||||
row
|
||||
for row in rows
|
||||
if _remediation_summary_matches_status(
|
||||
remediation_summaries.get(row.run_id),
|
||||
remediation_status,
|
||||
)
|
||||
]
|
||||
total = len(rows)
|
||||
|
||||
items = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user