feat(control-pipeline): add SDK endpoint demo package for applicability tests
Request payloads + response contract + api_runner.py for 6 priority cases. Can be run directly against /v1/applicability/evaluate endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
42
control-pipeline/tests/applicability_demo_sdk/README.md
Normal file
42
control-pipeline/tests/applicability_demo_sdk/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Applicability SDK Demo Contract Package
|
||||
|
||||
## Ziel
|
||||
Diese Version ist dafür gedacht, die Demo-Cases direkt gegen euren echten Endpoint zu schießen.
|
||||
|
||||
## Struktur
|
||||
- `requests/CASE-*.json` — Request-Payloads je Demo-Case
|
||||
- `contracts/response_contract.json` — fachlicher Mindestvertrag
|
||||
- `contracts/response_schema.json` — JSON-Schema für die technische Response-Struktur
|
||||
- `api_runner.py` — POSTet alle Cases an euren Endpoint und speichert die Responses
|
||||
- `../applicability_demo/evaluator.py` — kann anschließend gegen die gespeicherten Responses laufen
|
||||
|
||||
## Beispielablauf
|
||||
|
||||
### 1. Cases gegen euren Endpoint schicken
|
||||
```bash
|
||||
python api_runner.py --endpoint http://127.0.0.1:8098/v1/applicability/evaluate
|
||||
```
|
||||
|
||||
Die Responses landen dann in:
|
||||
```text
|
||||
actual_outputs/CASE-001.json
|
||||
...
|
||||
```
|
||||
|
||||
### 2. Gegen den Evaluator prüfen
|
||||
```bash
|
||||
python ../applicability_demo/evaluator.py --cases ../applicability_demo/demo_cases.yaml --actual-dir ./actual_outputs --report-json ./reports/latest_report.json --report-md ./reports/latest_report.md
|
||||
```
|
||||
|
||||
## Erwartung an euren Endpoint
|
||||
Request:
|
||||
- JSON POST
|
||||
- Request Body entspricht den Dateien in `requests/`
|
||||
|
||||
Response:
|
||||
- Muss mindestens die Felder aus `contracts/response_contract.json` enthalten
|
||||
|
||||
## Hinweise
|
||||
- Wenn euer Endpoint andere Feldnamen nutzt, baut einen kleinen Adapter vor dem Evaluator.
|
||||
- Wenn ihr mehrere Modi habt, könnt ihr `mode` nutzen, um deterministische Applicability-Analysen zu erzwingen.
|
||||
- Für Grenzfälle wie `CASE-011` soll das System nicht künstlich sicher tun, sondern eskalieren.
|
||||
56
control-pipeline/tests/applicability_demo_sdk/api_runner.py
Normal file
56
control-pipeline/tests/applicability_demo_sdk/api_runner.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib import request, error
|
||||
|
||||
def post_json(url: str, payload: dict, timeout: int = 60) -> dict:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = request.Request(
|
||||
url,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with request.urlopen(req, timeout=timeout) as resp:
|
||||
raw = resp.read().decode("utf-8")
|
||||
return json.loads(raw)
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Send demo applicability cases to an API endpoint.")
|
||||
parser.add_argument("--endpoint", required=True, help="Full HTTP endpoint URL")
|
||||
parser.add_argument("--requests-dir", type=Path, default=Path(__file__).resolve().parent / "requests")
|
||||
parser.add_argument("--out-dir", type=Path, default=Path(__file__).resolve().parent / "actual_outputs")
|
||||
parser.add_argument("--case-id", default=None, help="Optional single case id, e.g. CASE-001")
|
||||
args = parser.parse_args()
|
||||
|
||||
args.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
files = sorted(args.requests_dir.glob("CASE-*.json"))
|
||||
if args.case_id:
|
||||
files = [args.requests_dir / f"{args.case_id}.json"]
|
||||
|
||||
failures = 0
|
||||
for path in files:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
try:
|
||||
result = post_json(args.endpoint, payload)
|
||||
except error.HTTPError as exc:
|
||||
failures += 1
|
||||
print(f"[FAIL] {path.name}: HTTP {exc.code}", file=sys.stderr)
|
||||
continue
|
||||
except Exception as exc:
|
||||
failures += 1
|
||||
print(f"[FAIL] {path.name}: {exc}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
out_path = args.out_dir / path.name
|
||||
out_path.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"[OK] wrote {out_path}")
|
||||
|
||||
raise SystemExit(1 if failures else 0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "ApplicabilityAssessmentResponse",
|
||||
"description": "Mindestvertrag für Responses des Applicability-Endpoints.",
|
||||
"required_fields": {
|
||||
"case_id": "string",
|
||||
"assigned_controls": [
|
||||
"string"
|
||||
],
|
||||
"excluded_controls": [
|
||||
"string"
|
||||
],
|
||||
"escalations": [
|
||||
"string"
|
||||
],
|
||||
"inferred_industries": [
|
||||
"string"
|
||||
],
|
||||
"confidence": {
|
||||
"overall": "number",
|
||||
"industry_assignment": "number",
|
||||
"control_assignment": "number"
|
||||
},
|
||||
"explanation": "string",
|
||||
"uncertainty_flags": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"semantic_rules": [
|
||||
"must_assign controls müssen in assigned_controls enthalten sein",
|
||||
"must_not_assign controls dürfen nicht in assigned_controls enthalten sein",
|
||||
"escalate_for_legal_review muss in escalations abgebildet werden",
|
||||
"Grenzfälle sollen uncertainty_flags setzen",
|
||||
"explanation muss die juristische oder fachliche Abgrenzung nachvollziehbar beschreiben"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "ApplicabilityAssessmentResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"case_id",
|
||||
"assigned_controls",
|
||||
"excluded_controls",
|
||||
"escalations",
|
||||
"inferred_industries",
|
||||
"confidence",
|
||||
"explanation",
|
||||
"uncertainty_flags"
|
||||
],
|
||||
"properties": {
|
||||
"case_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"assigned_controls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"excluded_controls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"escalations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"inferred_industries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"confidence": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"overall",
|
||||
"industry_assignment",
|
||||
"control_assignment"
|
||||
],
|
||||
"properties": {
|
||||
"overall": {
|
||||
"type": "number"
|
||||
},
|
||||
"industry_assignment": {
|
||||
"type": "number"
|
||||
},
|
||||
"control_assignment": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string"
|
||||
},
|
||||
"uncertainty_flags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"case_id": "CASE-001",
|
||||
"mode": "applicability_assessment",
|
||||
"jurisdiction": "DE",
|
||||
"company_profile": {
|
||||
"company_type": "GmbH",
|
||||
"primary_industry": "retail_ecommerce",
|
||||
"summary": "Ein deutsches Unternehmen betreibt einen Webshop für physische Produkte. Zahlungen werden über Stripe Checkout abgewickelt. Das Unternehmen hält selbst keine Kundengelder, führt keine Zahlungskonten und bietet keine eigenen Zahlungsdienste an."
|
||||
},
|
||||
"facts": {
|
||||
"sells_physical_products": true,
|
||||
"webshop": true,
|
||||
"payment_provider": "Stripe",
|
||||
"stores_card_data": false,
|
||||
"holds_customer_funds": false,
|
||||
"operates_payment_service": false,
|
||||
"processes_personal_data": true,
|
||||
"sends_data_to_stripe": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"case_id": "CASE-002",
|
||||
"mode": "applicability_assessment",
|
||||
"jurisdiction": "DE",
|
||||
"company_profile": {
|
||||
"company_type": "AG",
|
||||
"primary_industry": "financial_services",
|
||||
"summary": "Eine Bank gibt physische TAN-Generatoren mit eingebauter Batterie an Endkunden aus. Die Geräte werden unter eigener Marke vertrieben."
|
||||
},
|
||||
"facts": {
|
||||
"provides_banking_services": true,
|
||||
"distributes_physical_products": true,
|
||||
"product_contains_battery": true,
|
||||
"product_under_own_brand": true,
|
||||
"imports_product_from_non_eu": false,
|
||||
"manufactures_product": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"case_id": "CASE-004",
|
||||
"mode": "applicability_assessment",
|
||||
"jurisdiction": "DE",
|
||||
"company_profile": {
|
||||
"company_type": "GmbH",
|
||||
"primary_industry": "financial_services",
|
||||
"summary": "Ein Fintech bietet eine App mit Wallet-Funktion, Kundengelder werden entgegengenommen und an Händler weitergeleitet."
|
||||
},
|
||||
"facts": {
|
||||
"provides_wallet": true,
|
||||
"holds_customer_funds": true,
|
||||
"executes_payment_transactions": true,
|
||||
"customer_onboarding": true,
|
||||
"transaction_monitoring": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"case_id": "CASE-006",
|
||||
"mode": "applicability_assessment",
|
||||
"jurisdiction": "DE",
|
||||
"company_profile": {
|
||||
"company_type": "UG",
|
||||
"primary_industry": "software_saas",
|
||||
"summary": "Eine SaaS-Plattform verschickt Login-Codes per Twilio/SMS-Gateway, betreibt aber kein eigenes öffentliches Telekommunikationsnetz und bietet keinen Telekommunikationsdienst am Markt an."
|
||||
},
|
||||
"facts": {
|
||||
"sends_sms_notifications": true,
|
||||
"uses_external_gateway": true,
|
||||
"provides_public_telecom_services": false,
|
||||
"operates_network": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"case_id": "CASE-008",
|
||||
"mode": "applicability_assessment",
|
||||
"jurisdiction": "DE",
|
||||
"company_profile": {
|
||||
"company_type": "GmbH",
|
||||
"primary_industry": "software_saas",
|
||||
"summary": "Ein Softwareunternehmen verkauft nun zusätzlich eigene IoT-Sensoren mit Batterie und Funkmodul unter eigener Marke."
|
||||
},
|
||||
"facts": {
|
||||
"imports_from_non_eu": true,
|
||||
"sells_hardware": true,
|
||||
"product_contains_battery": true,
|
||||
"product_has_radio": true,
|
||||
"own_brand": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"case_id": "CASE-011",
|
||||
"mode": "applicability_assessment",
|
||||
"jurisdiction": "DE",
|
||||
"company_profile": {
|
||||
"company_type": "GmbH",
|
||||
"primary_industry": "software_saas",
|
||||
"summary": "Eine Plattform ermöglicht Händlern Auszahlungen, virtuelle Konten, Split Settlements und einen Finanzierungsvorschuss, teilweise über Partnerbanken, teilweise über eigene Prozesse."
|
||||
},
|
||||
"facts": {
|
||||
"virtual_accounts": true,
|
||||
"split_settlements": true,
|
||||
"advance_payments": true,
|
||||
"partner_bank_involved": true,
|
||||
"own_funds_flow_unclear": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user