"""Single source of truth for MAS content relevance decisions."""
from __future__ import annotations
from dataclasses import dataclass
import re, unicodedata
try:
from content_safety import find_safety_block
except ImportError:
find_safety_block = None
ANCHORS = (
"lüftung", "lueftung", "dezentrale lüftung", "luftwechsel", "ventilation",
"feuchte", "feuchtigkeit", "feuchtemessung", "feuchteschaden", "moisture", "humedad",
"schimmel", "mold", "moho", "kondensation", "condensation", "taupunkt",
"raumluftqualität", "raumluftqualitaet", "luftqualität", "luftqualitaet", "air quality",
"co2", "voc", "sensorik", "sensor", "monitoring", "gebäudediagnostik", "gebaeudediagnostik",
"lüftungsplanung", "lueftungsplanung", "gebäudetechnik", "gebaeudetechnik", "innenraumklima",
"innenraum", "überströmung", "ueberstroemung", "bauliche feuchte", "wasserschaden",
"immobilienbetrieb", "gebäudebetrieb", "gebaeudebetrieb", "raumklima",
)
FOOD_CONTEXT = ("schimmel-essen", "schimmelemmahlzeit", "gammelfleisch", "schinken", "fleisch", "lebensmittel", "comida", "carne", "jamon", "queso")
TV_CONTEXT = ("goodbye deutschland", "reality show", "fernsehbericht", "tv-bericht", "reportage vox")
SPORT_CONTEXT = ("fußball", "fussball", "american football", "basketball", "tennis", "sport", "liga", "champions league")
@dataclass(frozen=True)
class MASRelevance:
relevant: bool
anchor_matches: list[str]
content_blocked: bool
block_reason: str
reason: str
def _fold(value: object) -> str:
text = unicodedata.normalize("NFKD", str(value or "").casefold())
text = "".join(c for c in text if not unicodedata.combining(c))
return " ".join(re.sub(r"[^a-z0-9]+", " ", text).split())
def assess_linkedin_fit(text: str, *, mas_relevant: bool = True) -> tuple[int, str]:
"""Bewertet die fachliche Aufbereitbarkeit für LinkedIn, nicht die Quelle.
Nur topic/summary-Inhalt wird bewertet. source_platform/source_type und
Provenienz geben keine Punkte und sperren keinen fachlich tragfähigen Beitrag.
1=ungeeignet, 2=prüfenswert, 3=klar fachlich aufbereitbar.
"""
if not mas_relevant:
return 1, "Kein fachlicher MAS-Anker"
folded = _fold(text)
# Basisthemen belegen das technische Problem; auch DE/ES/EN-Varianten.
technical = (
"diagnost", "peritaje", "medim", "medir", "parametro", "capilar", "capilaridad",
"humedad estructural", "ventilacion", "ventilación", "luftwechsel", "feuchtemess",
"co2", "voc", "sensor", "monitoring", "kondens", "condensacion", "schimmel", "feucht",
"humedad", "moho", "mold", "mould", "lueften", "luften", "aire", "circul", "renueva",
"keller", "fenster", "luftentfeuchter", "heizen", "daemmen", "dämmen",
"ursache", "causa", "mess", "mauerwerk", "muro", "wand", "gebaeude",
"gebäudehülle", "luftqualität", "raumluft", "innenraumklima", "calidad del aire",
"lüftungsplanung", "dezentrale lüftung", "auslegung", "luftmenge",
)
professional = (
"ingenier", "tecnic", "fach", "architekt", "administrador", "property",
"inmobili", "hausverwaltung", "planung", "planificación", "installateur", "perito",
"bauphys", "eigentümer", "eigentuemer", "verwalter", "fachpartner",
)
generic = ("urlaub", "tourismus", "tourist", "influencer", "lifestyle", "promi", "sport", "fussball", "fußball", "marmelade", "lebensmittel", "fleisch", "essen")
tech_hits = sum(1 for term in technical if term in folded)
prof_hits = sum(1 for term in professional if term in folded)
generic_hit = any(term in folded for term in generic)
if generic_hit and tech_hits < 2:
return 1, "MAS-Anker ohne belastbaren fachlichen Analysewert"
if tech_hits >= 3 or (tech_hits >= 2 and prof_hits >= 1):
return 3, "Konkrete technische Ursachen-/Mess-/Planungsfrage; für Eigentümer, Verwalter und Fachpartner fachlich aufbereitbar"
if tech_hits >= 2:
return 2, "Echter MAS-Fachinhalt (Feuchte/Schimmel/Lüftungszusammenhang) mit erkennbarem Erklärungswert; eigenständige LinkedIn-Aufbereitung prüfenswert"
if tech_hits >= 1:
return 2, "Echter MAS-Fachinhalt mit erkennbarem Erklärungswert; eigenständige LinkedIn-Aufbereitung prüfenswert"
return 1, "MAS-Anker vorhanden, aber kein ausreichender fachlicher LinkedIn-Erkenntniswert"
def assess_mas_relevance(text: str, *, fields: dict | None = None) -> MASRelevance:
raw_fields = fields or {}
content_parts = [text, raw_fields.get("topic", ""), raw_fields.get("short_summary", "")]
corpus = " ".join(str(v or "") for v in content_parts)
folded = _fold(corpus)
matches=[]
for anchor in ANCHORS:
if anchor not in matches and re.search(rf"(?<![a-z0-9]){re.escape(_fold(anchor))}(?![a-z0-9])", folded):
matches.append(anchor)
safety = find_safety_block(fields or {"topic": text}) if find_safety_block else None
if any(term in folded for term in FOOD_CONTEXT + TV_CONTEXT + SPORT_CONTEXT) and not any(term in folded for term in ("wand", "wohnung", "haus", "immobil", "raum", "gebäude", "gebaeude", "lüft", "lueft", "feuchte", "kondens")):
return MASRelevance(False, [], False, "", "Kein fachlicher MAS-Anker")
if safety:
kind, term = safety
return MASRelevance(False, matches, True, f"sensibles Ereignis ({kind}: {term})", "Kein MAS-Content: sensibles Ereignis")
if not matches:
return MASRelevance(False, [], False, "", "Kein fachlicher MAS-Anker")
return MASRelevance(True, matches, False, "", "Fachlicher MAS-Anker vorhanden: " + ", ".join(matches[:6]))