"""AI-Act Art. 50 Transparenzpflicht-Check (vereinfacht). Art. 50 AI Act verlangt, dass Nutzer beim Interagieren mit einem KI-System (Chatbot, Sprachassistent etc.) erkennen können, dass sie mit einer KI sprechen — es sei denn, das ist offensichtlich aus dem Kontext heraus. Der Check ist heuristisch (kein LLM) und prüft drei Schichten: 1. AI-Provider-Detection in DSE und Vendor-Liste (Vertex AI, OpenAI, Anthropic, etc.) 2. Disclosure-Text-Detection in DSE / Cookie-Doc ("KI-System", "Sie chatten mit einer KI", "automatisiert", "Artificial Intelligence", "Konversations-KI", "GPT", …) 3. Cross-Check: AI-Provider gefunden + keine Disclosure → HIGH AI-Provider gefunden + Disclosure vorhanden, aber kein "Sie interagieren mit einer KI"-Hinweis → MEDIUM (Pre-Chat-Hinweis vor erstem Input gefordert; kann nur ein consent-tester-DOM-Scan verifizieren) Bekannte Limitation: ohne consent-tester-Erweiterung kann der Check nicht entscheiden, ob ein Pre-Chat-Hinweis im Live-DOM vor dem ersten Nutzer-Input erscheint. Wir flaggen das daher als MEDIUM "manuell verifizieren". """ from __future__ import annotations import logging import re logger = logging.getLogger(__name__) # AI-Anbieter / Modelle / Frameworks, die "AI" auslösen. _AI_KEYWORDS = ( "vertex ai", "google vertex", "openai", "gpt-3", "gpt-4", "chatgpt", "anthropic", "claude.ai", "claude-3", "mistral ai", "huggingface", "hugging face", "stable diffusion", "midjourney", "llama-2", "llama-3", "qwen", "deepseek", "perplexity ai", "azure openai", "copilot", "konversations-ki", "konversations ki", "ai assistant", "ai-assistant", "ki-assistent", "ki assistent", "intelligenter assistent", "automatisierter chat", "chatbot", "live-chat", ) # Phrasen, die als Art-50-Disclosure gelten. _DISCLOSURE_PHRASES = ( "sie chatten mit einer ki", "sie kommunizieren mit einer ki", "automatisierter chat", "automatisierter assistent", "ki-gestützter", "ki gestützt", "ki-gestuetzt", "künstliche intelligenz", "kuenstliche intelligenz", "artificial intelligence", "art. 50 ai act", "ai act art. 50", "art. 50 ki-vo", "ki-verordnung", "ki verordnung", "automatisiertes system", "generativ", "generative ki", "generative ai", "large language model", "llm-", "machine learning", ) def _has_any(text: str, phrases) -> list[str]: text_lc = (text or "").lower() if not text_lc: return [] return [p for p in phrases if p in text_lc] def _find_ai_in_vendors(cmp_vendors: list[dict]) -> list[dict]: """Find vendors whose name or category mentions an AI provider.""" hits: list[dict] = [] for v in cmp_vendors or []: haystack = " ".join([ (v.get("name") or "").lower(), (v.get("category") or "").lower(), (v.get("processing_company") or "").lower(), ]) if not haystack.strip(): continue matched = [k for k in _AI_KEYWORDS if k in haystack] if matched: hits.append({ "vendor": v.get("name") or "—", "matched": matched[:3], }) return hits def check_ai_act_transparency(state: dict) -> list[dict]: """Return findings about AI Art. 50 transparency obligations.""" doc_texts = state.get("doc_texts") or {} dse_text = doc_texts.get("dse") or "" cookie_text = doc_texts.get("cookie") or "" cmp_vendors = state.get("cmp_vendors") or [] if not dse_text and not cookie_text and not cmp_vendors: return [] ai_mentions_dse = _has_any(dse_text, _AI_KEYWORDS) ai_mentions_cookie = _has_any(cookie_text, _AI_KEYWORDS) ai_vendors = _find_ai_in_vendors(cmp_vendors) has_ai_signal = bool( ai_mentions_dse or ai_mentions_cookie or ai_vendors ) if not has_ai_signal: return [] disc_dse = _has_any(dse_text, _DISCLOSURE_PHRASES) disc_cookie = _has_any(cookie_text, _DISCLOSURE_PHRASES) has_disclosure = bool(disc_dse or disc_cookie) findings: list[dict] = [] summary_signals = ( f"DSE-AI-Hinweise: {len(ai_mentions_dse)} " f"(z.B. {', '.join(ai_mentions_dse[:3])}); " f"Cookie-AI-Hinweise: {len(ai_mentions_cookie)}; " f"AI-Vendors: {len(ai_vendors)}" ) if not has_disclosure: findings.append({ "check_id": "AI-ACT-TRANSPARENCY-001", "severity": "HIGH", "severity_reason": "missing", "title": ( "AI-Act Art. 50 Transparenz-Hinweis fehlt — " "KI-System eingesetzt, aber keine Nutzer-Erklärung" ), "norm": "AI Act Art. 50 Abs. 1 (Transparenz gegenüber Nutzern)", "detected_signals": summary_signals, "ai_vendors": ai_vendors, "ai_keywords_in_dse": ai_mentions_dse[:5], "ai_keywords_in_cookie": ai_mentions_cookie[:5], "action": ( "DSE und Pre-Chat-UI mit ausdrücklichem Hinweis " "'Sie kommunizieren mit einer KI (System X)' ergänzen. " "Anbieter offen nennen + Rechtsgrundlage + Speicherdauer." ), }) else: # AI detected + DSE-Disclosure vorhanden — aber Pre-Chat-Hinweis # im Live-DOM kann der Check nicht verifizieren. findings.append({ "check_id": "AI-ACT-TRANSPARENCY-002", "severity": "MEDIUM", "severity_reason": "manual_review_required", "title": ( "AI-Act Art. 50: DSE-Disclosure vorhanden — Pre-Chat-Hinweis " "im UI manuell verifizieren" ), "norm": "AI Act Art. 50 Abs. 1", "detected_signals": summary_signals, "ai_vendors": ai_vendors, "disclosure_in_dse": disc_dse[:3], "disclosure_in_cookie": disc_cookie[:3], "action": ( "Pre-Chat-UI öffnen: vor der ersten Nutzereingabe muss " "ein klarer Hinweis erscheinen, dass die Konversation " "mit einer KI geführt wird. Verifizieren ob Banner/Modal " "vorhanden oder reine Footnote." ), }) # Zusatzcheck: Wenn AI vorhanden und Rechtsgrundlage = berechtigtes # Interesse (Art. 6 Abs. 1 lit. f) statt Einwilligung — MEDIUM if ai_vendors or ai_mentions_dse: if _legitimate_interest_for_ai(dse_text): findings.append({ "check_id": "AI-ACT-RISK-001", "severity": "MEDIUM", "severity_reason": "misclassified", "title": ( "Rechtsgrundlage 'berechtigtes Interesse' für " "KI-Verarbeitung — Einwilligung empfehlen" ), "norm": "DSGVO Art. 6 Abs. 1 lit. a vs lit. f + AI Act", "detected_signals": ( "AI-Provider erkannt; Art. 6 Abs. 1 lit. f als " "Rechtsgrundlage in DSE genannt" ), "ai_vendors": ai_vendors, "action": ( "Bei generativer KI (insbesondere mit Drittland-" "Transfer und Profiling-Verwandtschaft) " "Rechtsgrundlage auf Einwilligung (Art. 6 Abs. 1 " "lit. a) umstellen. Interessenabwägung dokumentieren." ), }) if findings: logger.info("ai-act-transparency: %d findings", len(findings)) return findings def _legitimate_interest_for_ai(dse_text: str) -> bool: """Detect 'Rechtsgrundlage Art. 6 Abs. 1 lit. f' near AI mentions.""" text_lc = (dse_text or "").lower() if not text_lc: return False # crude proximity check: any of the AI keywords AND lit-f phrase # within a ~600 char window import re lit_f_patterns = ( "art. 6 abs. 1 lit. f", "artikel 6 abs. 1 lit. f", "art. 6 1 f", "berechtigtes interesse", "berechtigten interesses", ) for ai_kw in _AI_KEYWORDS: for pos in range(0, len(text_lc) - 200): window = text_lc[max(0, pos-300):pos+300] if ai_kw in window and any(p in window for p in lit_f_patterns): return True # don't walk every char; jump to next ai_kw occurrence idx = text_lc.find(ai_kw, pos) if idx == -1: break pos = idx + 1 break return False