Spaces:
Running
Running
File size: 7,963 Bytes
9a955a5 a423344 9a955a5 a423344 9a955a5 a423344 9a955a5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | #!/usr/bin/env python3
"""Repeatability burn-in for the MCP server: fire the SAME brief N times.
Pulls the newest brief for a company from Supabase demo_history (status
mcp_intake — i.e. "Jack's last payload" for Tixr) and runs it through
build_demo_from_brief N times sequentially, polling each to completion.
Reports a per-run table + pass rate, exits non-zero if any run failed.
Every run is a fresh LLM blueprint draw, so N runs measure the real-world
flake rate (blueprint quirks, provider 529s, TS import flakes) — not just
whether one lucky draw works.
Usage (from either clone; MCP_ACCESS_TOKEN must be in the environment or in
the demoprep_mcp clone's .env, which is tried explicitly as documented):
./demoprep/bin/python tests/mcp_repeat_test.py # 5x Tixr
./demoprep/bin/python tests/mcp_repeat_test.py --count 3
./demoprep/bin/python tests/mcp_repeat_test.py --company Tixr --owner jack.rayner@thoughtspot.com
"""
import argparse
import json
import os
import sys
import time
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, ROOT)
from dotenv import load_dotenv
load_dotenv(os.path.join(ROOT, ".env"))
# The MCP access token historically lives in the demoprep_mcp clone's .env.
# Load it as a documented, explicit second source (does not override existing env).
_MCP_CLONE_ENV = "/Users/mike.boone/_source/demoprep_mcp/.env"
if not os.getenv("MCP_ACCESS_TOKEN") and os.path.exists(_MCP_CLONE_ENV):
load_dotenv(_MCP_CLONE_ENV)
import tests.e2e_mcp as H # noqa: E402
from supabase_client import SupabaseSettings # noqa: E402
def log(msg: str) -> None:
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
def fetch_latest_brief(company: str, attempts: int = 5) -> tuple[str, str, str]:
rows = None
for attempt in range(1, attempts + 1):
try:
rows = (
SupabaseSettings().client.table("demo_history")
.select("company_name,use_case,results,created_at")
.eq("status", "mcp_intake")
.ilike("company_name", f"%{company}%")
.order("created_at", desc=True)
.limit(1)
.execute()
.data
or []
)
break
except Exception as e:
# Venue WiFi / proxy flaps surface here as SSL or connect errors;
# retry rather than dying before the first build is even fired.
if attempt == attempts:
raise SystemExit(
f"Supabase unreachable after {attempts} attempts ({type(e).__name__}: "
f"{str(e)[:160]}). Check network/proxy, or pass --brief-file."
)
log(f"brief fetch failed ({type(e).__name__}), retry {attempt}/{attempts - 1} in 10s")
time.sleep(10)
if not rows:
raise SystemExit(f"No mcp_intake brief for {company!r} in demo_history")
row = rows[0]
brief = (row.get("results") or {}).get("brief") or ""
if not brief:
raise SystemExit(f"Brief for {company!r} ({row['created_at']}) is empty")
return brief, row.get("use_case") or "", row["created_at"][:19]
def run_once(ep: str, tok: str, brief: str, company: str, use_case: str,
owner: str, run_no: int, timeout_min: int) -> dict:
run_id = None
for _ in range(60):
try:
resp = H.call(ep, tok, "build_demo_from_brief",
{"brief": brief, "company_name": company,
"use_case": use_case, "owner_email": owner})
except Exception as e:
log(f"run {run_no}: start call failed ({type(e).__name__}) — retry in 30s")
time.sleep(30)
continue
status = resp.get("status")
if status == "started":
run_id = resp["run_id"]
log(f"run {run_no}: started run_id={run_id}")
break
if status == "busy":
log(f"run {run_no}: server busy — retry in 30s")
time.sleep(30)
continue
return {"run_no": run_no, "run_id": None, "grade": "FAIL",
"status": f"start failed: {str(resp)[:200]}", "elapsed_s": 0, "errors": []}
if not run_id:
return {"run_no": run_no, "run_id": None, "grade": "FAIL",
"status": "never got a slot", "elapsed_s": 0, "errors": []}
deadline = time.time() + timeout_min * 60
misses = 0
last_phase = ""
while time.time() < deadline:
try:
st = H.call(ep, tok, "status")
misses = 0
except Exception as e:
misses += 1
if misses >= 20:
return {"run_no": run_no, "run_id": run_id, "grade": "FAIL",
"status": "status endpoint unreachable", "elapsed_s": None, "errors": []}
time.sleep(30)
continue
recent = {x["run_id"]: x for x in st.get("recent_builds", [])}
if run_id in recent:
res = recent[run_id]
graded = H.grade(res)
return {"run_no": run_no, "run_id": run_id, "grade": graded["grade"],
"status": res.get("status"), "elapsed_s": res.get("elapsed_seconds"),
"errors": res.get("errors") or [],
"liveboard": res.get("liveboard_url"), "schema": res.get("schema")}
active = {x["run_id"]: x for x in st.get("active_builds", [])}.get(run_id, {})
phase = active.get("phase", "(waiting)")
if phase != last_phase:
log(f"run {run_no}: {phase} {active.get('elapsed_seconds', '')}s")
last_phase = phase
time.sleep(25)
return {"run_no": run_no, "run_id": run_id, "grade": "FAIL",
"status": f"timed out after {timeout_min} min", "elapsed_s": None, "errors": []}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--count", type=int, default=5)
ap.add_argument("--company", default="Tixr")
ap.add_argument("--owner", default="jack.rayner@thoughtspot.com")
ap.add_argument("--timeout-min", type=int, default=30, help="per-run timeout")
ap.add_argument("--brief-file", help="read the brief from a local file instead of Supabase "
"(use when the network can't reach Supabase)")
args = ap.parse_args()
ep = os.getenv("MCP_ENDPOINT", H.DEFAULT_ENDPOINT)
tok = os.getenv("MCP_ACCESS_TOKEN")
if not tok:
raise SystemExit("MCP_ACCESS_TOKEN not set (env or demoprep_mcp/.env)")
if args.brief_file:
brief = open(args.brief_file).read()
use_case, brief_ts = "Event Ticketing Analytics", f"file:{args.brief_file}"
else:
brief, use_case, brief_ts = fetch_latest_brief(args.company)
log(f"{args.company} brief from {brief_ts} | use_case={use_case!r} | len={len(brief)}")
log(f"Running {args.count}x sequentially against {ep}")
results = []
for i in range(1, args.count + 1):
log(f"===== RUN {i}/{args.count} =====")
results.append(run_once(ep, tok, brief, args.company, use_case,
args.owner, i, args.timeout_min))
passes = sum(1 for r in results if r["grade"] == "PASS")
print("\n" + "=" * 78)
print(f"{'run':>4} {'run_id':14} {'grade':6} {'status':22} {'elapsed':>9} errors")
print("-" * 78)
for r in results:
el = f"{r['elapsed_s']:.0f}s" if isinstance(r.get("elapsed_s"), (int, float)) else "-"
err = (r["errors"][0][:60] + "…") if r.get("errors") else ""
print(f"{r['run_no']:>4} {str(r.get('run_id'))[:12]:14} {r['grade']:6} "
f"{str(r.get('status'))[:22]:22} {el:>9} {err}")
print("-" * 78)
print(f"PASS RATE: {passes}/{len(results)}")
print("=" * 78)
print(json.dumps(results, indent=2, default=str))
sys.exit(0 if passes == len(results) else 1)
if __name__ == "__main__":
main()
|