Ctrl K
ComunidadGuías…

scripts/xkb_doctor.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Verify MCP initialization, tool discovery and a real knowledge recall."""
from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "tools"))
from runtime_config import runtime_env
from xkb_recall import validate_packet, recall_quality

SERVER = Path(__file__).resolve().parent / "xkb_recall_server.py"


def probe(query: str, *, env: dict | None = None, limit: int = 10, conversation=None) -> dict:
    """Use the real stdio server, not an import of its implementation."""
    requests = [
        {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
            "protocolVersion": "2024-11-05", "capabilities": {},
            "clientInfo": {"name": "xkb-doctor", "version": "1"}}},
        {"jsonrpc": "2.0", "method": "notifications/initialized"},
        {"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
        {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {
            "name": "xkb_recall", "arguments": {"message": query, "limit": limit,
                **({"conversation": conversation} if conversation is not None else {})}}},
    ]
    child_env = dict(runtime_env() if env is None else env)
    child_env.update({"PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8"})
    result = subprocess.run([sys.executable, "-u", str(SERVER)],
                            input="".join(json.dumps(r) + "\n" for r in requests),
                            text=True, encoding="utf-8", capture_output=True,
                            env=child_env, timeout=55)
    if result.returncode:
        raise RuntimeError(f"MCP process exited {result.returncode}")
    replies = {r["id"]: r for r in map(json.loads, result.stdout.splitlines()) if "id" in r}
    if set(replies) != {1, 2, 3} or any("error" in r for r in replies.values()):
        raise RuntimeError("MCP initialize/list/call did not all succeed")
    if replies[1]["result"].get("serverInfo", {}).get("name") != "xkb-recall":
        raise RuntimeError("unexpected MCP server identity")
    if "xkb_recall" not in [t["name"] for t in replies[2]["result"].get("tools", [])]:
        raise RuntimeError("MCP did not advertise xkb_recall")
    call = replies[3]["result"]
    for item in call.get("content", []):
        text = item.get("text", "")
        if text.startswith("[xkb_recall_meta] "):
            packet = json.loads(text[len("[xkb_recall_meta] "):])
            if call.get("isError") or packet.get("status") != "ok":
                raise RuntimeError(packet.get("error") or "MCP recall failed")
            return validate_packet(packet)
    raise RuntimeError("MCP recall omitted its structured metadata")


def assess(packet: dict, *, expect_ids: list[str] = (), require_semantic: bool = False,
           require_judge: bool = False) -> dict:
    ids = [str(r.get("id") or r.get("trace_id") or "") for r in packet["records"]]
    problems = []
    missing = sorted(set(expect_ids) - set(ids))
    if missing:
        problems.append("expected evidence missing: " + ", ".join(missing))
    semantic_used = packet.get("semantic_backend", {}).get("used", packet["retrieval_mode"] in {"xbrain_hybrid", "wiki_semantic"})
    if require_semantic and not semantic_used:
        problems.append("semantic retrieval was not used")
    judge = packet.get("judge", {"status": "not_attempted"})
    quality = recall_quality(packet)
    if require_judge and not quality["judge_complete"]:
        problems.append("relevance judge incomplete: " + judge.get("status", "not_attempted"))
    warnings = list(packet.get("warnings", []))
    degraded = quality["status"] == "degraded"
    if quality["warning"] and quality["warning"] not in warnings:
        warnings.append(quality["warning"])
    if packet["count"] == 0 and packet["retrieval_mode"] != "skipped":
        warnings.append("connection works, but this query returned no evidence; use --expect-id to verify a known item")
    return {"ok": not problems, "status": "failed" if problems else "degraded" if degraded else "ready",
            "checks": {"initialize": True, "tools_list": True,
            "tools_call": True}, "connection": packet.get("connection"),
            "retrieval_mode": packet["retrieval_mode"], "judge": judge, "backends": packet.get("backends", {}),
            "count": packet["count"], "record_ids": ids, "warnings": warnings, "quality": quality,
            "problems": problems,
            "scope": "fresh MCP subprocess; does not prove an already-open agent refreshed its tools"}


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--query", required=True, help="A conversational utterance with a known knowledge need")
    parser.add_argument("--expect-id", action="append", default=[])
    parser.add_argument("--require-semantic", action="store_true")
    parser.add_argument("--require-judge", action="store_true")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    started = time.monotonic()
    try:
        report = assess(probe(args.query), expect_ids=args.expect_id,
                        require_semantic=args.require_semantic, require_judge=args.require_judge)
    except Exception as exc:
        report = {"ok": False, "status": "failed", "problems": [str(exc)]}
    report["elapsed_ms"] = round((time.monotonic() - started) * 1000)
    # ASCII escapes also work on Windows terminals without UTF-8 configured.
    print(json.dumps(report, ensure_ascii=True, indent=None if args.json else 2))
    return 0 if report["ok"] else 1


if __name__ == "__main__":
    import xkb_usage
    xkb_usage.record(__file__)
    raise SystemExit(main())