Spaces:
Running
Running
Deploy: Stack A (NIM brain + Maverick judge + Sarvam voice). D-019.
Browse files- tools/heavy_smoke_test.py +213 -0
tools/heavy_smoke_test.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Heavy live smoke test for the deployed Insurance Bot.
|
| 2 |
+
|
| 3 |
+
Exercises every major API path and RAG retrieval mode. Reports latency
|
| 4 |
+
+ success rate + brain_used + citation count + faithfulness verdict.
|
| 5 |
+
|
| 6 |
+
Usage:
|
| 7 |
+
PYTHONPATH=. .venv/bin/python tools/heavy_smoke_test.py
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import sys
|
| 14 |
+
import time
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
import httpx
|
| 19 |
+
|
| 20 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 21 |
+
|
| 22 |
+
BASE_URL = os.environ.get("SMOKE_BASE_URL", "https://rohitsar567-insurancebot.hf.space")
|
| 23 |
+
ADMIN_PW = os.environ.get("ADMIN_PASSWORD", "")
|
| 24 |
+
TIMEOUT = 90.0
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class Result:
|
| 29 |
+
name: str
|
| 30 |
+
ok: bool
|
| 31 |
+
latency_ms: int
|
| 32 |
+
info: str = ""
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
RESULTS: list[Result] = []
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def banner(s: str) -> None:
|
| 39 |
+
print()
|
| 40 |
+
print("=" * 80)
|
| 41 |
+
print(f" {s}")
|
| 42 |
+
print("=" * 80)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def record(name: str, ok: bool, latency_ms: int, info: str = "") -> None:
|
| 46 |
+
RESULTS.append(Result(name, ok, latency_ms, info))
|
| 47 |
+
sigil = "β" if ok else "β"
|
| 48 |
+
print(f" {sigil} {name:50s} {latency_ms:>6}ms {info}")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def ping(client: httpx.Client, path: str, name: str | None = None) -> dict | None:
|
| 52 |
+
t0 = time.time()
|
| 53 |
+
try:
|
| 54 |
+
r = client.get(f"{BASE_URL}{path}", timeout=TIMEOUT)
|
| 55 |
+
latency = int((time.time() - t0) * 1000)
|
| 56 |
+
ok = r.status_code == 200
|
| 57 |
+
info = f"HTTP {r.status_code}"
|
| 58 |
+
record(name or path, ok, latency, info)
|
| 59 |
+
return r.json() if ok else None
|
| 60 |
+
except Exception as e:
|
| 61 |
+
latency = int((time.time() - t0) * 1000)
|
| 62 |
+
record(name or path, False, latency, f"EXC: {type(e).__name__}: {str(e)[:80]}")
|
| 63 |
+
return None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def chat(client: httpx.Client, user_text: str, name: str, session_id: str = "smoke",
|
| 67 |
+
profile: dict | None = None, policy_filter_ids: list[str] | None = None) -> dict | None:
|
| 68 |
+
body = {
|
| 69 |
+
"user_text": user_text,
|
| 70 |
+
"session_id": session_id,
|
| 71 |
+
"chat_history": [],
|
| 72 |
+
"profile": profile or {},
|
| 73 |
+
"policy_filter_ids": policy_filter_ids,
|
| 74 |
+
"return_audio": False,
|
| 75 |
+
}
|
| 76 |
+
t0 = time.time()
|
| 77 |
+
try:
|
| 78 |
+
r = client.post(f"{BASE_URL}/api/chat", json=body, timeout=TIMEOUT)
|
| 79 |
+
latency = int((time.time() - t0) * 1000)
|
| 80 |
+
if r.status_code != 200:
|
| 81 |
+
record(name, False, latency, f"HTTP {r.status_code} body[:80]={r.text[:80]}")
|
| 82 |
+
return None
|
| 83 |
+
d = r.json()
|
| 84 |
+
ok = bool(d.get("reply_text")) and not d.get("blocked")
|
| 85 |
+
info = (
|
| 86 |
+
f"brain={d.get('brain_used','?')} "
|
| 87 |
+
f"intent={d.get('intent','?')} "
|
| 88 |
+
f"cites={len(d.get('citations',[]))} "
|
| 89 |
+
f"faith={d.get('faithfulness_passed','?')} "
|
| 90 |
+
f"reply_chars={len(d.get('reply_text',''))}"
|
| 91 |
+
)
|
| 92 |
+
record(name, ok, latency, info)
|
| 93 |
+
return d
|
| 94 |
+
except Exception as e:
|
| 95 |
+
latency = int((time.time() - t0) * 1000)
|
| 96 |
+
record(name, False, latency, f"EXC: {type(e).__name__}: {str(e)[:80]}")
|
| 97 |
+
return None
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def main() -> int:
|
| 101 |
+
print(f"Smoke testing: {BASE_URL}")
|
| 102 |
+
print(f" ADMIN_PASSWORD set: {bool(ADMIN_PW)}")
|
| 103 |
+
|
| 104 |
+
with httpx.Client() as client:
|
| 105 |
+
banner("1. Health + coverage")
|
| 106 |
+
h = ping(client, "/api/health")
|
| 107 |
+
if h:
|
| 108 |
+
print(f" providers_ok={h.get('providers_ok')}")
|
| 109 |
+
cov = ping(client, "/api/coverage")
|
| 110 |
+
if cov:
|
| 111 |
+
print(f" coverage: {cov.get('total_policies')} policies, "
|
| 112 |
+
f"{cov.get('total_insurers')} insurers, "
|
| 113 |
+
f"{cov.get('total_chunks')} chunks")
|
| 114 |
+
|
| 115 |
+
banner("2. Policy text-RAG (specific waiting period question)")
|
| 116 |
+
chat(client, "What is the pre-existing disease waiting period in Star Health Comprehensive?",
|
| 117 |
+
"PED waiting (Star)")
|
| 118 |
+
chat(client, "Does Niva Bupa ReAssure 2.0 have room rent capping?",
|
| 119 |
+
"room rent cap (Niva)")
|
| 120 |
+
chat(client, "What's the day-care procedures count for HDFC Optima Secure?",
|
| 121 |
+
"day-care count (HDFC)")
|
| 122 |
+
|
| 123 |
+
banner("3. Regulatory RAG boost (IRDAI / Insurance Act intent)")
|
| 124 |
+
chat(client, "Is a 36-month PED waiting period legal under IRDAI rules?",
|
| 125 |
+
"PED legality (regulatory)")
|
| 126 |
+
chat(client, "What does IRDAI mandate for the free-look period?",
|
| 127 |
+
"free-look mandate (regulatory)")
|
| 128 |
+
chat(client, "What's the ombudsman process for claim disputes?",
|
| 129 |
+
"ombudsman (regulatory)")
|
| 130 |
+
|
| 131 |
+
banner("4. Review RAG boost (reputation / claim experience)")
|
| 132 |
+
chat(client, "How is HDFC ERGO's claim settlement experience?",
|
| 133 |
+
"HDFC claim experience (review)")
|
| 134 |
+
chat(client, "What do customers say about Care Health?",
|
| 135 |
+
"Care reviews (review)")
|
| 136 |
+
chat(client, "Which insurer has the best Trustpilot rating?",
|
| 137 |
+
"cross-insurer review compare")
|
| 138 |
+
|
| 139 |
+
banner("5. Profile-aware multi-turn (fact-find continuity)")
|
| 140 |
+
# Turn 1: start with empty profile
|
| 141 |
+
chat(client, "I'm looking for health insurance, I'm 39 with diabetes",
|
| 142 |
+
"profile intake T1", session_id="profile-smoke",
|
| 143 |
+
profile={"age": 39, "health_conditions": ["diabetes"]})
|
| 144 |
+
# Turn 2: with same session β profile should auto-prepend
|
| 145 |
+
chat(client, "Which policy would you recommend for me?",
|
| 146 |
+
"profile-aware recommend T2", session_id="profile-smoke",
|
| 147 |
+
profile={"age": 39, "health_conditions": ["diabetes"]})
|
| 148 |
+
|
| 149 |
+
banner("6. Structured endpoints (no LLM in path)")
|
| 150 |
+
m = ping(client, "/api/policies/all", "marketplace /api/policies/all")
|
| 151 |
+
if m:
|
| 152 |
+
n = len(m.get("policies", []))
|
| 153 |
+
print(f" {n} policies returned")
|
| 154 |
+
# Pick a real policy_id to drill into
|
| 155 |
+
if m and m.get("policies"):
|
| 156 |
+
pid = m["policies"][0].get("policy_id")
|
| 157 |
+
if pid:
|
| 158 |
+
ping(client, f"/api/policies/{pid}/scorecard", f"scorecard {pid}")
|
| 159 |
+
ping(client, "/api/insurers/star-health/reviews", "reviews /api/insurers/star-health/reviews")
|
| 160 |
+
|
| 161 |
+
banner("7. Hallucination resistance (try to elicit fake info)")
|
| 162 |
+
chat(client, "What is the sum insured limit for Star Galaxy Premium 2030?",
|
| 163 |
+
"fake policy name (should refuse)")
|
| 164 |
+
chat(client, "Tell me about Acko Diamond Plus Plus Pro Max",
|
| 165 |
+
"fake variant name (should refuse)")
|
| 166 |
+
|
| 167 |
+
banner("8. Admin endpoints (gated β should return 404 without auth)")
|
| 168 |
+
try:
|
| 169 |
+
r = client.get(f"{BASE_URL}/api/admin/health", timeout=15)
|
| 170 |
+
ok = r.status_code == 404
|
| 171 |
+
record("admin without password (expect 404)", ok, 0, f"HTTP {r.status_code}")
|
| 172 |
+
except Exception as e:
|
| 173 |
+
record("admin without password", False, 0, str(e)[:80])
|
| 174 |
+
if ADMIN_PW:
|
| 175 |
+
try:
|
| 176 |
+
r = client.get(f"{BASE_URL}/api/admin/health",
|
| 177 |
+
headers={"X-Admin-Password": ADMIN_PW}, timeout=15)
|
| 178 |
+
ok = r.status_code == 200
|
| 179 |
+
info = f"HTTP {r.status_code}"
|
| 180 |
+
if ok:
|
| 181 |
+
d = r.json()
|
| 182 |
+
info += f" healthy={d.get('by_status',{}).get('healthy',0)} models"
|
| 183 |
+
record("admin with password (expect 200)", ok, 0, info)
|
| 184 |
+
except Exception as e:
|
| 185 |
+
record("admin with password", False, 0, str(e)[:80])
|
| 186 |
+
|
| 187 |
+
banner("9. Latency p50/p95 across all /api/chat calls")
|
| 188 |
+
chat_lats = [r.latency_ms for r in RESULTS if "T1" in r.name or "T2" in r.name or "PED" in r.name or "room" in r.name or "day-care" in r.name or "free-look" in r.name or "ombudsman" in r.name or "review" in r.name.lower()]
|
| 189 |
+
if chat_lats:
|
| 190 |
+
sorted_l = sorted(chat_lats)
|
| 191 |
+
n = len(sorted_l)
|
| 192 |
+
p50 = sorted_l[n // 2]
|
| 193 |
+
p95 = sorted_l[min(int(n * 0.95), n - 1)]
|
| 194 |
+
mean = sum(sorted_l) / n
|
| 195 |
+
print(f" Chat calls: n={n} mean={mean:.0f}ms p50={p50}ms p95={p95}ms")
|
| 196 |
+
|
| 197 |
+
banner("10. Summary")
|
| 198 |
+
ok_count = sum(1 for r in RESULTS if r.ok)
|
| 199 |
+
total = len(RESULTS)
|
| 200 |
+
print(f" Total tests: {total}")
|
| 201 |
+
print(f" Passed: {ok_count}")
|
| 202 |
+
print(f" Failed: {total - ok_count}")
|
| 203 |
+
if total - ok_count > 0:
|
| 204 |
+
print(f"\n FAILED:")
|
| 205 |
+
for r in RESULTS:
|
| 206 |
+
if not r.ok:
|
| 207 |
+
print(f" - {r.name}: {r.info}")
|
| 208 |
+
|
| 209 |
+
return 0 if ok_count == total else 1
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
if __name__ == "__main__":
|
| 213 |
+
sys.exit(main())
|