from __future__ import annotations import argparse import json import os import time import urllib.error import urllib.request from datetime import datetime from pathlib import Path from typing import Any from uuid import uuid4 REPO_ROOT = Path(__file__).resolve().parents[1] DEFAULT_RUN_ROOT = REPO_ROOT / "artifacts" / "eval-readiness-runs" class ApiClient: def __init__(self, api_base: str, raw_dir: Path, timeout_s: int = 60) -> None: self.api_base = api_base.rstrip("/") self.raw_dir = raw_dir self.timeout_s = timeout_s self.raw_dir.mkdir(parents=True, exist_ok=True) def get(self, path: str, label: str) -> dict[str, Any]: return self._request("GET", path, None, label) def post(self, path: str, payload: dict[str, Any], label: str) -> dict[str, Any]: return self._request("POST", path, payload, label) def _request( self, method: str, path: str, payload: dict[str, Any] | None, label: str, ) -> dict[str, Any]: url = path if path.startswith(("http://", "https://")) else f"{self.api_base}{path}" body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8") request = urllib.request.Request(url, data=body, method=method) if label == "frontend_home": request.add_header("Accept", "text/html,*/*") else: request.add_header("Accept", "application/json") if payload is not None: request.add_header("Content-Type", "application/json") start = time.perf_counter() try: with urllib.request.urlopen(request, timeout=self.timeout_s) as response: text = response.read().decode("utf-8", errors="replace") status = response.status except urllib.error.HTTPError as exc: text = exc.read().decode("utf-8", errors="replace") status = exc.code except Exception as exc: text = json.dumps({"error": str(exc)}, ensure_ascii=False) status = 0 elapsed_ms = round((time.perf_counter() - start) * 1000, 2) parsed = _parse_json(text) envelope = { "label": label, "method": method, "url": url, "status_code": status, "elapsed_ms": elapsed_ms, "request": _preview(payload), "body": parsed, } (self.raw_dir / f"{_safe_name(label)}.json").write_text( json.dumps(envelope, indent=2, ensure_ascii=False), encoding="utf-8", ) return envelope def main() -> int: parser = argparse.ArgumentParser(description="Run an isolated SkillOS demo readiness check.") parser.add_argument("--api-base", default="http://127.0.0.1:8001/api/v1") parser.add_argument("--frontend-base", default="http://127.0.0.1:5174") parser.add_argument("--run-root", type=Path, default=DEFAULT_RUN_ROOT) parser.add_argument("--run-id", default=datetime.now().strftime("%Y%m%d-%H%M%S")) parser.add_argument("--timeout", type=int, default=90) parser.add_argument("--skip-mutating-probe", action="store_true") args = parser.parse_args() run_dir = args.run_root / f"readiness-{args.run_id}" raw_dir = run_dir / "raw" run_dir.mkdir(parents=True, exist_ok=True) client = ApiClient(args.api_base, raw_dir, timeout_s=args.timeout) summary: dict[str, Any] = { "started_at": datetime.now().isoformat(timespec="seconds"), "api_base": args.api_base, "frontend_base": args.frontend_base, "run_dir": str(run_dir), "isolation": { "raw_fixtures_modified": False, "writes_only_under_run_dir": str(run_dir), "mutating_probe": not args.skip_mutating_probe, "governance_repo": os.environ.get("SKILLOS_GOVERNANCE_REPO", ""), }, "checks": [], } api_root = _api_root(args.api_base) frontend_base = args.frontend_base.rstrip("/") _record(summary, "backend_health", client.get(f"{api_root}/health", "backend_health"), {200}) _record(summary, "frontend_home", client.get(f"{frontend_base}/", "frontend_home"), {200}) _record( summary, "frontend_proxy", client.get(f"{frontend_base}/api/v1/skills?limit=1", "frontend_proxy"), {200}, ) _record(summary, "skills_list", client.get("/skills?limit=1", "skills_list"), {200}) _record(summary, "graph_view", client.get("/graph/view?view=skill_only&limit=10", "graph_view"), {200}) _record(summary, "evaluation_dashboard", client.get("/evaluation/dashboard", "evaluation_dashboard"), {200}) _record(summary, "version_repo_status", client.get("/lifecycle/repository/status", "version_repo_status"), {200, 400}) _record(summary, "llm_config", _llm_config_envelope(), {200, 204}) if not args.skip_mutating_probe: parse = client.post("/ingest/parse", _probe_parse_payload(), "ingest_parse_probe") _record(summary, "ingest_parse", parse, {200}) unit = _first_unit(parse) if unit: create = client.post("/ingest/create-candidate", _candidate_payload(unit), "create_candidate_probe") _record(summary, "create_candidate", create, {200, 201}) skill_id = _body(create).get("created_skill", {}).get("skill_id") if isinstance(skill_id, str) and skill_id: _record(summary, "version_diff", client.get(f"/lifecycle/{skill_id}/diff", "version_diff_probe"), {200}) _record(summary, "version_snapshot", client.post( f"/lifecycle/{skill_id}/snapshot", {"author": "demo_readiness_check", "message": "readiness probe snapshot"}, "version_snapshot_probe", ), {200, 400}) draft = client.post( f"/lifecycle/{skill_id}/new-version", _harness_draft_payload(), "new_harness_draft_probe", ) _record( summary, "new_harness_draft", draft, {200}, semantic_ok=_body(draft).get("state") == "S2", ) harness_skill_id = _body(draft).get("skill_id") or skill_id harness = client.post( f"/harness/{harness_skill_id}/verify-loop", { "harness": "local_skillos", "max_attempts": 2, "promote_on_pass": True, "allow_repair": True, "test_cases": [ { "test_id": "readiness-email-positive", "name": "Readiness email extraction positive case", "goal": "Extract an email address from text.", "input_data": {"text": "Contact readiness@example.com"}, "verifier_specs": [{"type": "json_exists", "path": "output.email"}], "timeout_s": 20, } ], }, "harness_verify_loop_probe", ) _record( summary, "harness_verify_loop", harness, {200}, semantic_ok=_body(harness).get("status") == "verified" and _body(harness).get("promotion_allowed") is True, ) _record(summary, "execution_plan", client.post( "/execution/plan", {"goal": "Extract an email address from a JSON payload.", "context": {"text": "user@example.com"}}, "execution_plan_probe", ), {200}) summary["finished_at"] = datetime.now().isoformat(timespec="seconds") summary["scores"] = _score(summary) (run_dir / "summary.json").write_text(json.dumps(summary, indent=2, ensure_ascii=False), encoding="utf-8") (run_dir / "REPORT.md").write_text(_report(summary), encoding="utf-8") print(json.dumps({"run_dir": str(run_dir), "scores": summary["scores"]}, indent=2, ensure_ascii=False)) return 0 if summary["scores"]["overall"] >= 0.85 else 1 def _record( summary: dict[str, Any], name: str, envelope: dict[str, Any], ok_statuses: set[int], *, semantic_ok: bool | None = None, ) -> None: status = int(envelope.get("status_code") or 0) body = _body(envelope) passed = status in ok_statuses and (True if semantic_ok is None else semantic_ok) summary["checks"].append({ "name": name, "passed": passed, "status_code": status, "elapsed_ms": envelope.get("elapsed_ms"), "semantic_ok": semantic_ok, "error": body.get("error") or body.get("detail") if isinstance(body, dict) else None, }) def _score(summary: dict[str, Any]) -> dict[str, Any]: checks = summary.get("checks", []) total = len(checks) passed = sum(1 for check in checks if check.get("passed")) return { "passed": passed, "total": total, "overall": round(passed / total, 4) if total else 0.0, } def _api_root(api_base: str) -> str: stripped = api_base.rstrip("/") suffix = "/api/v1" if stripped.endswith(suffix): return stripped[: -len(suffix)] return stripped def _llm_config_envelope() -> dict[str, Any]: configured = bool(os.environ.get("LLM_API_URL") and os.environ.get("LLM_MODEL")) has_key = bool(os.environ.get("LLM_API_KEY")) return { "label": "llm_config", "method": "ENV", "url": "environment", "status_code": 200 if configured and has_key else 204, "elapsed_ms": 0.0, "request": None, "body": { "configured": configured, "api_url_configured": bool(os.environ.get("LLM_API_URL")), "api_key_configured": has_key, "model": os.environ.get("LLM_MODEL", ""), "note": "Readiness does not print or persist API keys.", }, } def _report(summary: dict[str, Any]) -> str: lines = [ "# SkillOS Demo Readiness Report", "", f"- API base: `{summary['api_base']}`", f"- Frontend base: `{summary['frontend_base']}`", f"- Run directory: `{summary['run_dir']}`", f"- Overall: `{summary['scores']['overall']}` ({summary['scores']['passed']}/{summary['scores']['total']})", "", "## Isolation", "", "- Raw fixtures are not modified.", f"- Request/response artifacts are written only under `{summary['run_dir']}`.", "- Mutating probes create only readiness-tagged candidate data in the target backend.", "", "## Checks", "", "| Check | Passed | HTTP | Latency ms | Error |", "| --- | --- | --- | --- | --- |", ] for check in summary.get("checks", []): lines.append( f"| {check['name']} | {check['passed']} | {check['status_code']} | " f"{check.get('elapsed_ms', '')} | {check.get('error') or ''} |" ) lines.append("") return "\n".join(lines) def _probe_parse_payload() -> dict[str, Any]: return { "source_type": "past_skills", "content": json.dumps({ "name": "readiness_email_extract_probe", "description": "Extract one email address from input text for readiness checks.", "inputs": {"text": "string"}, "outputs": {"email": "string"}, "steps": ["Read input text", "Find the first email address", "Return it as output.email"], }, ensure_ascii=False), "metadata": { "source_id": "readiness-email-extract-probe", "domain": "software", "max_candidates": 1, }, } def _candidate_payload(unit: dict[str, Any]) -> dict[str, Any]: metadata = unit.get("metadata") if isinstance(unit.get("metadata"), dict) else {} interface = metadata.get("candidate_interface") if isinstance(metadata.get("candidate_interface"), dict) else {} implementation = metadata.get("candidate_implementation") if isinstance(metadata.get("candidate_implementation"), dict) else {} unique_name = _safe_name( f"{unit.get('proposed_skill_name') or 'readiness_email_extract_probe'}_{uuid4().hex[:8]}" ) return { "source_type": "past_skills", "unit_id": unit.get("unit_id"), "raw_content": unit.get("raw_content", ""), "name": unique_name, "description": unit.get("proposed_description") or unit.get("summary") or "Readiness probe skill.", "skill_type": "atomic", "tags": ["readiness-probe", "isolated-eval"], "input_schema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}, "output_schema": {"type": "object", "properties": {"email": {"type": "string"}}}, "preconditions": ["input.text is provided"], "postconditions": ["output.email is present when an email exists"], "prompt_template": "Extract the first email from {text}.", "evaluation": { "verifier_specs": [{"type": "json_exists", "path": "output.email"}], "benchmark_task_ids": ["readiness-email-extract-probe"], }, "provenance": { "source_type": "past_skills", "source_ids": ["readiness-email-extract-probe"], "parent_skill_ids": [], "created_by_agent": "demo_readiness_check", "creation_context": {"isolated_probe": True}, }, "author": "demo_readiness_check", } def _harness_draft_payload() -> dict[str, Any]: return { "bump": "patch", "description": "Readiness harness Draft that extracts the first email address from input text.", "implementation": { "language": "python", "code": ( "text = input_data.get('text', '')\n" "for token in str(text).replace('<', ' ').replace('>', ' ').split():\n" " candidate = token.strip('.,;:()[]{}\\\"\\'')\n" " if '@' in candidate and '.' in candidate.split('@')[-1]:\n" " output['email'] = candidate\n" " break\n" ), }, "evaluation": { "verifier_specs": [{"type": "json_exists", "path": "output.email"}], "benchmark_task_ids": ["readiness-email-extract-probe"], "validation_summary": "Readiness harness Draft uses deterministic local postcondition verification.", }, "test_cases": [ { "name": "Readiness email extraction positive case", "input_data": {"text": "Contact readiness@example.com"}, "expected_output": {"email": "readiness@example.com"}, } ], "metadata": { "readiness_probe": True, "requires_harness_reverification": True, }, "author": "demo_readiness_check", } def _first_unit(envelope: dict[str, Any]) -> dict[str, Any] | None: body = _body(envelope) units = body.get("units") if isinstance(body, dict) else None if isinstance(units, list) and units and isinstance(units[0], dict): return units[0] return None def _body(envelope: dict[str, Any]) -> dict[str, Any]: body = envelope.get("body") return body if isinstance(body, dict) else {} def _parse_json(text: str) -> Any: try: return json.loads(text) except Exception: return text def _preview(payload: dict[str, Any] | None) -> Any: if payload is None: return None text = json.dumps(payload, ensure_ascii=False) if len(text) <= 1000: return payload return {"preview": text[:1000], "truncated": True} def _safe_name(value: str) -> str: return "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in value)[:120] if __name__ == "__main__": raise SystemExit(main())