"""Read-only KB proposal review routes for Argus. This surface is the operator-visible bridge between Leo's Telegram KB reasoning and the narrow apply worker. It intentionally performs no approve/reject/apply mutation; it only renders the same packets produced by ``scripts/kb_proposal_review_packet.py``. """ from __future__ import annotations import argparse import logging import os import sys from collections import Counter from datetime import datetime, timezone from html import escape from pathlib import Path from typing import Any from urllib.parse import urlencode from aiohttp import web from shared_ui import render_page ROOT = Path(__file__).resolve().parent.parent SCRIPT_DIR_CANDIDATES = [ ROOT / "scripts", Path(os.environ.get("TELEO_INFRA_REPO_DIR", "/opt/teleo-eval/workspaces/deploy-infra")) / "scripts", ] for scripts_dir in SCRIPT_DIR_CANDIDATES: if str(scripts_dir) not in sys.path: sys.path.insert(0, str(scripts_dir)) import kb_proposal_review_packet as proposal_review # noqa: E402 logger = logging.getLogger("argus.kb_proposals") KB_PROPOSAL_PUBLIC_PATHS = frozenset({"/kb-proposals", "/api/kb-proposals"}) KB_PROPOSAL_LOADER_KEY = web.AppKey("kb_proposal_loader", object) DEFAULT_LIMIT = 20 MAX_LIMIT = 100 def _query_limit(value: str | None) -> int: if not value: return DEFAULT_LIMIT try: limit = int(value) except ValueError: return DEFAULT_LIMIT return max(1, min(limit, MAX_LIMIT)) def _query_filters(request: web.Request) -> dict[str, Any]: status = request.query.get("status", "approved").strip() or "approved" proposal_id = (request.query.get("proposal_id") or "").strip() if proposal_id: status = "" return { "status": status, "proposal_id": proposal_id, "limit": _query_limit(request.query.get("limit")), } def _db_args(filters: dict[str, Any]) -> argparse.Namespace: return argparse.Namespace( proposal_id=filters["proposal_id"] or None, status=filters["status"] or None, limit=filters["limit"], secrets_file=os.environ.get( "KB_PROPOSAL_REVIEW_SECRETS_FILE", os.environ.get("KB_APPLY_SECRETS_FILE", proposal_review.ap.DEFAULT_SECRETS_FILE), ), container=os.environ.get("KB_PROPOSAL_REVIEW_CONTAINER", proposal_review.ap.DEFAULT_CONTAINER), db=os.environ.get("KB_PROPOSAL_REVIEW_DB", proposal_review.ap.DEFAULT_DB), host=os.environ.get("KB_PROPOSAL_REVIEW_HOST", proposal_review.ap.DEFAULT_HOST), role=os.environ.get("KB_PROPOSAL_REVIEW_ROLE", proposal_review.ap.DEFAULT_ROLE), ) def _load_packets(request: web.Request, filters: dict[str, Any]) -> list[dict[str, Any]]: loader = request.app.get(KB_PROPOSAL_LOADER_KEY) proposals = loader(_db_args(filters)) if loader else proposal_review.load_from_db(_db_args(filters)) return [proposal_review.classify_proposal(proposal) for proposal in proposals] def _counts(packets: list[dict[str, Any]], key: str) -> dict[str, int]: return dict(Counter(str(packet.get(key) or "unknown") for packet in packets)) def build_kb_proposal_response(packets: list[dict[str, Any]], filters: dict[str, Any]) -> dict[str, Any]: return { "generated_at": datetime.now(timezone.utc).isoformat(), "filters": filters, "total": len(packets), "status_counts": _counts(packets, "status"), "review_state_counts": _counts(packets, "review_state"), "worker_applyable_count": sum(1 for packet in packets if packet.get("worker_applyable")), "packets": packets, "read_only": True, } def _badge_class(value: str) -> str: if value in {"applied", "approved_applyable"}: return "badge-green" if value in {"approved", "approved_needs_apply_payload", "needs_human_review", "pending_review"}: return "badge-yellow" if value in {"unsupported_by_apply_worker", "not_ready"}: return "badge-red" return "badge-blue" def _badge(value: Any) -> str: text = escape(str(value or "unknown")) return f'{text}' def _join_values(values: list[Any]) -> str: if not values: return 'none' return ", ".join(escape(str(value)) for value in values) def _status_link(label: str, status: str, filters: dict[str, Any]) -> str: query = urlencode({"status": status, "limit": filters["limit"]}) return f'{escape(label)}' def _packet_card(packet: dict[str, Any]) -> str: auth = packet.get("identity_and_authorization") or {} summary = packet.get("payload_summary") or {} applyability = packet.get("applyability") or {} proposal_id = escape(str(packet.get("proposal_id") or "unknown")) proposal_kind = escape(str(summary.get("proposal_kind") or "unknown")) next_action = escape(str(packet.get("next_admin_action") or "No next action recorded.")) source_ref = escape(str(auth.get("source_ref") or "unknown")) channel = escape(str(auth.get("channel") or "unknown")) reviewed_by = escape(str(auth.get("reviewed_by_handle") or "unreviewed")) rows = [ ("Status", _badge(packet.get("status"))), ("Review state", _badge(packet.get("review_state"))), ("Worker applyable", "yes" if packet.get("worker_applyable") else "no"), ("Has strict apply payload", "yes" if packet.get("has_apply_payload") else "no"), ("Proposal kind", proposal_kind), ("Reviewed by", reviewed_by), ("Channel", channel), ("Source ref", source_ref), ("Old claim id", escape(str(summary.get("old_claim_id") or "unknown"))), ("Claim candidates", str(summary.get("claim_candidate_count", 0))), ("Source candidates", str(summary.get("source_candidate_count", 0))), ("Supersession edges", str(summary.get("supersession_edge_count", 0))), ("Missing contract", _join_values(applyability.get("missing_contract") or [])), ] details = "\n".join( f"