160 lines
5.9 KiB
Python
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()
|