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

This commit is contained in:
Your Name
2026-07-03 16:12:48 +08:00
parent 37dda358a3
commit f99305e481
10 changed files with 312 additions and 17 deletions

View File

@@ -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)"""

View File

@@ -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

View File

@@ -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)"""
...

View File

@@ -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}%"

View File

@@ -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: