feat(awooop): filter runs by callback reply state
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m9s
CD Pipeline / build-and-deploy (push) Successful in 3m53s
CD Pipeline / post-deploy-checks (push) Successful in 1m40s

This commit is contained in:
Your Name
2026-05-18 15:54:21 +08:00
parent e81e3f7b8a
commit f3494e0bfb
6 changed files with 133 additions and 8 deletions

View File

@@ -98,7 +98,8 @@ class DecideApprovalResponse(BaseModel):
response_model=ListRunsResponse,
summary="列出 Runs",
description=(
"返回 awooop_run_state 記錄,支援 project_id / state / remediation_status / incident_id filter 與分頁。\n\n"
"返回 awooop_run_state 記錄,支援 project_id / state / remediation_status / "
"callback_reply_status / incident_id filter 與分頁。\n\n"
"- 按 created_at DESC 排序\n"
"- 注意:此路徑為 /runs/list 以避免與 runs.py 的 /runs/{run_id} 衝突"
),
@@ -110,6 +111,10 @@ async def list_runs(
None,
description="AI 證據狀態 filterno_evidence/mcp_observed/read_only_dry_run/write_observed/blocked/observed",
),
callback_reply_status: str | None = Query(
None,
description="Telegram callback reply 狀態 filterno_callback/sent/fallback_sent/rescue_sent/failed/observed",
),
incident_id: str | None = Query(None, description="關聯 Incident ID filter可選"),
page: int = Query(1, ge=1, description="頁碼,從 1 開始"),
per_page: int = Query(_DEFAULT_PER_PAGE, ge=1, le=_MAX_PER_PAGE, description="每頁筆數"),
@@ -118,6 +123,7 @@ async def list_runs(
project_id=project_id,
state=state,
remediation_status=remediation_status,
callback_reply_status=callback_reply_status,
incident_id=incident_id,
page=page,
per_page=per_page,

View File

@@ -57,6 +57,14 @@ _REMEDIATION_STATUS_FILTERS = {
"blocked",
"observed",
}
_CALLBACK_REPLY_STATUS_FILTERS = {
"no_callback",
"sent",
"fallback_sent",
"rescue_sent",
"failed",
"observed",
}
# =============================================================================
# Tenants
@@ -146,12 +154,14 @@ async def list_runs(
project_id: str | None,
state: str | None,
remediation_status: str | None,
callback_reply_status: str | None,
incident_id: str | None,
page: int,
per_page: int,
) -> dict[str, Any]:
"""列出 runs支援 project_id、state、remediation_status、incident_id filter 與分頁。"""
"""列出 runs支援 project/state/evidence/callback/incident filter 與分頁。"""
_validate_remediation_status_filter(remediation_status)
_validate_callback_reply_status_filter(callback_reply_status)
_validate_incident_id_filter(incident_id)
async with get_db_context("awoooi") as db:
@@ -162,7 +172,7 @@ async def list_runs(
stmt = stmt.where(AwoooPRunState.state == state)
offset = (page - 1) * per_page
if remediation_status or incident_id:
if remediation_status or incident_id or callback_reply_status:
result = await db.execute(stmt)
candidate_rows = list(result.scalars().all())
context_limit = _list_filter_context_limit(len(candidate_rows))
@@ -176,6 +186,10 @@ async def list_runs(
inbound_by_run=inbound_by_run,
outbound_by_run=outbound_by_run,
)
callback_reply_summaries = {
row.run_id: _run_callback_reply_summary(outbound_by_run.get(row.run_id, []))
for row in candidate_rows
}
filtered_rows = [
row
for row in candidate_rows
@@ -187,6 +201,10 @@ async def list_runs(
remediation_summaries.get(row.run_id),
incident_id,
)
and _callback_reply_summary_matches_status(
callback_reply_summaries.get(row.run_id),
callback_reply_status,
)
]
total = len(filtered_rows)
rows = filtered_rows[offset : offset + per_page]
@@ -204,6 +222,10 @@ async def list_runs(
inbound_by_run=inbound_by_run,
outbound_by_run=outbound_by_run,
)
callback_reply_summaries = {
row.run_id: _run_callback_reply_summary(outbound_by_run.get(row.run_id, []))
for row in rows
}
runs = [
{
@@ -217,9 +239,7 @@ async def list_runs(
"created_at": r.created_at,
"timeout_at": r.timeout_at,
"remediation_summary": remediation_summaries.get(r.run_id),
"callback_reply_summary": _run_callback_reply_summary(
outbound_by_run.get(r.run_id, [])
),
"callback_reply_summary": callback_reply_summaries.get(r.run_id),
}
for r in rows
]
@@ -738,6 +758,17 @@ def _validate_remediation_status_filter(value: str | None) -> None:
)
def _validate_callback_reply_status_filter(value: str | None) -> None:
if value is None:
return
if value not in _CALLBACK_REPLY_STATUS_FILTERS:
allowed = ", ".join(sorted(_CALLBACK_REPLY_STATUS_FILTERS))
raise HTTPException(
status_code=422,
detail=f"callback_reply_status 必須是: {allowed}",
)
def _validate_incident_id_filter(value: str | None) -> None:
if value is None:
return
@@ -758,6 +789,16 @@ def _remediation_summary_matches_status(
return status_value == remediation_status
def _callback_reply_summary_matches_status(
summary: dict[str, Any] | None,
callback_reply_status: str | None,
) -> bool:
if callback_reply_status is None:
return True
status_value = str((summary or {}).get("status") or "no_callback")
return status_value == callback_reply_status
def _remediation_summary_matches_incident_id(
summary: dict[str, Any] | None,
incident_id: str | None,