dotb_ai-platform/scripts/seed_demo.py

160 lines
5.9 KiB
Python

"""Seed one local rules-only demo run through the public HTTP API."""
import json
import os
import time
from pathlib import Path
from typing import Any
from urllib.parse import urlsplit
import httpx
from ai_platform.infrastructure.config.settings import Settings
from ai_platform.presentation.http.schemas import RunRequest
FIXTURE = Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "mock_omni_run.json"
BASE_URL = "http://127.0.0.1:8000"
DEMO_TENANT = "local-demo-seed"
DEMO_TENANT_KEY_ENV = "AI_PLATFORM_DEMO_TENANT_KEY"
POLL_ATTEMPTS = 100
POLL_INTERVAL_SECONDS = 0.1
def load_payload() -> dict[str, Any]:
return json.loads(FIXTURE.read_text())
def validate_local_target(settings: Settings) -> None:
database_name = urlsplit(settings.database_url).path.rstrip("/").rsplit("/", 1)[-1]
database_host = urlsplit(settings.database_url).hostname
if settings.environment != "development":
raise SystemExit("Refusing demo seed: AI_PLATFORM_ENVIRONMENT must be development")
if database_name != "ai_platform" or database_host not in {
"127.0.0.1",
"localhost",
"::1",
"postgres",
}:
raise SystemExit(
"Refusing demo seed: API target must use the local ai_platform development database"
)
def tenant_headers(
client: httpx.Client,
settings: Settings,
existing_tenant_key: str | None,
) -> dict[str, str]:
if existing_tenant_key:
return {"Authorization": f"Bearer {existing_tenant_key}"}
if settings.tenant_provisioning_secret is None:
raise SystemExit(
"Demo seed blocked: tenant provisioning secret and existing demo tenant key are required"
)
response = client.post(
"/v1/platform/tenant-keys",
headers={
"Authorization": ("Bearer " + settings.tenant_provisioning_secret.get_secret_value())
},
json={"tenant_id": DEMO_TENANT},
)
if response.status_code == 409:
raise SystemExit(
"Demo seed blocked: the demo tenant already exists and its one-time key is unavailable; "
f"set {DEMO_TENANT_KEY_ENV} to its existing key. No key was rotated or data changed."
)
if response.status_code != 201:
raise SystemExit(
f"Demo seed blocked: tenant provisioning returned HTTP {response.status_code}"
)
tenant_key = response.json().get("tenant_key")
if not isinstance(tenant_key, str) or not tenant_key:
raise SystemExit("Demo seed blocked: tenant provisioning response omitted its key")
return {"Authorization": f"Bearer {tenant_key}"}
def read_run(client: httpx.Client, run_id: str, headers: dict[str, str]) -> dict[str, Any]:
for _ in range(POLL_ATTEMPTS):
response = client.get(f"/v1/lead-scoring/runs/{run_id}", headers=headers)
if response.status_code != 200:
raise SystemExit(
f"Demo seed blocked: run readback returned HTTP {response.status_code}"
)
run = response.json()
if run.get("status") in {"completed", "failed"}:
return run
time.sleep(POLL_INTERVAL_SECONDS)
raise SystemExit("Demo seed blocked: run did not reach a terminal state")
def persisted_snapshot(run: dict[str, Any]) -> dict[str, Any] | None:
snapshot = run.get("snapshot")
if isinstance(snapshot, dict):
return snapshot
request = run.get("request")
if isinstance(request, dict) and isinstance(request.get("snapshot"), dict):
return request["snapshot"]
return None
def seed_demo(
client: httpx.Client,
settings: Settings,
existing_tenant_key: str | None = None,
) -> tuple[str, dict[str, Any]]:
payload = load_payload()
if payload["scoring_config"]["ai_weight"] != 0:
raise SystemExit("Demo seed requires a rules-only fixture; it must not claim AI success")
headers = tenant_headers(client, settings, existing_tenant_key)
response = client.post(
"/v1/lead-scoring/runs",
headers={**headers, "Idempotency-Key": payload["request_id"]},
json=payload,
)
if response.status_code not in {200, 202}:
raise SystemExit(f"Demo seed blocked: run admission returned HTTP {response.status_code}")
run_id = response.json().get("run_id")
if not isinstance(run_id, str) or not run_id:
raise SystemExit("Demo seed blocked: run admission response omitted run_id")
run = read_run(client, run_id, headers)
if run.get("status") != "completed":
raise SystemExit("Demo seed blocked: the persisted run did not complete")
if run.get("ai_score") is not None or run.get("degraded") is not False:
raise SystemExit("Demo seed blocked: rules-only run returned an unexpected AI result")
expected_snapshot = RunRequest.model_validate(payload).model_dump(mode="json")["snapshot"]
if persisted_snapshot(run) != expected_snapshot:
raise SystemExit(
"Demo seed blocked: run readback did not return the persisted conversation snapshot"
)
return run_id, run
def main() -> None:
settings = Settings.load()
validate_local_target(settings)
existing_tenant_key = os.environ.get(DEMO_TENANT_KEY_ENV)
try:
with httpx.Client(base_url=BASE_URL, timeout=5) as client:
run_id, run = seed_demo(client, settings, existing_tenant_key)
except httpx.HTTPError as exc:
raise SystemExit(
"Demo seed blocked: local backend at 127.0.0.1:8000 is unavailable"
) from exc
snapshot = load_payload()["snapshot"]
conversations = len(snapshot["conversations"])
messages = sum(len(conversation["messages"]) for conversation in snapshot["conversations"])
print(
f"Seeded or reused local demo run {run_id}: status={run['status']}, "
f"score={run.get('final_score')}, persisted conversations={conversations}, messages={messages}."
)
print("AI execution was not requested; the run uses deterministic rules only.")
if __name__ == "__main__":
main()