fix(api): include auto repair executions in AI stats
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m36s
CD Pipeline / build-and-deploy (push) Successful in 4m55s
CD Pipeline / post-deploy-checks (push) Successful in 1m34s

This commit is contained in:
Your Name
2026-06-27 16:01:54 +08:00
parent 37620ef8ad
commit 9132525d3c
3 changed files with 76 additions and 4 deletions

View File

@@ -122,6 +122,11 @@ class AIPerformance(BaseModel):
effectiveness_distribution: dict[int, int] = Field(
description="有效性評分分佈 {1: count, 2: count, ...}"
)
outcome_proposal_count: int = Field(default=0, description="Incident outcome 舊提案數")
outcome_executed_count: int = Field(default=0, description="Incident outcome 舊執行數")
auto_repair_total: int = Field(default=0, description="自動修復執行紀錄數")
auto_repair_success: int = Field(default=0, description="自動修復成功紀錄數")
source: str = Field(default="incident_outcome", description="AI 效能資料來源")
class ServiceImpact(BaseModel):

View File

@@ -27,7 +27,7 @@ from sqlalchemy import func, select
from src.core.redis_client import get_redis
from src.db.base import get_db_context
from src.db.models import IncidentRecord
from src.db.models import AutoRepairExecution, IncidentRecord
from src.models.incident import IncidentStatus
logger = structlog.get_logger(__name__)
@@ -343,12 +343,26 @@ class StatsService:
)
outcomes = [row[0] for row in result.all() if row[0]]
total = len(outcomes)
executed = sum(1 for o in outcomes if o.get("proposal_executed"))
success = sum(
outcome_total = len(outcomes)
outcome_executed = sum(1 for o in outcomes if o.get("proposal_executed"))
outcome_success = sum(
1 for o in outcomes if o.get("proposal_executed") and o.get("execution_success")
)
auto_result = await db.execute(
select(
AutoRepairExecution.success,
AutoRepairExecution.execution_time_ms,
).where(AutoRepairExecution.created_at >= since)
)
auto_rows = auto_result.all()
auto_total = len(auto_rows)
auto_success = sum(1 for row in auto_rows if row[0] is True)
total = max(outcome_total, auto_total)
executed = max(outcome_executed, auto_total)
success = auto_success if auto_total > 0 else outcome_success
effectiveness_dist: dict[int, int] = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
scores = []
for o in outcomes:
@@ -367,6 +381,13 @@ class StatsService:
"success_rate": round((success / executed * 100) if executed > 0 else 0, 2),
"avg_effectiveness": round(avg_effectiveness, 2) if avg_effectiveness else None,
"effectiveness_distribution": effectiveness_dist,
"outcome_proposal_count": outcome_total,
"outcome_executed_count": outcome_executed,
"auto_repair_total": auto_total,
"auto_repair_success": auto_success,
"source": "auto_repair_executions+incident_outcome"
if auto_total > 0
else "incident_outcome",
}
return await self.get_cached_or_compute(cache_key, compute)

View File

@@ -22,11 +22,27 @@ class _EmptyResult:
return []
class _RowsResult:
def __init__(self, rows: list[tuple]) -> None:
self._rows = rows
def all(self) -> list[tuple]:
return self._rows
class _FakeDb:
async def execute(self, statement) -> _EmptyResult:
return _EmptyResult()
class _QueuedDb:
def __init__(self, results: list[_RowsResult]) -> None:
self._results = results
async def execute(self, statement) -> _RowsResult:
return self._results.pop(0)
@pytest.mark.asyncio
async def test_stats_service_uses_default_project_for_db_context(monkeypatch) -> None:
seen_project_ids: list[str | None] = []
@@ -68,3 +84,33 @@ async def test_stats_service_allows_explicit_project_for_db_context(monkeypatch)
assert result["total_proposals"] == 0
assert seen_project_ids == ["vibework"]
assert seen_cache_keys == ["stats:vibework:ai_performance:7"]
@pytest.mark.asyncio
async def test_ai_performance_uses_auto_repair_execution_truth(monkeypatch) -> None:
seen_project_ids: list[str | None] = []
db = _QueuedDb(
[
_RowsResult([]),
_RowsResult([(True, 1200), (False, 800), (True, 500)]),
]
)
@asynccontextmanager
async def fake_db_context(project_id: str | None = None) -> AsyncGenerator[_QueuedDb, None]:
seen_project_ids.append(project_id)
yield db
monkeypatch.setattr(stats_service_module, "get_db_context", fake_db_context)
monkeypatch.setattr(stats_service_module, "get_redis", lambda: _NoopRedis())
result = await StatsService().get_ai_performance(days=7)
assert seen_project_ids == ["awoooi"]
assert result["total_proposals"] == 3
assert result["executed_count"] == 3
assert result["success_count"] == 2
assert result["success_rate"] == 66.67
assert result["auto_repair_total"] == 3
assert result["auto_repair_success"] == 2
assert result["source"] == "auto_repair_executions+incident_outcome"