"""Knowledge router — ADRs, memories, knowledge catalog, HOWTOs, agents, skills via MCP proxy.""" from __future__ import annotations import json import os from pathlib import Path from typing import Optional from fastapi import APIRouter, HTTPException from fastapi.responses import PlainTextResponse from app.redis_client import get_redis, list_adrs, get_adr, list_memories import app.mcp_client as mcp router = APIRouter(prefix="/api/v1", tags=["knowledge"]) DATA_DIR = Path(os.environ.get("DATA_DIR", "/data")) KNOWLEDGE_CATALOG = DATA_DIR / "knowledge" / "catalog.json" # ── ADRs ────────────────────────────────────────────────────────────────────── @router.get("/adrs") def api_list_adrs(status: Optional[str] = None): r = get_redis() adrs = list_adrs(r, status_filter=status) return {"adrs": adrs, "total": len(adrs)} @router.get("/adrs/{adr_id}") def api_get_adr(adr_id: str): r = get_redis() adr = get_adr(r, adr_id) if not adr: raise HTTPException(status_code=404, detail="ADR not found") return adr # ── Memories ────────────────────────────────────────────────────────────────── @router.get("/memories") def api_list_memories(entity_type: Optional[str] = None, limit: int = 50): r = get_redis() memories = list_memories(r, entity_type=entity_type, limit=limit) return {"memories": memories, "total": len(memories)} # ── Knowledge catalog — MCP proxy (file-based in DevOpsMCP) ────────────────── @router.get("/knowledge") async def api_list_knowledge(category: Optional[str] = None, tag: Optional[str] = None): try: return await mcp.list_knowledge(category=category, tag=tag) except Exception as exc: # Fallback: try local bind-mount if not KNOWLEDGE_CATALOG.exists(): return {"entries": [], "total": 0, "error": str(exc)} try: catalog = json.loads(KNOWLEDGE_CATALOG.read_text(encoding="utf-8")) entries = list(catalog.values()) if category: entries = [e for e in entries if e.get("category", "").lower() == category.lower()] if tag: entries = [e for e in entries if tag.lower() in [t.lower() for t in e.get("tags", [])]] entries.sort(key=lambda e: e.get("learned_at", ""), reverse=True) return {"entries": entries, "total": len(entries)} except Exception as e2: raise HTTPException(status_code=500, detail=str(e2)) from e2 @router.get("/knowledge/{filename}") async def api_get_knowledge_file(filename: str): try: result = await mcp.get_knowledge(filename) content = result.get("content", result.get("raw", "")) return PlainTextResponse(str(content), media_type="text/markdown") except Exception as exc: # Fallback: local bind-mount safe_name = Path(filename).name file_path = DATA_DIR / "knowledge" / safe_name if not file_path.exists(): raise HTTPException(status_code=404, detail="File not found") from exc return PlainTextResponse(file_path.read_text(encoding="utf-8"), media_type="text/markdown") # ── HOWTOs — MCP proxy ──────────────────────────────────────────────────────── @router.get("/howtos") async def api_list_howtos(): try: return await mcp.list_howtos() except Exception as exc: raise HTTPException(status_code=502, detail=f"MCP proxy error: {exc}") from exc @router.get("/howtos/{filename}") async def api_get_howto(filename: str): try: result = await mcp.get_howto(filename) content = result.get("content", result.get("raw", "")) return PlainTextResponse(str(content), media_type="text/markdown") except Exception as exc: raise HTTPException(status_code=502, detail=f"MCP proxy error: {exc}") from exc # ── Agents — MCP proxy ──────────────────────────────────────────────────────── @router.get("/agents") async def api_list_agents(domain: Optional[str] = None): try: return await mcp.list_agents(domain=domain) except Exception as exc: raise HTTPException(status_code=502, detail=f"MCP proxy error: {exc}") from exc @router.get("/agents/{name}") async def api_get_agent(name: str): try: return await mcp.get_agent(name) except Exception as exc: raise HTTPException(status_code=502, detail=f"MCP proxy error: {exc}") from exc # ── Skills — MCP proxy ──────────────────────────────────────────────────────── @router.get("/skills") async def api_list_skills(domain: Optional[str] = None): try: return await mcp.list_skills(domain=domain) except Exception as exc: raise HTTPException(status_code=502, detail=f"MCP proxy error: {exc}") from exc @router.get("/skills/{name}") async def api_get_skill(name: str): try: return await mcp.get_skill(name) except Exception as exc: raise HTTPException(status_code=502, detail=f"MCP proxy error: {exc}") from exc # ── Generic file browser (bind-mount fallback) ──────────────────────────────── @router.get("/files") def api_list_files(subdir: str = ""): target = (DATA_DIR / subdir if subdir else DATA_DIR).resolve() if not str(target).startswith(str(DATA_DIR.resolve())): raise HTTPException(status_code=400, detail="Path traversal not allowed") if not target.exists(): return {"files": [], "dirs": [], "path": subdir, "available": False} files, dirs = [], [] for item in sorted(target.iterdir()): if item.is_file() and item.suffix in {".md", ".yml", ".yaml", ".txt", ".rst", ".json"}: files.append({"name": item.name, "size": item.stat().st_size, "path": str(item.relative_to(DATA_DIR))}) elif item.is_dir(): dirs.append(item.name) return {"files": files, "dirs": dirs, "path": subdir, "available": True}