Add app credentials, account-scoped idempotency and per-app terminal notifications. Consolidate the unreleased migration baseline, preserve rule descriptions as AI context, and regenerate provider contracts with regression coverage.
133 lines
4.9 KiB
Python
133 lines
4.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_APP_KEY_ENV = "AI_PLATFORM_DEMO_APP_API_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 app_headers(existing_app_key: str | None) -> dict[str, str]:
|
|
if not existing_app_key:
|
|
raise SystemExit(
|
|
f"Demo seed blocked: set {DEMO_APP_KEY_ENV} to an operator-issued app key; "
|
|
"no credential was created or rotated"
|
|
)
|
|
return {"Authorization": f"Bearer {existing_app_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_app_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 = app_headers(existing_app_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_app_key = os.environ.get(DEMO_APP_KEY_ENV)
|
|
try:
|
|
with httpx.Client(base_url=BASE_URL, timeout=5) as client:
|
|
run_id, run = seed_demo(client, settings, existing_app_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()
|