|
| 1 | + |
| 2 | +import asyncio |
| 3 | +import httpx |
| 4 | +import json |
| 5 | +import sys |
| 6 | +from datetime import datetime |
| 7 | + |
| 8 | +# Configuration |
| 9 | +API_URL = "http://localhost:8000/api/v1" |
| 10 | +# For a real demo with auth, we would need to login first. |
| 11 | +# Assuming default dev setup might allow open access or we mock a token if RBAC is strict. |
| 12 | +# Based on existing code, some endpoints might be protected. |
| 13 | +# Let's assume we can use a hardcoded admin token or similar if needed, |
| 14 | +# but for now we'll try to follow the likely open dev paths or basic auth patterns. |
| 15 | +# Note: In a fresh dev setup, we might need to create a user first. |
| 16 | + |
| 17 | +print(f"🚀 Starting AI Governance Hub Demo against {API_URL}...\n") |
| 18 | + |
| 19 | +async def run_demo(): |
| 20 | + async with httpx.AsyncClient(base_url=API_URL, timeout=10.0) as client: |
| 21 | + |
| 22 | + # 1. Check Health |
| 23 | + print("[1] Checking API Health...") |
| 24 | + try: |
| 25 | + resp = await client.get("/") |
| 26 | + print(f" ✅ API is up: {resp.json()['message']}") |
| 27 | + except Exception as e: |
| 28 | + print(f" ❌ API Not Reachable: {e}") |
| 29 | + print(" Make sure 'docker compose up' is running!") |
| 30 | + sys.exit(1) |
| 31 | + |
| 32 | + # 2. Register a High-Risk Model |
| 33 | + print("\n[2] Registering 'Credit Risk Scoring v1' Model...") |
| 34 | + model_payload = { |
| 35 | + "name": f"Credit Risk Scoring {datetime.now().strftime('%H%M%S')}", |
| 36 | + "owner": "Finance-Risk-Team", |
| 37 | + "description": "Predicts loan default probability for retail customers.", |
| 38 | + "risk_level": "high", |
| 39 | + "domain": "finance", |
| 40 | + "data_sensitivity": "pii", |
| 41 | + "data_classification": "confidential", |
| 42 | + "monitor_plan": "Monthly drift checks" |
| 43 | + } |
| 44 | + resp = await client.post("/models/", json=model_payload) |
| 45 | + if resp.status_code not in [200, 201]: |
| 46 | + print(f" ❌ Failed: {resp.text}") |
| 47 | + return |
| 48 | + model = resp.json() |
| 49 | + model_id = model['id'] |
| 50 | + print(f" ✅ Created Model ID {model_id}: {model['name']}") |
| 51 | + |
| 52 | + # 3. Register a Sensitive Dataset |
| 53 | + print("\n[3] Registering 'Customer Transaction Data 2024' Dataset...") |
| 54 | + dataset_payload = { |
| 55 | + "name": "Customer Transactions 2024", |
| 56 | + "source_system": "DataLake_Finance", |
| 57 | + "data_sensitivity": "pii", |
| 58 | + "data_classification": "restricted" |
| 59 | + } |
| 60 | + resp = await client.post("/datasets/", json=dataset_payload) |
| 61 | + dataset = resp.json() |
| 62 | + dataset_id = dataset['id'] |
| 63 | + print(f" ✅ Created Dataset ID {dataset_id}: {dataset['name']} ({dataset['data_sensitivity']})") |
| 64 | + |
| 65 | + # 4. Link Dataset to Model (Lineage) |
| 66 | + print("\n[4] Linking Dataset to Model...") |
| 67 | + link_payload = { |
| 68 | + "dataset_id": dataset_id, |
| 69 | + "dataset_type": "training" |
| 70 | + } |
| 71 | + resp = await client.post(f"/models/{model_id}/datasets/", json=link_payload) |
| 72 | + print(" ✅ Lineage Established") |
| 73 | + |
| 74 | + # 5. Create a Policy (Block High Risk without Approval) |
| 75 | + print("\n[5] Creating Policy: 'Block Unapproved High Risk'...") |
| 76 | + policy_payload = { |
| 77 | + "name": f"High Risk Guardrail {datetime.now().strftime('%H%M')}", |
| 78 | + "description": "Blocks deployment of high-risk models without human approval", |
| 79 | + "scope": "global", |
| 80 | + "condition_type": "block_high_risk_without_approval", |
| 81 | + "is_active": True |
| 82 | + } |
| 83 | + # Note: Policy endpoint might differ slightly based on implementation, adjusting if needed |
| 84 | + # Assuming /policies/ based on previous browsing |
| 85 | + resp = await client.post("/policies/", json=policy_payload) |
| 86 | + if resp.status_code in [200, 201]: |
| 87 | + print(f" ✅ Policy Created: {resp.json()['name']}") |
| 88 | + else: |
| 89 | + print(f" ⚠️ Could not create policy (might already exist): {resp.status_code}") |
| 90 | + |
| 91 | + # 6. Attempt Invalid Status Change (Simulating Policy Violation) |
| 92 | + print("\n[6] Attempting to approve model WITHOUT missing notes...") |
| 93 | + # Assuming we try to set to 'approved' directly |
| 94 | + # The prompt mentioned "Human Approvals" requiring notes. |
| 95 | + status_payload = { |
| 96 | + "status": "approved", |
| 97 | + "reason": "Rushing into production!" |
| 98 | + # Missing "approval_notes" |
| 99 | + } |
| 100 | + resp = await client.patch(f"/models/{model_id}/compliance-status", json=status_payload) |
| 101 | + if resp.status_code == 400 or resp.status_code == 403: |
| 102 | + print(f" ✅ Request Blocked as Expected! Code: {resp.status_code}") |
| 103 | + print(f" Reason: {resp.json()['detail']}") |
| 104 | + else: |
| 105 | + print(f" ❌ Unexpected success or error: {resp.status_code} {resp.text}") |
| 106 | + |
| 107 | + # 7. Valid Approval (Human-in-the-Loop) |
| 108 | + print("\n[7] Approving model WITH required notes...") |
| 109 | + status_payload_valid = { |
| 110 | + "status": "approved", |
| 111 | + "reason": "Passed all risk checks.", |
| 112 | + "approval_notes": "Reviewed by CISO on 2024-01-01. Mitigation controls in place." |
| 113 | + } |
| 114 | + resp = await client.patch(f"/models/{model_id}/compliance-status", json=status_payload_valid) |
| 115 | + if resp.status_code == 200: |
| 116 | + updated_model = resp.json() |
| 117 | + print(f" ✅ Model Approved!") |
| 118 | + print(f" Approver ID: {updated_model.get('approved_by_user_id')}") |
| 119 | + print(f" Notes: {updated_model.get('approval_notes')}") |
| 120 | + else: |
| 121 | + print(f" ❌ Failed to approve: {resp.status_code} {resp.text}") |
| 122 | + |
| 123 | + print("\n✨ Demo Completed Successfully! View your model in the Dashboard.") |
| 124 | + |
| 125 | +if __name__ == "__main__": |
| 126 | + asyncio.run(run_demo()) |
0 commit comments