"""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_normalize as proposal_normalize # noqa: E402 import kb_proposal_review_packet as proposal_review # noqa: E402 from kb_claim_routes import claim_link_html # 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)) packets = [] for proposal in proposals: packet = proposal_review.classify_proposal(proposal) packet["normalization_preview"] = proposal_normalize.normalize_proposal(proposal) packet["apply_preview"] = build_apply_preview(proposal, packet) packets.append(packet) return packets 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 _code(value: Any) -> str: return f"{escape(str(value))}" def _payload_dict(value: Any) -> dict[str, Any]: return value if isinstance(value, dict) else {} def _payload_list(value: Any) -> list[Any]: return value if isinstance(value, list) else [] def _apply_preview_row( *, action: str, table: str, target: str, claim_ids: list[str] | None = None, details: dict[str, Any] | None = None, source: str = "proposal", ) -> dict[str, Any]: return { "action": action, "table": table, "target": target, "claim_ids": claim_ids or [], "details": details or {}, "source": source, } def _strict_apply_preview_rows( proposal_type: str | None, apply_payload: dict[str, Any], *, source: str = "proposal.apply_payload", ) -> list[dict[str, Any]]: if proposal_type == "add_edge": from_claim = str(apply_payload.get("from_claim") or "") to_claim = str(apply_payload.get("to_claim") or "") edge_type = str(apply_payload.get("edge_type") or "") return [ _apply_preview_row( action="insert_if_missing", table="public.claim_edges", target=f"{from_claim} -> {to_claim} ({edge_type or 'edge_type missing'})", claim_ids=[claim_id for claim_id in [from_claim, to_claim] if claim_id], details={ "from_claim": from_claim, "to_claim": to_claim, "edge_type": edge_type, "weight": apply_payload.get("weight"), }, source=source, ) ] if proposal_type == "attach_evidence": rows = [] for evidence in _payload_list(apply_payload.get("evidence")): ev = _payload_dict(evidence) claim_id = str(ev.get("claim_id") or "") source_id = str(ev.get("source_id") or "") role = str(ev.get("role") or "grounds") rows.append( _apply_preview_row( action="insert_if_missing", table="public.claim_evidence", target=f"{claim_id} <= {source_id} ({role})", claim_ids=[claim_id] if claim_id else [], details={ "claim_id": claim_id, "source_id": source_id, "role": role, "weight": ev.get("weight"), }, source=source, ) ) return rows if proposal_type == "revise_strategy": agent_id = str(apply_payload.get("agent_id") or "") strategy_nodes = _payload_list(apply_payload.get("strategy_nodes")) return [ _apply_preview_row( action="update_then_insert", table="public.strategies", target=f"new active strategy for agent {agent_id or 'unknown'}", details={ "agent_id": agent_id, "strategy_keys": sorted(_payload_dict(apply_payload.get("strategy")).keys()), }, source=source, ), _apply_preview_row( action="retire_then_insert", table="public.strategy_nodes", target=f"{len(strategy_nodes)} replacement strategy node(s)", details={"agent_id": agent_id, "strategy_node_count": len(strategy_nodes)}, source=source, ), ] return [ _apply_preview_row( action="blocked", table="none", target=f"unsupported proposal_type {proposal_type or 'unknown'}", details={"proposal_type": proposal_type}, source=source, ) ] def build_apply_preview(proposal: dict[str, Any], packet: dict[str, Any]) -> dict[str, Any]: """Build a read-only, row-level preview of what an apply step would touch.""" payload = _payload_dict(proposal.get("payload")) apply_payload = _payload_dict(payload.get("apply_payload")) proposal_type = proposal.get("proposal_type") normalization = _payload_dict(packet.get("normalization_preview")) if proposal.get("status") == "applied": return { "state": "applied", "rows": [], "blocked_fragments": [], "note": "Proposal is already applied; use the canonical claim/edge/evidence readback.", } if apply_payload and proposal_type in proposal_review.ap.APPLYABLE_TYPES: return { "state": "strict_apply_payload_ready", "rows": _strict_apply_preview_rows(proposal_type, apply_payload), "blocked_fragments": [], "note": "Preview only; the page does not execute canonical writes.", } child_rows = [] for index, child in enumerate(_payload_list(normalization.get("strict_child_proposals"))): child_payload = _payload_dict(_payload_dict(child).get("payload")) child_apply_payload = _payload_dict(child_payload.get("apply_payload")) child_type = str(_payload_dict(child).get("proposal_type") or "") child_rows.extend( _strict_apply_preview_rows( child_type, child_apply_payload, source=f"normalization.strict_child_proposals[{index}]", ) ) blocked = _payload_list(normalization.get("blocked_fragments")) if child_rows and not blocked: state = "strict_child_proposals_ready" note = "Preview of strict child proposals; reviewer still needs to stage those children before apply." elif child_rows: state = "partial_preview_blocked_fragments" note = "Some strict child rows can be previewed; blocked fragments still need canonical IDs or schema decisions." else: state = "not_applyable_yet" note = "No canonical write preview is safe until the proposal has a strict apply_payload or strict child proposals." return { "state": state, "rows": child_rows, "blocked_fragments": blocked, "note": note, } def _status_link(label: str, status: str, filters: dict[str, Any]) -> str: query = urlencode({"status": status, "limit": filters["limit"]}) return f'{escape(label)}' def _apply_preview_html(preview: dict[str, Any]) -> str: state = _badge(preview.get("state")) rows = _payload_list(preview.get("rows")) blocked = _payload_list(preview.get("blocked_fragments")) note = escape(str(preview.get("note") or "")) row_html = "" if rows: rendered_rows = [] for row in rows: details = _payload_dict(_payload_dict(row).get("details")) detail_text = ", ".join( f"{escape(str(key))}={escape(str(value))}" for key, value in details.items() if value not in (None, "") ) claim_links = " ".join( claim_link_html(claim_id, label=str(claim_id)[:8]) for claim_id in _payload_list(_payload_dict(row).get("claim_ids")) ) or 'none' detail_html = escape(detail_text) if detail_text else 'none' rendered_rows.append( "" f"{_code(_payload_dict(row).get('action') or 'unknown')}" f"{_code(_payload_dict(row).get('table') or 'unknown')}" f"{escape(str(_payload_dict(row).get('target') or ''))}" f"{claim_links}" f"{detail_html}" "" ) row_html = f""" {''.join(rendered_rows)}
ActionTableTargetClaimsDetails
""" else: row_html = '

No canonical row preview is executable yet.

' blocked_html = "" if blocked: blocked_rows = [] for fragment in blocked: frag = _payload_dict(fragment) missing = ", ".join(escape(str(value)) for value in _payload_list(frag.get("missing"))) or "unknown" blocked_rows.append( "" f"{_code(frag.get('kind') or 'fragment')}" f"{escape(str(frag.get('reason') or 'blocked'))}" f"{missing}" "" ) blocked_html = f"""
Blocked fragments
{''.join(blocked_rows)}
KindReasonMissing
""" return f"""
Apply preview

{state} {note}

{row_html} {blocked_html}
""" 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_raw = str(packet.get("proposal_id") or "unknown") proposal_id = _code(proposal_id_raw) 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", claim_link_html(summary.get("old_claim_id") or "unknown")), ("Claim candidates", _code(summary.get("claim_candidate_count", 0))), ("Source candidates", _code(summary.get("source_candidate_count", 0))), ("Supersession edges", _code(summary.get("supersession_edge_count", 0))), ("Missing contract", _join_values(applyability.get("missing_contract") or [])), ] normalization = packet.get("normalization_preview") or {} if normalization: rows.extend( [ ("Normalization", _badge(normalization.get("normalization_state"))), ("Strict child proposals", _code(normalization.get("strict_child_count", 0))), ("Blocked fragments", _code(normalization.get("blocked_count", 0))), ] ) details = "\n".join( f"{escape(label)}{value}" for label, value in rows ) normalization_html = "" if normalization: normalization_html = f"""
Normalization action

{escape(str(normalization.get("next_normalization_action") or "No normalization action recorded."))}

""" apply_preview_html = _apply_preview_html(packet.get("apply_preview") or {}) return f"""

{proposal_id}

{details}
Next admin action

{next_action}

{normalization_html} {apply_preview_html}
""" def render_kb_proposals_page(data: dict[str, Any]) -> str: filters = data["filters"] packets = data["packets"] generated_at = escape(str(data["generated_at"])) cards = "\n".join(_packet_card(packet) for packet in packets) if not cards: cards = '
No proposals matched this filter.
' filter_html = " ".join( [ _status_link("Approved", "approved", filters), _status_link("Pending review", "pending_review", filters), _status_link("Applied", "applied", filters), ] ) body = f"""
Read-only review surface. It shows proposal intent, applyability, missing contracts, and the next admin action; it does not approve, reject, or apply proposals.
Matching proposals
{data["total"]}
Worker applyable
{data["worker_applyable_count"]}
Current status filter
{escape(filters.get("status") or "by id")}
Filters
{filter_html}
Proposal Review Packets
{cards}
""" extra_css = """ .small-value { font-size: 16px !important; overflow-wrap: anywhere; } .filters { display: flex; flex-wrap: wrap; gap: 8px; } .filter-link { color: #58a6ff; background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 6px 10px; text-decoration: none; font-size: 13px; } .filter-link:hover { background: #21262d; } .claim-link { color: #58a6ff; text-decoration: none; } .claim-link:hover { text-decoration: underline; } .proposal-list { display: grid; gap: 16px; } .proposal-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; } .proposal-card-header { display: flex; justify-content: space-between; gap: 12px; align-items: start; } .proposal-card h2 { color: #c9d1d9; font-size: 15px; overflow-wrap: anywhere; } .proposal-detail { margin-top: 12px; } .proposal-detail th { width: 210px; vertical-align: top; } .proposal-detail td { overflow-wrap: anywhere; } .next-action { margin-top: 14px; padding: 12px; border: 1px solid #30363d; border-radius: 6px; background: #0d1117; } .next-action .label { color: #8b949e; font-size: 11px; text-transform: uppercase; margin-bottom: 6px; } .next-action p { color: #c9d1d9; line-height: 1.4; } .muted { color: #8b949e; } """ return render_page( "KB Proposals", "Review Leo KB proposal packets before any apply step", "/kb-proposals", body, extra_css=extra_css, timestamp=generated_at, ) async def handle_api_kb_proposals(request: web.Request) -> web.Response: filters = _query_filters(request) try: packets = _load_packets(request, filters) except BaseException as exc: logger.exception("KB proposal packet load failed") return web.json_response({"error": "kb_proposal_load_failed", "detail": str(exc)}, status=500) return web.json_response(build_kb_proposal_response(packets, filters)) async def handle_kb_proposals_page(request: web.Request) -> web.Response: filters = _query_filters(request) try: packets = _load_packets(request, filters) except BaseException as exc: logger.exception("KB proposal packet page failed") return web.Response( text=render_page( "KB Proposals", "Review Leo KB proposal packets before any apply step", "/kb-proposals", f'
Failed to load KB proposals: {escape(str(exc))}
', ), content_type="text/html", status=500, ) data = build_kb_proposal_response(packets, filters) return web.Response(text=render_kb_proposals_page(data), content_type="text/html") def register_kb_proposal_routes(app: web.Application) -> None: app.router.add_get("/api/kb-proposals", handle_api_kb_proposals) app.router.add_get("/kb-proposals", handle_kb_proposals_page)