feat(km): add full corpus asset taxonomy readback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
This commit is contained in:
@@ -20,6 +20,7 @@ from fastapi import APIRouter, HTTPException, Query
|
||||
from src.models.knowledge import (
|
||||
EntryStatus,
|
||||
EntryType,
|
||||
KnowledgeAssetTaxonomyCount,
|
||||
KnowledgeEntry,
|
||||
KnowledgeEntryCreate,
|
||||
KnowledgeEntryUpdate,
|
||||
@@ -105,6 +106,13 @@ async def get_categories() -> list[dict]:
|
||||
return [cat.model_dump() for cat in cats]
|
||||
|
||||
|
||||
@router.get("/asset-taxonomy", response_model=list[KnowledgeAssetTaxonomyCount])
|
||||
async def get_asset_taxonomy() -> list[KnowledgeAssetTaxonomyCount]:
|
||||
"""取得 AI 自動化資產維度統計(只讀,不寫入)。"""
|
||||
service = get_knowledge_service()
|
||||
return await service.get_asset_taxonomy()
|
||||
|
||||
|
||||
@router.get("/{entry_id}", response_model=KnowledgeEntry)
|
||||
async def get_entry(entry_id: str) -> KnowledgeEntry:
|
||||
"""取得單筆知識條目 (view_count +1)"""
|
||||
|
||||
@@ -122,11 +122,18 @@ class CategoryCount(BaseModel):
|
||||
count: int
|
||||
|
||||
|
||||
class KnowledgeAssetTaxonomyCount(BaseModel):
|
||||
"""AI 自動化資產維度統計"""
|
||||
key: str
|
||||
count: int
|
||||
|
||||
|
||||
class KnowledgeListResponse(BaseModel):
|
||||
"""列表回應"""
|
||||
items: list[KnowledgeEntry]
|
||||
total: int
|
||||
categories: list[CategoryCount] = Field(default_factory=list)
|
||||
asset_taxonomy: list[KnowledgeAssetTaxonomyCount] = Field(default_factory=list)
|
||||
readback_status: str = "ready"
|
||||
operator_stage: str | None = None
|
||||
next_step: str | None = None
|
||||
|
||||
@@ -265,6 +265,10 @@ class IKnowledgeRepository(Protocol):
|
||||
"""取得分類統計 [(category, count)]"""
|
||||
...
|
||||
|
||||
async def get_asset_taxonomy_counts(self) -> list[tuple[str, int]]:
|
||||
"""取得 AI 自動化資產維度統計 [(taxonomy_key, count)]"""
|
||||
...
|
||||
|
||||
async def search(self, query: str, limit: int = 20) -> list[KnowledgeEntry]:
|
||||
"""關鍵字搜尋 (title + content + tags)"""
|
||||
...
|
||||
|
||||
@@ -29,6 +29,113 @@ logger = structlog.get_logger(__name__)
|
||||
|
||||
_DEFAULT_CATEGORY = "general"
|
||||
|
||||
_ASSET_TAXONOMY_KEYS = (
|
||||
"project",
|
||||
"product",
|
||||
"website",
|
||||
"service",
|
||||
"package",
|
||||
"tool",
|
||||
"log",
|
||||
"alert",
|
||||
"playbook",
|
||||
"rag",
|
||||
"mcp",
|
||||
"schedule",
|
||||
)
|
||||
|
||||
_ASSET_TAXONOMY_TERMS: dict[str, tuple[str, ...]] = {
|
||||
"project": (
|
||||
"project",
|
||||
"repo",
|
||||
"repository",
|
||||
"gitea",
|
||||
"branch",
|
||||
"workflow",
|
||||
"source_control",
|
||||
),
|
||||
"product": (
|
||||
"product",
|
||||
"awoooi",
|
||||
"awooop",
|
||||
"iwooos",
|
||||
"stockplatform",
|
||||
"momo",
|
||||
"awooogo",
|
||||
"agent-bounty",
|
||||
"agent_bounty",
|
||||
"tsenyang",
|
||||
"vibework",
|
||||
"2026fifa",
|
||||
),
|
||||
"website": ("website", "site", "nginx", "ssl", "domain", "route", "frontend"),
|
||||
"service": ("service", "daemon", "api", "worker", "runtime", "container", "pod"),
|
||||
"package": (
|
||||
"package",
|
||||
"dependency",
|
||||
"npm",
|
||||
"pnpm",
|
||||
"node",
|
||||
"python",
|
||||
"pip",
|
||||
"prisma",
|
||||
"next",
|
||||
"library",
|
||||
),
|
||||
"tool": (
|
||||
"tool",
|
||||
"ansible",
|
||||
"mcp",
|
||||
"playbook",
|
||||
"telegram",
|
||||
"wazuh",
|
||||
"kali",
|
||||
"sentry",
|
||||
"signoz",
|
||||
"runner",
|
||||
),
|
||||
"log": (
|
||||
"log",
|
||||
"logs",
|
||||
"event",
|
||||
"timeline",
|
||||
"trace",
|
||||
"audit",
|
||||
"callback",
|
||||
"conversation_event",
|
||||
"telemetry",
|
||||
),
|
||||
"alert": (
|
||||
"alert",
|
||||
"telegram",
|
||||
"sentry",
|
||||
"signoz",
|
||||
"notification",
|
||||
"warning",
|
||||
"critical",
|
||||
),
|
||||
"playbook": ("playbook", "runbook", "sop"),
|
||||
"rag": ("rag", "vector", "embedding", "semantic", "retrieval"),
|
||||
"mcp": ("mcp", "connector", "gateway", "tool integration", "tool-integration"),
|
||||
"schedule": ("schedule", "cron", "job", "worker", "patrol", "recurrence", "cadence"),
|
||||
}
|
||||
|
||||
_ASSET_TAXONOMY_CATEGORY_HINTS: dict[str, tuple[str, ...]] = {
|
||||
"website": ("external_site",),
|
||||
"service": (
|
||||
"infrastructure",
|
||||
"application",
|
||||
"ai_system",
|
||||
"database",
|
||||
"host_resource",
|
||||
"kubernetes",
|
||||
"alert_handling",
|
||||
),
|
||||
"tool": ("devops_tool", "AI自動化/Ansible受控修復"),
|
||||
"alert": ("alert_handling",),
|
||||
"playbook": ("auto_repair",),
|
||||
}
|
||||
|
||||
|
||||
def _enum_text(column):
|
||||
"""Return a lowercase text view for enum/varchar columns across old KM schemas."""
|
||||
@@ -85,6 +192,32 @@ def _knowledge_entry_columns():
|
||||
)
|
||||
|
||||
|
||||
def _asset_taxonomy_condition(key: str):
|
||||
"""Build a broad-but-readable taxonomy matcher for AI automation surfaces."""
|
||||
category_expr = _normalized_category_expr()
|
||||
category_text = func.lower(category_expr.cast(String))
|
||||
text_columns = (
|
||||
KnowledgeEntryRecord.title,
|
||||
KnowledgeEntryRecord.content,
|
||||
KnowledgeEntryRecord.tags.cast(String),
|
||||
category_expr.cast(String),
|
||||
)
|
||||
conditions = []
|
||||
category_hints = tuple(
|
||||
hint.lower() for hint in _ASSET_TAXONOMY_CATEGORY_HINTS.get(key, ())
|
||||
)
|
||||
if category_hints:
|
||||
conditions.append(category_text.in_(category_hints))
|
||||
for term in _ASSET_TAXONOMY_TERMS.get(key, ()):
|
||||
pattern = f"%{term.lower()}%"
|
||||
conditions.extend(func.lower(column).like(pattern) for column in text_columns)
|
||||
if key == "playbook":
|
||||
conditions.append(KnowledgeEntryRecord.related_playbook_id.is_not(None))
|
||||
if not conditions:
|
||||
return category_text == key
|
||||
return or_(*conditions)
|
||||
|
||||
|
||||
class KnowledgeDBRepository:
|
||||
"""
|
||||
Knowledge Repository - PostgreSQL 實作
|
||||
@@ -290,6 +423,26 @@ class KnowledgeDBRepository:
|
||||
)
|
||||
return [(_normalize_category(row.category), row.cnt) for row in result.all()]
|
||||
|
||||
async def get_asset_taxonomy_counts(self) -> list[tuple[str, int]]:
|
||||
"""取得 AI 自動化資產維度統計。
|
||||
|
||||
這不是替代原始 category,而是讓 UI / Agent 可以用穩定 taxonomy
|
||||
將 KM 依專案、產品、網站、服務、套件、工具、Log、Alert、PlayBook、
|
||||
RAG、MCP 與排程分群。
|
||||
"""
|
||||
rows: list[tuple[str, int]] = []
|
||||
for key in _ASSET_TAXONOMY_KEYS:
|
||||
result = await self.db.execute(
|
||||
select(func.count())
|
||||
.select_from(KnowledgeEntryRecord)
|
||||
.where(
|
||||
_active_status_filter(),
|
||||
_asset_taxonomy_condition(key),
|
||||
)
|
||||
)
|
||||
rows.append((key, int(result.scalar() or 0)))
|
||||
return rows
|
||||
|
||||
async def search(self, query: str, limit: int = 20) -> list[KnowledgeEntry]:
|
||||
"""關鍵字搜尋 (title + content + tags)"""
|
||||
like_q = f"%{query}%"
|
||||
|
||||
@@ -21,6 +21,7 @@ from src.models.knowledge import (
|
||||
CategoryCount,
|
||||
EntryStatus,
|
||||
EntryType,
|
||||
KnowledgeAssetTaxonomyCount,
|
||||
KnowledgeEntry,
|
||||
KnowledgeEntryCreate,
|
||||
KnowledgeEntryUpdate,
|
||||
@@ -49,6 +50,8 @@ _DEGRADED_CATEGORY_FALLBACKS = (
|
||||
"general",
|
||||
)
|
||||
|
||||
_ASSET_TAXONOMY_FALLBACKS = _DEGRADED_CATEGORY_FALLBACKS[:-1]
|
||||
|
||||
# =============================================================================
|
||||
# Singleton
|
||||
# =============================================================================
|
||||
@@ -65,6 +68,10 @@ def build_knowledge_list_readback_degraded_response(reason: str) -> KnowledgeLis
|
||||
CategoryCount(category=category, count=0)
|
||||
for category in _DEGRADED_CATEGORY_FALLBACKS
|
||||
],
|
||||
asset_taxonomy=[
|
||||
KnowledgeAssetTaxonomyCount(key=key, count=0)
|
||||
for key in _ASSET_TAXONOMY_FALLBACKS
|
||||
],
|
||||
readback_status="degraded",
|
||||
operator_stage="knowledge_readback_degraded_ai_controlled_repair",
|
||||
next_step=(
|
||||
@@ -191,8 +198,16 @@ class KnowledgeService:
|
||||
categories = [
|
||||
CategoryCount(category=cat, count=cnt) for cat, cnt in categories_raw
|
||||
]
|
||||
asset_taxonomy_raw = await repo.get_asset_taxonomy_counts()
|
||||
asset_taxonomy = [
|
||||
KnowledgeAssetTaxonomyCount(key=key, count=count)
|
||||
for key, count in asset_taxonomy_raw
|
||||
]
|
||||
return KnowledgeListResponse(
|
||||
items=items, total=total, categories=categories
|
||||
items=items,
|
||||
total=total,
|
||||
categories=categories,
|
||||
asset_taxonomy=asset_taxonomy,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - production readback must fail soft
|
||||
logger.warning(
|
||||
@@ -207,6 +222,23 @@ class KnowledgeService:
|
||||
)
|
||||
return build_knowledge_list_readback_degraded_response(str(exc))
|
||||
|
||||
async def get_asset_taxonomy(self) -> list[KnowledgeAssetTaxonomyCount]:
|
||||
"""取得 AI 自動化資產維度統計。"""
|
||||
try:
|
||||
async with get_db_context() as db:
|
||||
repo: IKnowledgeRepository = KnowledgeDBRepository(db)
|
||||
rows = await repo.get_asset_taxonomy_counts()
|
||||
return [
|
||||
KnowledgeAssetTaxonomyCount(key=key, count=count)
|
||||
for key, count in rows
|
||||
]
|
||||
except Exception as exc: # noqa: BLE001 - taxonomy must not 500 the KM UI
|
||||
logger.warning("knowledge_asset_taxonomy_readback_degraded", error=str(exc))
|
||||
return [
|
||||
KnowledgeAssetTaxonomyCount(key=key, count=0)
|
||||
for key in _ASSET_TAXONOMY_FALLBACKS
|
||||
]
|
||||
|
||||
async def get_categories(self) -> list[CategoryCount]:
|
||||
"""取得分類統計(直接呼叫 repo,不走 list_entries)"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user