feat(agent): Impressum-Tab auf Haupt-Engine + Profil/§36-Fixes
Ergebnis-Tab rendert jetzt result.results (Haupt-Doc-Check) statt des abweichenden v3-Agenten — BMW korrekt statt False Positives: - DocResultView: ein Dokument als Pflichtangaben-Tabelle (Label + gefundener Text + 3-Tier-Status), KEINE MC-IDs. ComplianceResultTabs speist Tabs aus result.results; ChecklistView-Bausteine exportiert + wiederverwendet. - profile_extractor: Firmenname/Rechtsform = fruehester Treffer + ausge- schriebene Formen (Aktiengesellschaft) -> BMW AG statt "juris GmbH". - 36 VSBG (MC-010): reines b2c -> POSSIBLY_APPLICABLE (Pruef-Hinweis) statt MEDIUM-FAIL; hart nur bei ecommerce. possibly_hint pro MC. - McCoverage traegt label + found (Snippet); mc_possibly-Aggregat. - AgentFindingCard/Methodik: interne check_id/mc_id nicht mehr angezeigt. Tests: test_four_status (16) + Frontend-Vitest gruen; CI-Suite 206, v3/GT unveraendert. Nur eigene Dateien (geteilter Tree). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,11 @@ class McCoverage(BaseModel):
|
||||
mc_id: str
|
||||
status: str
|
||||
reason: str = ""
|
||||
# Menschlicher Feldname (für die Pflichtangaben-Tabelle im Frontend —
|
||||
# NICHT die mc_id zeigen, sonst Reverse-Engineering der MC-Bibliothek).
|
||||
label: str = ""
|
||||
# Der tatsächlich gefundene Text/Wert (Snippet) bei status=ok.
|
||||
found: str = ""
|
||||
|
||||
|
||||
class EscalationLog(BaseModel):
|
||||
|
||||
@@ -78,6 +78,25 @@ def _build_measure(label: str, norm: str) -> str:
|
||||
return msg
|
||||
|
||||
|
||||
def _line_of(text: str, start_pos: int, end_pos: int) -> str:
|
||||
"""Die Zeile um einen Regex-Treffer — als 'gefundener Wert' für die
|
||||
Pflichtangaben-Tabelle. Gekappt + bereinigt."""
|
||||
start = text.rfind("\n", 0, start_pos) + 1
|
||||
end = text.find("\n", end_pos)
|
||||
if end == -1:
|
||||
end = len(text)
|
||||
return " ".join(text[start:end].split())[:160]
|
||||
|
||||
|
||||
def _coverage(mc, status: str, reason: str, found: str = "") -> McCoverage:
|
||||
"""McCoverage mit menschlichem Label (mc.label) — das Frontend zeigt
|
||||
NIE die mc_id (Reverse-Engineering-Schutz)."""
|
||||
return McCoverage(
|
||||
mc_id=mc.mc_id, status=status, reason=reason,
|
||||
label=mc.label, found=found,
|
||||
)
|
||||
|
||||
|
||||
class ImpressumAgent(BaseSpecialistAgent):
|
||||
agent_id = "impressum"
|
||||
agent_version = "3.0"
|
||||
@@ -103,10 +122,7 @@ class ImpressumAgent(BaseSpecialistAgent):
|
||||
if len(text) < 100:
|
||||
# Doc zu kurz — alle eigenen Pattern-IDs als skipped
|
||||
for mc in MCS:
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id, status="skipped",
|
||||
reason="text too short",
|
||||
))
|
||||
coverage.append(_coverage(mc, "skipped", "text too short"))
|
||||
return self._finalize(
|
||||
start, findings, esc_logs, coverage,
|
||||
confidence=0.0,
|
||||
@@ -129,27 +145,35 @@ class ImpressumAgent(BaseSpecialistAgent):
|
||||
for mc in MCS:
|
||||
disp = scope_disposition(mc, scope, is_auto)
|
||||
if disp == "na":
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id, status="na",
|
||||
reason="nicht anwendbar (Rechtsform/Branche)",
|
||||
))
|
||||
coverage.append(_coverage(
|
||||
mc, "na", "nicht anwendbar (Rechtsform/Branche)"))
|
||||
continue
|
||||
if any(p.search(text) for p in mc.patterns):
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id, status="ok", reason="Pattern-Treffer",
|
||||
matched = None
|
||||
for p in mc.patterns:
|
||||
m = p.search(text)
|
||||
if m:
|
||||
matched = m
|
||||
break
|
||||
if matched is not None:
|
||||
coverage.append(_coverage(
|
||||
mc, "ok", "Pattern-Treffer",
|
||||
found=_line_of(text, matched.start(), matched.end()),
|
||||
))
|
||||
continue
|
||||
if mc.optional:
|
||||
# fehlt + optional → KEIN Finding (z.B. USt-IdNr;
|
||||
# Kleinunternehmer §19 haben legitim keine).
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id, status="na",
|
||||
reason="optional — nicht angegeben",
|
||||
))
|
||||
coverage.append(_coverage(
|
||||
mc, "na", "optional — nicht angegeben"))
|
||||
continue
|
||||
if disp == "possible":
|
||||
# Graubereich (z.B. Corporate-Blog → §18 MStV evtl.) →
|
||||
# POSSIBLY_APPLICABLE: Pruef-Hinweis (LOW), kein Verstoss.
|
||||
# Graubereich (z.B. Corporate-Blog → §18, OEM-Markenseite →
|
||||
# §36 VSBG) → POSSIBLY_APPLICABLE: Pruef-Hinweis (LOW),
|
||||
# kein Verstoss. Hinweistext kommt MC-spezifisch.
|
||||
hint = mc.possibly_hint or (
|
||||
f"Diese Angabe ist nur situativ Pflicht ({mc.norm}). "
|
||||
"Bitte prüfen, ob sie auf Ihre Seite zutrifft."
|
||||
)
|
||||
findings.append(Finding(
|
||||
check_id=f"IMP-{mc.field_id}",
|
||||
agent=self.agent_id,
|
||||
@@ -161,12 +185,7 @@ class ImpressumAgent(BaseSpecialistAgent):
|
||||
title=f"{mc.label}: ggf. relevant — manuell prüfen",
|
||||
norm=mc.norm,
|
||||
evidence="",
|
||||
action=(
|
||||
"Bei journalistisch-redaktionellen Inhalten "
|
||||
"(Nachrichten/Magazin) ist ein Verantwortlicher nach "
|
||||
"§ 18 MStV anzugeben. Bei reinem Corporate-Blog meist "
|
||||
"nicht erforderlich — bitte prüfen."
|
||||
),
|
||||
action=hint,
|
||||
confidence=0.5,
|
||||
sources=[EvidenceSource(
|
||||
source_type=SourceType.REGEX,
|
||||
@@ -175,10 +194,8 @@ class ImpressumAgent(BaseSpecialistAgent):
|
||||
confidence=0.5,
|
||||
)],
|
||||
))
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id, status="possibly_applicable",
|
||||
reason="Graubereich — manuelle Prüfung",
|
||||
))
|
||||
coverage.append(_coverage(
|
||||
mc, "possibly_applicable", "Graubereich — manuelle Prüfung"))
|
||||
continue
|
||||
if mc.legal_form_dependent and not form_known:
|
||||
# Rechtsform unbestimmt → kein hartes FAIL, sondern
|
||||
@@ -207,10 +224,8 @@ class ImpressumAgent(BaseSpecialistAgent):
|
||||
confidence=0.4,
|
||||
)],
|
||||
))
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id, status="insufficient_evidence",
|
||||
reason="Rechtsform unbestimmt",
|
||||
))
|
||||
coverage.append(_coverage(
|
||||
mc, "insufficient_evidence", "Rechtsform unbestimmt"))
|
||||
continue
|
||||
sev = _SEV_TO_ENUM.get(mc.severity_if_missing, Severity.MEDIUM)
|
||||
findings.append(Finding(
|
||||
@@ -233,10 +248,8 @@ class ImpressumAgent(BaseSpecialistAgent):
|
||||
confidence=0.9,
|
||||
)],
|
||||
))
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id, status=sev.value.lower(),
|
||||
reason="kein Pattern-Treffer",
|
||||
))
|
||||
coverage.append(_coverage(
|
||||
mc, sev.value.lower(), "kein Pattern-Treffer"))
|
||||
n_fail = sum(1 for f in findings
|
||||
if f.status == CheckStatus.FAIL.value)
|
||||
n_unklar = sum(1 for f in findings
|
||||
|
||||
@@ -40,6 +40,9 @@ class MC:
|
||||
# ist die MC NICHT hart anwendbar, sondern POSSIBLY_APPLICABLE — Pruef-
|
||||
# Hinweis (severity LOW) statt FAIL. Z.B. Corporate-Blog (§18 MStV evtl.).
|
||||
possibly_applies_scope: tuple[str, ...] = field(default_factory=tuple)
|
||||
# MC-spezifischer Pruef-Hinweis fuer den POSSIBLY_APPLICABLE-Fall
|
||||
# (warum Graubereich + was der Nutzer pruefen soll).
|
||||
possibly_hint: str = ""
|
||||
|
||||
|
||||
MCS: tuple[MC, ...] = (
|
||||
@@ -182,6 +185,11 @@ MCS: tuple[MC, ...] = (
|
||||
severity_if_missing="MEDIUM",
|
||||
requires_scope=("editorial",),
|
||||
possibly_applies_scope=("editorial_possible",),
|
||||
possibly_hint=(
|
||||
"Bei journalistisch-redaktionellen Inhalten (Nachrichten/Magazin) "
|
||||
"ist ein Verantwortlicher nach § 18 MStV anzugeben. Bei reinem "
|
||||
"Corporate-Blog meist nicht erforderlich — bitte prüfen."
|
||||
),
|
||||
patterns=(re.compile(
|
||||
r"(?:Verantwortlich(?:er|e)?\s+(?:f(?:ue|ü)r|i\.S\.d\.|"
|
||||
r"nach|gem(?:ae|ä)ß)\s+§\s*18|"
|
||||
@@ -194,9 +202,17 @@ MCS: tuple[MC, ...] = (
|
||||
mc_id="IMP-MC-010",
|
||||
field_id="verbraucher_streitbeilegung",
|
||||
label="Verbraucher-Streitbeilegung-Hinweis",
|
||||
norm="§ 36 VSBG (B2C-Anbieter Pflicht)",
|
||||
norm="§ 36 VSBG (Verbraucherverträge über die Website)",
|
||||
severity_if_missing="MEDIUM",
|
||||
requires_scope=("ecommerce", "b2c"),
|
||||
# Hart nur bei echtem Online-Verkauf; reine B2C-Orientierung (z.B.
|
||||
# OEM-Markenseite, Verkauf über Händler) = Graubereich → Prüf-Hinweis.
|
||||
requires_scope=("ecommerce",),
|
||||
possibly_applies_scope=("b2c",),
|
||||
possibly_hint=(
|
||||
"§ 36 VSBG gilt, wenn auf dieser Seite Verbraucherverträge "
|
||||
"geschlossen werden. Bei reiner Marken-/Info-Seite (Verkauf über "
|
||||
"Händler/Vertragspartner) meist nicht erforderlich — bitte prüfen."
|
||||
),
|
||||
patterns=(re.compile(
|
||||
r"(?:Verbraucherschlichtungs|VSBG|"
|
||||
r"Streitbeilegung|"
|
||||
|
||||
Reference in New Issue
Block a user