From a7c6ffe4dd11ef95f9d8ffda1a1d2a20771d6a38 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 23 Apr 2026 19:11:44 +0200 Subject: [PATCH] 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) --- .../tests/applicability_demo_sdk/README.md | 42 +++++++++++ .../applicability_demo_sdk/api_runner.py | 56 ++++++++++++++ .../contracts/response_contract.json | 35 +++++++++ .../contracts/response_schema.json | 73 +++++++++++++++++++ .../requests/CASE-001.json | 20 +++++ .../requests/CASE-002.json | 18 +++++ .../requests/CASE-004.json | 17 +++++ .../requests/CASE-006.json | 16 ++++ .../requests/CASE-008.json | 17 +++++ .../requests/CASE-011.json | 17 +++++ 10 files changed, 311 insertions(+) create mode 100644 control-pipeline/tests/applicability_demo_sdk/README.md create mode 100644 control-pipeline/tests/applicability_demo_sdk/api_runner.py create mode 100644 control-pipeline/tests/applicability_demo_sdk/contracts/response_contract.json create mode 100644 control-pipeline/tests/applicability_demo_sdk/contracts/response_schema.json create mode 100644 control-pipeline/tests/applicability_demo_sdk/requests/CASE-001.json create mode 100644 control-pipeline/tests/applicability_demo_sdk/requests/CASE-002.json create mode 100644 control-pipeline/tests/applicability_demo_sdk/requests/CASE-004.json create mode 100644 control-pipeline/tests/applicability_demo_sdk/requests/CASE-006.json create mode 100644 control-pipeline/tests/applicability_demo_sdk/requests/CASE-008.json create mode 100644 control-pipeline/tests/applicability_demo_sdk/requests/CASE-011.json diff --git a/control-pipeline/tests/applicability_demo_sdk/README.md b/control-pipeline/tests/applicability_demo_sdk/README.md new file mode 100644 index 0000000..6f2d442 --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/README.md @@ -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. diff --git a/control-pipeline/tests/applicability_demo_sdk/api_runner.py b/control-pipeline/tests/applicability_demo_sdk/api_runner.py new file mode 100644 index 0000000..ce5bcee --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/api_runner.py @@ -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() diff --git a/control-pipeline/tests/applicability_demo_sdk/contracts/response_contract.json b/control-pipeline/tests/applicability_demo_sdk/contracts/response_contract.json new file mode 100644 index 0000000..a6ab614 --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/contracts/response_contract.json @@ -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" + ] +} \ No newline at end of file diff --git a/control-pipeline/tests/applicability_demo_sdk/contracts/response_schema.json b/control-pipeline/tests/applicability_demo_sdk/contracts/response_schema.json new file mode 100644 index 0000000..056418f --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/contracts/response_schema.json @@ -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 +} \ No newline at end of file diff --git a/control-pipeline/tests/applicability_demo_sdk/requests/CASE-001.json b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-001.json new file mode 100644 index 0000000..3ac54ba --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-001.json @@ -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 + } +} \ No newline at end of file diff --git a/control-pipeline/tests/applicability_demo_sdk/requests/CASE-002.json b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-002.json new file mode 100644 index 0000000..93f5e80 --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-002.json @@ -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 + } +} \ No newline at end of file diff --git a/control-pipeline/tests/applicability_demo_sdk/requests/CASE-004.json b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-004.json new file mode 100644 index 0000000..5882f1c --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-004.json @@ -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 + } +} \ No newline at end of file diff --git a/control-pipeline/tests/applicability_demo_sdk/requests/CASE-006.json b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-006.json new file mode 100644 index 0000000..bb0d1be --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-006.json @@ -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 + } +} \ No newline at end of file diff --git a/control-pipeline/tests/applicability_demo_sdk/requests/CASE-008.json b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-008.json new file mode 100644 index 0000000..addd9fe --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-008.json @@ -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 + } +} \ No newline at end of file diff --git a/control-pipeline/tests/applicability_demo_sdk/requests/CASE-011.json b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-011.json new file mode 100644 index 0000000..c5232ce --- /dev/null +++ b/control-pipeline/tests/applicability_demo_sdk/requests/CASE-011.json @@ -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 + } +} \ No newline at end of file