fix(km): expose degraded readback reason
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m25s
CD Pipeline / build-and-deploy (push) Successful in 5m0s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Failing after 1m48s
CD Pipeline / post-deploy-checks (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m25s
CD Pipeline / build-and-deploy (push) Successful in 5m0s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Failing after 1m48s
CD Pipeline / post-deploy-checks (push) Has been cancelled
This commit is contained in:
@@ -22,7 +22,6 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from src.utils.timezone import now_taipei
|
from src.utils.timezone import now_taipei
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Enums
|
# Enums
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -135,6 +134,8 @@ class KnowledgeListResponse(BaseModel):
|
|||||||
categories: list[CategoryCount] = Field(default_factory=list)
|
categories: list[CategoryCount] = Field(default_factory=list)
|
||||||
asset_taxonomy: list[KnowledgeAssetTaxonomyCount] = Field(default_factory=list)
|
asset_taxonomy: list[KnowledgeAssetTaxonomyCount] = Field(default_factory=list)
|
||||||
readback_status: str = "ready"
|
readback_status: str = "ready"
|
||||||
|
primary_readback_ready: bool = True
|
||||||
|
degraded_reason_code: str | None = None
|
||||||
operator_stage: str | None = None
|
operator_stage: str | None = None
|
||||||
next_step: str | None = None
|
next_step: str | None = None
|
||||||
writes_on_read: bool = False
|
writes_on_read: bool = False
|
||||||
|
|||||||
@@ -210,6 +210,19 @@ _SOURCE_BACKED_KNOWLEDGE_SPECS: tuple[dict[str, object], ...] = (
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_knowledge_readback_degraded_reason(reason: str) -> str:
|
||||||
|
normalized = reason.lower()
|
||||||
|
if "pool" in normalized or "timeout" in normalized or "timed out" in normalized:
|
||||||
|
return "primary_km_db_timeout_or_pool_exhausted"
|
||||||
|
if "missing tenant context" in normalized or "project_id" in normalized or "unauthorized" in normalized:
|
||||||
|
return "primary_km_project_context_missing"
|
||||||
|
if "undefinedcolumn" in normalized or "does not exist" in normalized or "missing column" in normalized:
|
||||||
|
return "primary_km_schema_mismatch"
|
||||||
|
if "validation" in normalized or "pydantic" in normalized:
|
||||||
|
return "primary_km_legacy_row_validation"
|
||||||
|
return "primary_km_readback_exception"
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Singleton
|
# Singleton
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -245,6 +258,8 @@ def build_knowledge_list_readback_degraded_response(
|
|||||||
categories=_source_category_counts(entries),
|
categories=_source_category_counts(entries),
|
||||||
asset_taxonomy=_source_asset_taxonomy_counts(entries),
|
asset_taxonomy=_source_asset_taxonomy_counts(entries),
|
||||||
readback_status=readback_status,
|
readback_status=readback_status,
|
||||||
|
primary_readback_ready=False,
|
||||||
|
degraded_reason_code=_classify_knowledge_readback_degraded_reason(reason),
|
||||||
operator_stage="knowledge_readback_source_backed_ai_controlled_repair",
|
operator_stage="knowledge_readback_source_backed_ai_controlled_repair",
|
||||||
next_step=(
|
next_step=(
|
||||||
"repair_primary_km_db_readback_then_promote_source_backed_receipts_to_persistent_km"
|
"repair_primary_km_db_readback_then_promote_source_backed_receipts_to_persistent_km"
|
||||||
@@ -496,6 +511,7 @@ class KnowledgeService:
|
|||||||
total=total,
|
total=total,
|
||||||
categories=categories,
|
categories=categories,
|
||||||
asset_taxonomy=asset_taxonomy,
|
asset_taxonomy=asset_taxonomy,
|
||||||
|
primary_readback_ready=True,
|
||||||
)
|
)
|
||||||
except Exception as exc: # noqa: BLE001 - production readback must fail soft
|
except Exception as exc: # noqa: BLE001 - production readback must fail soft
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -555,9 +571,19 @@ class KnowledgeService:
|
|||||||
|
|
||||||
async def search(self, query: str, limit: int = 20) -> list[KnowledgeEntry]:
|
async def search(self, query: str, limit: int = 20) -> list[KnowledgeEntry]:
|
||||||
"""關鍵字搜尋"""
|
"""關鍵字搜尋"""
|
||||||
|
try:
|
||||||
async with get_db_context() as db:
|
async with get_db_context() as db:
|
||||||
repo: IKnowledgeRepository = KnowledgeDBRepository(db)
|
repo: IKnowledgeRepository = KnowledgeDBRepository(db)
|
||||||
return await repo.search(query, limit)
|
return await repo.search(query, limit)
|
||||||
|
except Exception as exc: # noqa: BLE001 - KM search must not 500 the UI
|
||||||
|
logger.warning(
|
||||||
|
"knowledge_search_readback_degraded",
|
||||||
|
error=str(exc),
|
||||||
|
q=query,
|
||||||
|
limit=limit,
|
||||||
|
degraded_reason_code=_classify_knowledge_readback_degraded_reason(str(exc)),
|
||||||
|
)
|
||||||
|
return _filter_source_backed_entries(q=query)[:limit]
|
||||||
|
|
||||||
async def semantic_search(
|
async def semantic_search(
|
||||||
self,
|
self,
|
||||||
@@ -630,7 +656,9 @@ class KnowledgeService:
|
|||||||
list[KnowledgeEntry] — ANTI_PATTERN 條目,空表示無已知失敗案例
|
list[KnowledgeEntry] — ANTI_PATTERN 條目,空表示無已知失敗案例
|
||||||
"""
|
"""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from sqlalchemy import text as sa_text
|
from sqlalchemy import text as sa_text
|
||||||
|
|
||||||
from src.utils.timezone import now_taipei
|
from src.utils.timezone import now_taipei
|
||||||
|
|
||||||
cutoff = now_taipei() - timedelta(days=days)
|
cutoff = now_taipei() - timedelta(days=days)
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ async def test_knowledge_list_entries_fails_soft_when_readback_breaks(monkeypatc
|
|||||||
]
|
]
|
||||||
assert all(row.count >= 1 for row in response.asset_taxonomy)
|
assert all(row.count >= 1 for row in response.asset_taxonomy)
|
||||||
assert response.readback_status == "source_backed_degraded"
|
assert response.readback_status == "source_backed_degraded"
|
||||||
|
assert response.primary_readback_ready is False
|
||||||
|
assert response.degraded_reason_code == "primary_km_db_timeout_or_pool_exhausted"
|
||||||
assert response.operator_stage == "knowledge_readback_source_backed_ai_controlled_repair"
|
assert response.operator_stage == "knowledge_readback_source_backed_ai_controlled_repair"
|
||||||
assert response.next_step == "repair_primary_km_db_readback_then_promote_source_backed_receipts_to_persistent_km"
|
assert response.next_step == "repair_primary_km_db_readback_then_promote_source_backed_receipts_to_persistent_km"
|
||||||
assert response.writes_on_read is False
|
assert response.writes_on_read is False
|
||||||
@@ -83,6 +85,26 @@ async def test_knowledge_list_entries_source_backed_filter_and_search(monkeypatc
|
|||||||
assert response.asset_taxonomy[[row.key for row in response.asset_taxonomy].index("alert")].count == 1
|
assert response.asset_taxonomy[[row.key for row in response.asset_taxonomy].index("alert")].count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_knowledge_search_fails_soft_to_source_backed_entries(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
knowledge_service_module,
|
||||||
|
"get_db_context",
|
||||||
|
lambda: _BrokenDbContext(),
|
||||||
|
)
|
||||||
|
service = KnowledgeService.__new__(KnowledgeService)
|
||||||
|
|
||||||
|
entries = await service.search("Telegram", limit=10)
|
||||||
|
|
||||||
|
entry_ids = {entry.id for entry in entries}
|
||||||
|
assert all(entry.id.startswith("source-backed-") for entry in entries)
|
||||||
|
assert {
|
||||||
|
"source-backed-service-telegram-alert-receipts",
|
||||||
|
"source-backed-alert-telegram-monitoring-coverage",
|
||||||
|
"source-backed-schedule-report-monitoring",
|
||||||
|
}.issubset(entry_ids)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_knowledge_categories_fails_soft_when_readback_breaks(monkeypatch) -> None:
|
async def test_knowledge_categories_fails_soft_when_readback_breaks(monkeypatch) -> None:
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
|||||||
Reference in New Issue
Block a user