fix(api): include auto repair executions in AI stats
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user