from typing import Any, Dict, List from fastapi import FastAPI, HTTPException from pydantic import BaseModel LEVELS = ["public", "interclan", "incircle", "soulsafe", "sacred"] LEVEL_RANK = {k: i for i, k in enumerate(LEVELS)} SENSITIVE_KEYWORDS = [ "child", "children", "minor", "health", "trauma", "violence", "abuse", "ребен", "дитин", "здоров", "травм", "насил", ] app = FastAPI(title="CLAN Visibility Guard", version="1.0.0") class VisibilityCheck(BaseModel): current_level: str requested_level: str class ClassifyRequest(BaseModel): text: str class RedactRequest(BaseModel): text: str target_level: str def _norm(level: str) -> str: v = (level or "").strip().lower() if v not in LEVEL_RANK: raise HTTPException(status_code=400, detail=f"invalid_visibility_level:{level}") return v @app.get("/health") def health() -> Dict[str, Any]: return {"status": "ok", "service": "clan-visibility-guard"} @app.post("/visibility/check_downgrade") def check_downgrade(body: VisibilityCheck) -> Dict[str, Any]: cur = _norm(body.current_level) req = _norm(body.requested_level) allowed = LEVEL_RANK[req] >= LEVEL_RANK[cur] return { "allowed": allowed, "reason": "ok" if allowed else "downgrade_requires_consent", "current_level": cur, "requested_level": req, } @app.post("/visibility/classify") def classify(body: ClassifyRequest) -> Dict[str, Any]: t = (body.text or "").lower() flags: List[str] = [k for k in SENSITIVE_KEYWORDS if k in t] recommended = "soulsafe" if flags else "incircle" if any(x in t for x in ["sacred", "сакрал", "духовн"]): recommended = "sacred" return { "recommended_level": recommended, "sensitivity_flags": flags, } @app.post("/visibility/redact_for_level") def redact_for_level(body: RedactRequest) -> Dict[str, Any]: target = _norm(body.target_level) txt = body.text or "" redacted = txt if target in {"public", "interclan", "incircle"}: for token in ["ребен", "дитин", "здоров", "травм", "насил", "health", "trauma", "violence", "abuse"]: redacted = redacted.replace(token, "[sensitive]") return { "target_level": target, "redacted_text": redacted, "changed": redacted != txt, }