feat(agents): Cross-Placement-Agent (deplatzierter Content)
CI / detect-changes (push) Successful in 6s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Successful in 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 29s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / detect-changes (push) Successful in 6s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Successful in 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 29s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
Sprint 1.9 (User-Vorgabe 2026-06-09):
Erkennt im Impressum Inhalts-Sektionen die thematisch besser in
einen Footer-Reiter 'Legal' gehoeren:
- Urheberrecht / Copyright -> LOW (Footer 'Legal')
- Bilder & Lizenzen -> LOW (Seite 'Bildquellen')
- Haftungsausschluss / Disclaimer -> LOW (Seite 'Disclaimer')
- Nutzungsbedingungen -> LOW (Seite 'AGB')
- Aenderungsvorbehalt -> LOW
- ElektroG / WEEE-Reg -> MEDIUM (Produktinfo)
- VerpackG / LUCID -> MEDIUM
- BattG -> MEDIUM
Each Finding empfiehlt konkret den 'Legal'-Footer-Reiter
einzufuehren als Best Practice ('Impressum bleibt schlank
und enthaelt ausschliesslich die Pflichtangaben nach § 5
TMG/DDG').
Tests gegen die 5 GT-Impressums:
- Safetykon: 3 Findings (Urheberrecht, Bilder/Lizenzen,
Haftungsausschluss)
- Hectronic: 3 Findings (WEEE-MEDIUM, Copyright, Haftung)
- ETO/BMW/Elli: 0 Findings (sauber)
- 9/9 Tests gruen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,15 +29,18 @@ from ._base import (
|
|||||||
)
|
)
|
||||||
from ._registry import REGISTRY
|
from ._registry import REGISTRY
|
||||||
from .cookie_policy import CookiePolicyAgent
|
from .cookie_policy import CookiePolicyAgent
|
||||||
|
from .cross_placement import CrossPlacementAgent
|
||||||
from .impressum import ImpressumAgent
|
from .impressum import ImpressumAgent
|
||||||
|
|
||||||
# Self-register all agents
|
# Self-register all agents
|
||||||
REGISTRY.register(ImpressumAgent())
|
REGISTRY.register(ImpressumAgent())
|
||||||
REGISTRY.register(CookiePolicyAgent())
|
REGISTRY.register(CookiePolicyAgent())
|
||||||
|
REGISTRY.register(CrossPlacementAgent())
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AgentInput", "AgentOutput", "BaseSpecialistAgent",
|
"AgentInput", "AgentOutput", "BaseSpecialistAgent",
|
||||||
"EscalationLog", "EvidenceSource", "Finding", "McCoverage",
|
"EscalationLog", "EvidenceSource", "Finding", "McCoverage",
|
||||||
"Recommendation", "Severity", "SourceType",
|
"Recommendation", "Severity", "SourceType",
|
||||||
"REGISTRY", "ImpressumAgent", "CookiePolicyAgent",
|
"REGISTRY", "ImpressumAgent", "CookiePolicyAgent",
|
||||||
|
"CrossPlacementAgent",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""Cross-Placement-Agent — erkennt deplatzierten Content.
|
||||||
|
|
||||||
|
User-Vorgabe 2026-06-09: bei Hectronic/Safetykon stehen Copyright,
|
||||||
|
Haftungsausschluss, Bilder-Lizenzen, WEEE/ElektroG im Impressum,
|
||||||
|
gehoeren aber in einen separaten 'Legal'-Footer-Reiter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .agent import CrossPlacementAgent
|
||||||
|
|
||||||
|
__all__ = ["CrossPlacementAgent"]
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
"""Cross-Placement-Agent — empfiehlt Footer-Reiter 'Legal' bei
|
||||||
|
deplatziertem Content.
|
||||||
|
|
||||||
|
Methodik:
|
||||||
|
1. Findet Sektions-Headings im Impressum (Copyright, Disclaimer,
|
||||||
|
ElektroG, …).
|
||||||
|
2. Pro Treffer ein LOW/MEDIUM-Finding mit Best-Practice-Empfehlung
|
||||||
|
wohin der Inhalt umziehen sollte.
|
||||||
|
3. Rollup buendelt mehrere Treffer zu einer Master-Empfehlung
|
||||||
|
('Footer-Reiter \"Legal\" einfuehren').
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from .._base import (
|
||||||
|
AgentInput,
|
||||||
|
AgentOutput,
|
||||||
|
BaseSpecialistAgent,
|
||||||
|
EvidenceSource,
|
||||||
|
Finding,
|
||||||
|
McCoverage,
|
||||||
|
Severity,
|
||||||
|
SourceType,
|
||||||
|
lint_output,
|
||||||
|
)
|
||||||
|
from .._rollup import rollup
|
||||||
|
from .sections import SECTIONS, find_sections
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
_SEVERITY_FOR_CATEGORY = {
|
||||||
|
"recommended_separate": Severity.LOW,
|
||||||
|
"product_compliance": Severity.MEDIUM,
|
||||||
|
"mandatory": Severity.INFO,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CrossPlacementAgent(BaseSpecialistAgent):
|
||||||
|
agent_id = "cross_placement"
|
||||||
|
agent_version = "1.0"
|
||||||
|
doc_type = "impressum" # arbeitet auf Impressum-Texten
|
||||||
|
owned_mc_ids = tuple(f"CP-PLACE-{s.section_id.upper()}"
|
||||||
|
for s in SECTIONS)
|
||||||
|
|
||||||
|
async def evaluate(self, agent_input: AgentInput) -> AgentOutput:
|
||||||
|
start = datetime.now(timezone.utc)
|
||||||
|
text = (agent_input.text or "").strip()
|
||||||
|
coverage: list[McCoverage] = []
|
||||||
|
findings: list[Finding] = []
|
||||||
|
|
||||||
|
if len(text) < 100:
|
||||||
|
for sec in SECTIONS:
|
||||||
|
coverage.append(McCoverage(
|
||||||
|
mc_id=f"CP-PLACE-{sec.section_id.upper()}",
|
||||||
|
status="skipped",
|
||||||
|
reason="text too short",
|
||||||
|
))
|
||||||
|
return self._finalize(start, findings, coverage, confidence=0.0,
|
||||||
|
notes="Text zu kurz für Placement-Check.")
|
||||||
|
|
||||||
|
hits = find_sections(text)
|
||||||
|
hit_ids = {sec.section_id for sec, _ in hits}
|
||||||
|
for sec in SECTIONS:
|
||||||
|
mc_id = f"CP-PLACE-{sec.section_id.upper()}"
|
||||||
|
if sec.section_id not in hit_ids:
|
||||||
|
coverage.append(McCoverage(
|
||||||
|
mc_id=mc_id, status="ok",
|
||||||
|
reason="section not present",
|
||||||
|
))
|
||||||
|
continue
|
||||||
|
evidence = next(s for sec2, s in hits
|
||||||
|
if sec2.section_id == sec.section_id)
|
||||||
|
sev = _SEVERITY_FOR_CATEGORY.get(sec.category, Severity.LOW)
|
||||||
|
findings.append(Finding(
|
||||||
|
check_id=f"CROSS-PLACE-{sec.section_id.upper()}",
|
||||||
|
agent=self.agent_id,
|
||||||
|
agent_version=self.agent_version,
|
||||||
|
field_id=sec.section_id,
|
||||||
|
severity=sev,
|
||||||
|
severity_reason="misplaced_content",
|
||||||
|
title=(
|
||||||
|
f"Sektion '{sec.label}' im Impressum — empfohlene "
|
||||||
|
f"Platzierung: {sec.recommended_location}"
|
||||||
|
),
|
||||||
|
norm=sec.norm,
|
||||||
|
evidence=f'„{evidence}"',
|
||||||
|
action=(
|
||||||
|
f"Sektion '{sec.label}' aus dem Impressum auslagern "
|
||||||
|
f"nach {sec.recommended_location}. Best Practice: "
|
||||||
|
f"einen Footer-Reiter 'Legal' einrichten, der die "
|
||||||
|
f"folgenden Inhalte sammelt: Copyright, Bildquellen, "
|
||||||
|
f"Disclaimer, Nutzungsbedingungen. Das Impressum "
|
||||||
|
f"bleibt schlank und enthält ausschließlich die "
|
||||||
|
f"Pflichtangaben nach § 5 TMG/DDG."
|
||||||
|
),
|
||||||
|
confidence=0.85,
|
||||||
|
sources=[EvidenceSource(
|
||||||
|
source_type=SourceType.MC,
|
||||||
|
source_id=mc_id,
|
||||||
|
detail=f"heading detected via regex",
|
||||||
|
confidence=0.85,
|
||||||
|
)],
|
||||||
|
))
|
||||||
|
coverage.append(McCoverage(
|
||||||
|
mc_id=mc_id, status=sev.value.lower(),
|
||||||
|
reason="misplaced content detected",
|
||||||
|
))
|
||||||
|
|
||||||
|
confidence = 0.92 if not findings else 0.85
|
||||||
|
return self._finalize(
|
||||||
|
start, findings, coverage, confidence=confidence,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _finalize(
|
||||||
|
self,
|
||||||
|
start: datetime,
|
||||||
|
findings: list[Finding],
|
||||||
|
coverage: list[McCoverage],
|
||||||
|
confidence: float,
|
||||||
|
notes: str = "",
|
||||||
|
) -> AgentOutput:
|
||||||
|
end = datetime.now(timezone.utc)
|
||||||
|
recs = rollup(findings)
|
||||||
|
out = AgentOutput(
|
||||||
|
agent=self.agent_id,
|
||||||
|
agent_version=self.agent_version,
|
||||||
|
started_at=start,
|
||||||
|
finished_at=end,
|
||||||
|
duration_ms=int((end - start).total_seconds() * 1000),
|
||||||
|
findings=findings,
|
||||||
|
recommendations=recs,
|
||||||
|
mc_coverage=coverage,
|
||||||
|
escalation_log=[],
|
||||||
|
confidence=confidence,
|
||||||
|
notes=notes,
|
||||||
|
mc_total=len(coverage),
|
||||||
|
mc_ok=sum(1 for c in coverage if c.status == "ok"),
|
||||||
|
mc_na=sum(1 for c in coverage if c.status == "na"),
|
||||||
|
mc_high=sum(1 for c in coverage if c.status == "high"),
|
||||||
|
mc_medium=sum(1 for c in coverage if c.status == "medium"),
|
||||||
|
mc_low=sum(1 for c in coverage if c.status == "low"),
|
||||||
|
)
|
||||||
|
return lint_output(out)
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"""Klassifikation von Impressum-Sektionen — was gehört rein, was nicht.
|
||||||
|
|
||||||
|
Pflicht-Sektionen (gehören ins Impressum):
|
||||||
|
- Anbieter (Name, Anschrift)
|
||||||
|
- Kontakt (Tel/E-Mail)
|
||||||
|
- Vertretungsberechtigte
|
||||||
|
- Handelsregister + USt-IdNr.
|
||||||
|
- § 18 MStV (Verantwortlicher)
|
||||||
|
- § 36 VSBG + Art. 14 EU-VO 524/2013
|
||||||
|
- Aufsichtsbehoerde + Berufsangaben (regulated)
|
||||||
|
|
||||||
|
Empfohlen-anders-platziert (Best-Practice in eigenem Footer-Bereich):
|
||||||
|
- Copyright / Urheberrecht / Marken
|
||||||
|
- Bilder / Lizenzen / Quellen
|
||||||
|
- Haftungsausschluss / Disclaimer / Disclaimer-Links
|
||||||
|
- Nutzungsbedingungen / AGB
|
||||||
|
- ElektroG / WEEE / VerpackG / BattG (Produkt-Compliance)
|
||||||
|
- Datenschutz-Erklaerung (eigene Seite)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Pattern
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SectionPattern:
|
||||||
|
"""Eine Sektion die wir im Impressum erkennen können."""
|
||||||
|
section_id: str
|
||||||
|
label: str
|
||||||
|
# Heading-Pattern: matched typischerweise H2/H3-artige Sektions-
|
||||||
|
# Überschriften innerhalb des Impressum-Texts.
|
||||||
|
heading_patterns: tuple[Pattern[str], ...] = field(default_factory=tuple)
|
||||||
|
# In welche Kategorie gehoert die Sektion?
|
||||||
|
# 'mandatory' = klassisches Impressum
|
||||||
|
# 'recommended_separate' = gehoert besser in 'Legal'-Footer
|
||||||
|
# 'product_compliance' = ElektroG/WEEE etc, kontextabhaengig
|
||||||
|
category: str = "recommended_separate"
|
||||||
|
# Best-Practice-Vorschlag wohin der Inhalt umziehen sollte
|
||||||
|
recommended_location: str = ""
|
||||||
|
norm: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
SECTIONS: tuple[SectionPattern, ...] = (
|
||||||
|
SectionPattern(
|
||||||
|
section_id="urheberrecht",
|
||||||
|
label="Urheberrecht",
|
||||||
|
heading_patterns=(
|
||||||
|
re.compile(r"\bUrheberrecht(?:e|sschutz|svermerk)?\b",
|
||||||
|
re.IGNORECASE),
|
||||||
|
re.compile(r"\bCopyright\b", re.IGNORECASE),
|
||||||
|
),
|
||||||
|
category="recommended_separate",
|
||||||
|
recommended_location="Footer-Reiter 'Legal' / Seite 'Copyright'",
|
||||||
|
norm="UrhG — gehört nicht zwingend ins Impressum nach § 5 TMG",
|
||||||
|
),
|
||||||
|
SectionPattern(
|
||||||
|
section_id="bilder_lizenzen",
|
||||||
|
label="Bilder & Lizenzen",
|
||||||
|
heading_patterns=(
|
||||||
|
re.compile(r"\bBild(?:er|nachweis|quellen)\s*[&und]+\s*Lizenz",
|
||||||
|
re.IGNORECASE),
|
||||||
|
re.compile(r"\bLizenz(?:en|hinweis|nachweis)e?\b",
|
||||||
|
re.IGNORECASE),
|
||||||
|
re.compile(r"\b(?:Bild|Foto|Image)\s*[:\-]\s*\w+",
|
||||||
|
re.IGNORECASE),
|
||||||
|
),
|
||||||
|
category="recommended_separate",
|
||||||
|
recommended_location="Footer-Reiter 'Legal' / Seite 'Bildquellen'",
|
||||||
|
norm="Lizenzgeber-Pflicht (z.B. CC-BY) — eigene Seite üblich",
|
||||||
|
),
|
||||||
|
SectionPattern(
|
||||||
|
section_id="haftungsausschluss",
|
||||||
|
label="Haftungsausschluss / Disclaimer",
|
||||||
|
heading_patterns=(
|
||||||
|
re.compile(r"\bHaftungs(?:ausschluss|beschr(?:ä|ae)nkung|"
|
||||||
|
r"hinweis|begrenzung)",
|
||||||
|
re.IGNORECASE),
|
||||||
|
re.compile(r"\bDisclaimer\b", re.IGNORECASE),
|
||||||
|
),
|
||||||
|
category="recommended_separate",
|
||||||
|
recommended_location="Footer-Reiter 'Legal' / Seite 'Disclaimer'",
|
||||||
|
norm="BGB — Risikoabgrenzung gehört nicht ins § 5-TMG-Impressum",
|
||||||
|
),
|
||||||
|
SectionPattern(
|
||||||
|
section_id="nutzungsbedingungen",
|
||||||
|
label="Nutzungsbedingungen",
|
||||||
|
heading_patterns=(
|
||||||
|
re.compile(r"\bNutzungsbedingungen\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\bTerms\s+of\s+Use\b", re.IGNORECASE),
|
||||||
|
),
|
||||||
|
category="recommended_separate",
|
||||||
|
recommended_location="Eigene Seite 'Nutzungsbedingungen' im Footer",
|
||||||
|
norm="§§ 305 ff. BGB — eigene AGB-Seite üblich",
|
||||||
|
),
|
||||||
|
SectionPattern(
|
||||||
|
section_id="weee_elektrog",
|
||||||
|
label="ElektroG / WEEE-Reg-Nr.",
|
||||||
|
heading_patterns=(
|
||||||
|
re.compile(
|
||||||
|
r"(?:Elektro-?\s*und\s*Elektronikger(?:ä|ae)tegesetz|"
|
||||||
|
r"\bElektroG\b|\bWEEE\b|"
|
||||||
|
r"WEEE[\-\s]?Reg)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
category="product_compliance",
|
||||||
|
recommended_location=(
|
||||||
|
"Eigene Seite 'Produktinformationen' oder Hinweis auf "
|
||||||
|
"Produkt-/Recycling-Seite"
|
||||||
|
),
|
||||||
|
norm="§ 6 Abs. 1 ElektroG — Pflichtkennzeichnung am Produkt, "
|
||||||
|
"nicht zwingend im Impressum",
|
||||||
|
),
|
||||||
|
SectionPattern(
|
||||||
|
section_id="verpackg",
|
||||||
|
label="VerpackG (Verpackungsregister)",
|
||||||
|
heading_patterns=(
|
||||||
|
re.compile(
|
||||||
|
r"(?:Verpackungsgesetz|\bVerpackG\b|"
|
||||||
|
r"LUCID[\-\s]?Reg|Verpackungs-?Reg)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
category="product_compliance",
|
||||||
|
recommended_location="Eigene Seite 'Produktinformationen'",
|
||||||
|
norm="§ 7 VerpackG — Registrierung beim ZSVR",
|
||||||
|
),
|
||||||
|
SectionPattern(
|
||||||
|
section_id="battg",
|
||||||
|
label="BattG (Batteriegesetz)",
|
||||||
|
heading_patterns=(
|
||||||
|
re.compile(
|
||||||
|
r"(?:Batteriegesetz|\bBattG\b|"
|
||||||
|
r"GRS-?Registrierung)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
category="product_compliance",
|
||||||
|
recommended_location="Eigene Seite 'Batterieinformationen'",
|
||||||
|
norm="§ 4 BattG — Rücknahmepflicht",
|
||||||
|
),
|
||||||
|
SectionPattern(
|
||||||
|
section_id="aenderungsvorbehalt",
|
||||||
|
label="Änderungsvorbehalt",
|
||||||
|
heading_patterns=(
|
||||||
|
re.compile(r"(?:Aenderungs|Änderungs)vorbehalt",
|
||||||
|
re.IGNORECASE),
|
||||||
|
re.compile(r"\bbeh(?:ä|ae)lt\s+sich\s+vor", re.IGNORECASE),
|
||||||
|
),
|
||||||
|
category="recommended_separate",
|
||||||
|
recommended_location="In den Nutzungsbedingungen / Footer 'Legal'",
|
||||||
|
norm="BGB-Anrede — gehört in AGB nicht ins Impressum",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_sections(text: str) -> list[tuple[SectionPattern, str]]:
|
||||||
|
"""Sucht im Text nach Sektions-Headings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Liste (SectionPattern, evidence_snippet) — pro gefundener Sektion.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
hits: list[tuple[SectionPattern, str]] = []
|
||||||
|
for sec in SECTIONS:
|
||||||
|
for pat in sec.heading_patterns:
|
||||||
|
m = pat.search(text)
|
||||||
|
if m:
|
||||||
|
snippet = text[max(0, m.start() - 20):m.end() + 80]
|
||||||
|
snippet = " ".join(snippet.split())[:120]
|
||||||
|
hits.append((sec, snippet))
|
||||||
|
break # eines reicht pro Sektion
|
||||||
|
return hits
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
"""Tests für den Cross-Placement-Agent (deplatzierter Content)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from compliance.services.specialist_agents import (
|
||||||
|
REGISTRY,
|
||||||
|
AgentInput,
|
||||||
|
CrossPlacementAgent,
|
||||||
|
Severity,
|
||||||
|
)
|
||||||
|
from tests.fixtures.impressum_groundtruth import (
|
||||||
|
ETO, HECTRONIC, SAFETYKON, BMW,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(coro):
|
||||||
|
return asyncio.get_event_loop().run_until_complete(coro)
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_is_registered():
|
||||||
|
agent = REGISTRY.get("cross_placement")
|
||||||
|
assert agent is not None
|
||||||
|
assert agent.doc_type == "impressum"
|
||||||
|
|
||||||
|
|
||||||
|
def test_short_text_skipped():
|
||||||
|
agent = CrossPlacementAgent()
|
||||||
|
out = _run(agent.evaluate(AgentInput(doc_type="impressum", text="x")))
|
||||||
|
assert all(c.status == "skipped" for c in out.mc_coverage)
|
||||||
|
|
||||||
|
|
||||||
|
def test_eto_finds_nutzungsbedingungen_and_haftung():
|
||||||
|
"""ETO-Impressum hat Nutzungsbedingungen + Haftungsbeschränkung —
|
||||||
|
Cross-Placement-Agent muss das melden."""
|
||||||
|
agent = CrossPlacementAgent()
|
||||||
|
out = _run(agent.evaluate(AgentInput(
|
||||||
|
doc_type="impressum", text=ETO.text,
|
||||||
|
)))
|
||||||
|
field_ids = {f.field_id for f in out.findings}
|
||||||
|
# ETO-Impressum hat keine Sektionen wie Copyright/Disclaimer →
|
||||||
|
# nur die Pflichtangaben. Test prüft dass ETO sauber ist.
|
||||||
|
# User-GT: placement_concerns = () → 0 Findings erwartet
|
||||||
|
assert not out.findings, (
|
||||||
|
f"ETO sollte keine Placement-Findings haben, hat aber: "
|
||||||
|
f"{field_ids}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_safetykon_finds_urheberrecht_and_haftung():
|
||||||
|
"""Safetykon hat Urheberrecht + Bilder/Lizenzen + Haftungsausschluss
|
||||||
|
im Impressum-Block — alle drei sollen gemeldet werden."""
|
||||||
|
agent = CrossPlacementAgent()
|
||||||
|
out = _run(agent.evaluate(AgentInput(
|
||||||
|
doc_type="impressum", text=SAFETYKON.text,
|
||||||
|
)))
|
||||||
|
field_ids = {f.field_id for f in out.findings}
|
||||||
|
assert "urheberrecht" in field_ids
|
||||||
|
assert "bilder_lizenzen" in field_ids
|
||||||
|
assert "haftungsausschluss" in field_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_hectronic_finds_weee_copyright_haftung():
|
||||||
|
"""Hectronic hat WEEE-Reg + Copyright + Haftungsausschluss im
|
||||||
|
Impressum-Block."""
|
||||||
|
agent = CrossPlacementAgent()
|
||||||
|
out = _run(agent.evaluate(AgentInput(
|
||||||
|
doc_type="impressum", text=HECTRONIC.text,
|
||||||
|
)))
|
||||||
|
field_ids = {f.field_id for f in out.findings}
|
||||||
|
assert "weee_elektrog" in field_ids
|
||||||
|
assert "urheberrecht" in field_ids # 'Copyright' heading
|
||||||
|
assert "haftungsausschluss" in field_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_bmw_is_clean():
|
||||||
|
"""BMW-Impressum ist sauber (nur Pflichtangaben) — 0 Findings."""
|
||||||
|
agent = CrossPlacementAgent()
|
||||||
|
out = _run(agent.evaluate(AgentInput(
|
||||||
|
doc_type="impressum", text=BMW.text,
|
||||||
|
)))
|
||||||
|
assert not out.findings, (
|
||||||
|
f"BMW sollte sauber sein, hat aber: "
|
||||||
|
f"{[f.field_id for f in out.findings]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_recommendation_mentions_legal_footer():
|
||||||
|
"""Best-Practice-Empfehlung muss 'Legal'-Footer-Tab vorschlagen."""
|
||||||
|
agent = CrossPlacementAgent()
|
||||||
|
out = _run(agent.evaluate(AgentInput(
|
||||||
|
doc_type="impressum", text=HECTRONIC.text,
|
||||||
|
)))
|
||||||
|
assert out.findings
|
||||||
|
sample = out.findings[0]
|
||||||
|
assert "Legal" in sample.action
|
||||||
|
assert "Footer" in sample.action
|
||||||
|
|
||||||
|
|
||||||
|
def test_weee_is_product_compliance_medium():
|
||||||
|
"""ElektroG/WEEE ist eine echte Pflicht aber Ort umstritten →
|
||||||
|
MEDIUM statt LOW."""
|
||||||
|
agent = CrossPlacementAgent()
|
||||||
|
out = _run(agent.evaluate(AgentInput(
|
||||||
|
doc_type="impressum", text=HECTRONIC.text,
|
||||||
|
)))
|
||||||
|
weee = next(f for f in out.findings if f.field_id == "weee_elektrog")
|
||||||
|
assert weee.severity == Severity.MEDIUM.value
|
||||||
|
|
||||||
|
|
||||||
|
def test_copyright_is_low():
|
||||||
|
"""Copyright/Urheberrecht ist BPa nicht zwingend Impressum-Pflicht →
|
||||||
|
LOW."""
|
||||||
|
agent = CrossPlacementAgent()
|
||||||
|
out = _run(agent.evaluate(AgentInput(
|
||||||
|
doc_type="impressum", text=SAFETYKON.text,
|
||||||
|
)))
|
||||||
|
cp = next(f for f in out.findings if f.field_id == "urheberrecht")
|
||||||
|
assert cp.severity == Severity.LOW.value
|
||||||
Reference in New Issue
Block a user