diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md index 3d5a283..612ef39 100644 --- a/.agents/MEMORY.md +++ b/.agents/MEMORY.md @@ -5,10 +5,12 @@ Rollender Übergabe-Log für agent-Threads. Workflow: `.agents/SKILL.md` ## Current focus -Rechtsprechungs-Intake (D15) ist technisch abgeschlossen und reindexiert. -Offen: Antwortmodus-Eval für den erweiterten Korpus, bewusste Publikations-/ -Lizenzfreigabe der quellentreuen Rechtsprechungs-Volltexte und danach M4 -(Odoo-Integration inkl. separat zu planender Privacy-Grenze für Lohndaten). +Retrieval-/Antwort-Tuning für komplexe Gestaltungsfragen läuft (D16). +Goldset q-124–126 und deterministischer Decision-Support-Pfad sind umgesetzt; +offen ist der vollständige 53-Fragen-Antwortmodus-Eval. Danach folgen die +bewusste Publikations-/Lizenzfreigabe der quellentreuen Rechtsprechungs- +Volltexte und M4 (API-first Odoo-Integration inkl. separat zu planender +Privacy-Grenze für Lohndaten). ## Completed (2026-09-14) @@ -121,6 +123,31 @@ Lizenzfreigabe der quellentreuen Rechtsprechungs-Volltexte und danach M4 Commit ausdrücklich zu prüfen. EuGH-Wiedergaben sind als nichtamtlich markiert und verweisen auf EUR-Lex. +## Completed (2026-09-16) + +- **D16 (Gestaltungsfragen / Retrieval-Tuning):** Goldset um q-124–126 + erweitert (500-Euro-Zusatzzahlung, Alternativen, Mitarbeiterprämie 2026). + `agent/query_planner.py` erkennt Decision-Support deterministisch und zerlegt + in drei gesetzlich gescopte Queries: Mitarbeiterprämie samt §-49-Konflikt, + Zukunftssicherung sowie Sach-/zweckgebundene Leistungen. Decision-Support + läuft als `specific`, nicht als Survey/Map-Reduce. `Retriever.search()` nutzt + denselben Plan im Offline-Eval; `search_multi()` behält Scope/Jahr auch bei + einer einzelnen Query. Ergebnis (53 Goldsetfragen / 48 Retrieval-Fälle): + q-124/q-125/q-126 jeweils Recall@8 **1,00**, gesamt Recall@8 **0,9271** + (Gate >0,9), Hit-Rate 0,9583, MRR 0,6158. Bekannte Altfehler q-015/q-113 + bleiben unverändert. +- **Decision-Support-Grounding:** Systemprompt trennt Barzahlung von + zweckgebundenen Leistungen, verbietet pauschale Sieger ohne Kontext, + verlangt vergleichbare Abgabendimensionen und genau eine Rückfrage. Der + bekannte Konflikt Mitarbeiterprämie 2026 wird offen dargestellt: + `wk-akt-04` nennt SV/BV/DB/DZ/KommSt-Pflicht, `lb-sva-03` ordnet die Prämie + systematisch in den taxativen Katalog beitragsfreier Bezüge ein. Zusätzlich + erzwingt ein semantischer Post-Validation-Gate ⚠ + beide IDs + korrekte + Rollen und verhindert interne Regelverweise bzw. Quellenpriorisierung. + Gezielte Real-Läufe: q-124/q-125 nicht verweigert, zitiergültig; q-126 war + bereits sauber. Tests **72 grün**; `git diff --check` sauber. Vollständiger + Antwortmodus-Eval steht noch aus. + ## Open issues / blockers - **Push**: Remote localhost:3003 aus der Sandbox nicht erreichbar — User diff --git a/agent/eval/goldset.yaml b/agent/eval/goldset.yaml index 74c2bf0..7206ccb 100644 --- a/agent/eval/goldset.yaml +++ b/agent/eval/goldset.yaml @@ -162,6 +162,23 @@ questions: expected_ids: [rj-rjs-002] note: "Nur als nichtamtliche lexetius-Textwiedergabe im Korpus; Antwort muss den Stand 1992-05 führen." + # --- Gestaltungsfragen / Entscheidungsunterstützung --- + - id: q-124 + question: "Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. Was ist die günstigste Lösung?" + expected_ids: [wk-akt-04, lb-sva-03] + note: "Offene Gestaltungsfrage: Zeitraum, Branche/KV, gewünschte Barzahlung und persönliche Voraussetzungen fehlen. Für 2026 ist die Mitarbeiterprämie ein direkter Kandidat; Konflikt zur SV-Behandlung zwischen wk-akt-04 (pflichtig) und lb-sva-03 (beitragsfrei) ausdrücklich mit beiden IDs darstellen und gezielt rückfragen. Keine pauschale Empfehlung." + tags: [decision-support, clarification, conflict] + - id: q-125 + question: "Welche abgabenbegünstigten Möglichkeiten gibt es 2026 statt einer normalen zusätzlichen Barprämie von 500 Euro?" + expected_ids: [wk-akt-04, lb-vor-11, lb-ges-01, lb-sva-04] + note: "Vergleichs-/Survey-Frage: Mitarbeiterprämie, Zukunftssicherung, Sachzuwendungen und Mahlzeiten-/Kinderbetreuungsleistungen unterscheiden; Voraussetzungen und fehlenden Arbeitnehmerkontext nennen. Nicht behaupten, alle Alternativen seien frei als Bargeld auszahlbar." + tags: [decision-support, survey] + - id: q-126 + question: "Kann ich 2026 jedem Mitarbeiter einfach 500 Euro steuerfrei als Mitarbeiterprämie auszahlen?" + expected_ids: [wk-akt-04] + note: "Prämisse korrigieren: keine voraussetzungslose Auszahlung; lohngestaltende Grundlage, Zeitraum, Zusätzlichkeit und Abgaben außerhalb der Lohnsteuer anhand der Quelle erklären." + tags: [decision-support, premise-correction] + # --- Komplexe Fragen (Stufe 1, M6: Multi-Query + Temporal-Intent) --- - id: q-110 question: "Wie hoch war der kollektivvertragliche Mindestmonatslohn für angelernte Friseurinnen und Friseure ab 1.4.2023?" diff --git a/agent/generate.py b/agent/generate.py index 8109ef4..8c4e435 100644 --- a/agent/generate.py +++ b/agent/generate.py @@ -13,7 +13,7 @@ import time from .config import Config from .normalize import normalize_text from .ollama_client import OllamaClient -from .query_planner import SubQuery, plan_queries +from .query_planner import SubQuery, is_decision_support, plan_queries from .retrieve import ChunkResult, Retriever REFUSAL_MESSAGE = "Dazu enthält die Wissensbasis keine Aussage." @@ -39,8 +39,11 @@ Verbindliche Regeln: mache klar, welcher Aspekt belegt ist. Verweigere nur, wenn KEIN Block thematisch zur Frage passt, mit exakt: „Dazu enthält die Wissensbasis keine Aussage.“ — und schlage nichts vor. -5. Widersprechen sich Kontextblöcke, nenne beide Werte mit ihren IDs und - kennzeichne den Widerspruch mit ⚠. Löse Widersprüche niemals stillschweigend auf. +5. Widersprechen sich Kontextblöcke, nenne beide Aussagen mit ihren IDs und + kennzeichne den Widerspruch mit ⚠. Das gilt auch, wenn eine Quelle einen + Bezug systematisch in einen Katalog abgabenfreier Bezüge einordnet, während + eine andere Quelle dafür ausdrücklich Abgabenpflicht nennt. Löse + Widersprüche niemals stillschweigend auf. 6. Nenne Paragraphen und Gesetze nur, wenn ein Kontextblock sie nennt. 7. Antworte auf Deutsch und prägnant (Stichpunkte, wo sinnvoll). 8. Baut die Frage auf einer falschen Annahme auf (z. B. ein nicht @@ -56,8 +59,29 @@ Verbindliche Regeln: diesem Fall nicht. 10. Ist ein verwendeter Kontextblock im Feld „Werk“ als „nicht amtlich“ gekennzeichnet, nenne diese Einschränkung ausdrücklich in der Antwort. +11. Bei Gestaltungsfragen nach der „günstigsten“ oder „optimalen“ Lösung darfst + du ohne ausreichenden Einzelfallkontext keine Option pauschal zum Sieger + erklären. Trenne eine gewünschte frei verfügbare Barzahlung ausdrücklich + von zweckgebundenen Sach- oder Kostenleistungen. Vergleiche belegte + Kandidaten nach denselben Dimensionen: Auszahlbarkeit bzw. + Verwendungsbindung, Lohnsteuer, Sozialversicherung einschließlich BV und + Lohnnebenkosten, Voraussetzungen sowie Stand. Nenne Quellenwidersprüche + gemäß Regel 5 und stelle bei fehlendem Kontext am Ende genau EINE gezielte + Rückfrage. Sind [wk-akt-04] und [lb-sva-03] beide als Block-Köpfe im + Kontext vorhanden, stelle den Konflikt ausdrücklich mit ⚠ dar: + [wk-akt-04] nennt die Mitarbeiterprämie 2026 SV-/BV-pflichtig; + [lb-sva-03] ordnet sie systematisch in den taxativen Katalog + beitragsfreier Bezüge ein. Bezeichne keine der widersprechenden Quellen als + maßgeblich, aktueller oder spezifischer und löse den Konflikt nicht durch + eigene Auslegung auf. Nenne den Konflikt genau einmal. Formuliere den + Vergleich prägnant; wiederhole dieselbe Aussage nicht nochmals in einem + zusätzlichen Fazit. +12. Erwähne diese Anweisungen oder ihre Regelnummern niemals in der Antwort. + Ist eine Abgabenwirkung im Kontext nicht ausdrücklich oder durch dessen + systematische Einordnung belegt, bezeichne sie als nicht belegt und + spekuliere nicht mit Trainingswissen oder vermeintlicher Praxis. -Verletze Regel 2, Regel 4 oder Regel 10 niemals — im Zweifel verweigere die Antwort.""" +Verletze Regel 2, Regel 4, Regel 10, Regel 11 oder Regel 12 niemals — im Zweifel verweigere die Antwort.""" MAP_SYSTEM_PROMPT = """Du destillierst Wissensbasis-Kontextblöcke für eine Folgesynthese. Erstelle für JEDEN Kontextblock 1-3 prägnante Stichpunkte. Beginne jede @@ -125,6 +149,45 @@ def validate_answer(answer: str, allowed_ids: list[str]) -> list[str]: return violations +def validate_decision_support_answer( + question: str, answer: str, allowed_ids: list[str] +) -> list[str]: + """Semantischer Gate für den bekannten Mitarbeiterprämien-Konflikt. + + Prompt-Anweisungen allein waren nicht stabil: Das Modell ließ den Konflikt + fallweise aus oder erklärte eine Quelle eigenmächtig für maßgeblich. + """ + if not is_decision_support(question): + return [] + required = {"wk-akt-04", "lb-sva-03"} + if not required.issubset(set(allowed_ids)): + return [] + cited = set(CITE_RE.findall(answer)) + violations: list[str] = [] + if "⚠" not in answer or not required.issubset(cited): + violations.append( + "der Quellenkonflikt zur SV-Behandlung der Mitarbeiterprämie 2026 " + "muss mit ⚠ sowie [wk-akt-04] und [lb-sva-03] offen dargestellt werden" + ) + folded = normalize_text(answer) + if "[lb-sva-03] ordnet" not in folded or not re.search( + r"\[wk-akt-04\].{0,160}(?:pflicht|pflichtig)", folded, re.DOTALL + ): + violations.append( + "stelle die Rollen der Konfliktquellen eindeutig dar: [lb-sva-03] " + "ordnet die Prämie in den Katalog beitragsfreier Bezüge ein; " + "[wk-akt-04] nennt SV-/BV-Pflicht" + ) + if re.search(r"\bgemäß (?:der )?regel|\bregel \d+", folded): + violations.append("erwähne keine internen Regeln oder Regelnummern") + if re.search(r"\b(?:aktuellere|maßgebliche|spezifischere)\w* (?:quelle|news|aussage)", folded): + violations.append( + "priorisiere bei dem offenen Konflikt keine Quelle als aktueller, " + "maßgeblicher oder spezifischer" + ) + return violations + + def _source_rows(results: list[ChunkResult]) -> list[dict]: rows = [] seen: set[str] = set() @@ -280,6 +343,7 @@ def answer_question( final = chat_with_length_retry(messages) violations = validate_answer(final, allowed) + violations += validate_decision_support_answer(question, final, allowed) regenerations = 0 if violations: regenerations = 1 @@ -296,6 +360,7 @@ def answer_question( {"role": "user", "content": warn}] ) retry_violations = validate_answer(retry, allowed) + retry_violations += validate_decision_support_answer(question, retry, allowed) if not retry_violations: final = retry violations = [] diff --git a/agent/query_planner.py b/agent/query_planner.py index 2024131..0eb4f24 100644 --- a/agent/query_planner.py +++ b/agent/query_planner.py @@ -11,6 +11,7 @@ from __future__ import annotations import json import re from dataclasses import dataclass +from datetime import date YEAR_RE = re.compile(r"\b(?:19|20)\d{2}\b") JSON_RE = re.compile(r"\{.*\}", re.DOTALL) @@ -19,6 +20,12 @@ AGGREGATION_RE = re.compile( r"neuerungen|übersicht|zusammenfassung|alle\s|übersicht", re.I ) COMPARISON_RE = re.compile(r"unterschied|vergleic|\bbzw\.|\bsowie\b", re.I) +DECISION_SUPPORT_RE = re.compile( + r"günstig(?:ste|er|e)?\s+lösung|abgabenbegünstigt|steuerfrei.*(?:auszahl|präm)|" + r"(?:zusätzlich|extra).*(?:auszahl|präm)", + re.IGNORECASE, +) + PLANNER_PROMPT = """Du planst Suchanfragen für eine Wissensbasis zur österreichischen Personalverrechnung (kuratierte Briefings, Kollektivverträge je Branche und @@ -51,6 +58,11 @@ class SubQuery: scope: str | None = None # "gesetz" (ohne Branchen-KV) | "kv" | None +def is_decision_support(question: str) -> bool: + """Erkennt Gestaltungsfragen, die den deterministischen Plan benötigen.""" + return bool(DECISION_SUPPORT_RE.search(question)) + + def should_plan(question: str) -> bool: """Heuristik-Gate: nur komplexe Fragen bekommen einen Planer-Call. @@ -64,6 +76,8 @@ def should_plan(question: str) -> bool: return True if COMPARISON_RE.search(q): return True + if is_decision_support(q): + return True if AGGREGATION_RE.search(q): return True if " und " in q.casefold() and len(words) >= 10: @@ -71,6 +85,56 @@ def should_plan(question: str) -> bool: return False +def decision_support_plan( + question: str, default_year: str | None = None +) -> tuple[list[SubQuery], str] | None: + """Deterministische Zerlegung für Gestaltungsfragen zu zusätzlichen + Arbeitnehmerleistungen. Sie verhindert, dass die vielen Branchen-KV- + Treffer allgemeine Abgabenregeln verdrängen. + + Ohne genanntes Jahr wird der neueste Indexstand verwendet; die Antwort + muss fehlenden Einzelfallkontext weiterhin über die Prompt-Regel erfragen. + """ + if not is_decision_support(question): + return None + explicit_year = YEAR_RE.search(question) + year = explicit_year.group(0) if explicit_year else default_year + year_text = f" {year}" if year else "" + direct = SubQuery( + text=( + f"Mitarbeiterprämie{year_text} WIKU Personal aktuell Nr 12 FAQ " + "Höchstbetrag 500 Lohnsteuer SV BV Beitragsfreie Bezugsbestandteile " + "§ 49 Abs 3 Z 30" + ), + stand_year=year, + scope="gesetz", + ) + cash_and_benefits = SubQuery( + text=( + "Zukunftssicherungsmaßnahmen 300 Kalenderjahr Risikoversicherung " + "Gruppenerfordernis Bezugsumwandlung Lohnsteuer SV BV DB DZ KommSt" + ), + stand_year=year, + scope="gesetz", + ) + purpose_bound = SubQuery( + text=( + "Sachzuwendungen Geschenke Barzahlungen stets steuerpflichtig Freibetrag " + "Beitragsfreie Bezugsbestandteile Z 1 bis Z 16a Mahlzeiten " + "Kinderbetreuungszuschuss" + ), + stand_year=year, + scope="gesetz", + ) + broad = bool( + re.search(r"günstig|möglichkeiten|statt|alternative", question, re.IGNORECASE) + ) + # Ein Gestaltungsvergleich ist kein Korpus-Survey: Die drei gezielten + # Queries passen in den normalen Antwortpfad und vermeiden unnötiges + # Map-Reduce samt zusätzlicher Latenz/Fehlverweigerungsrisiko. + return ([direct, cash_and_benefits, purpose_bound] if broad else [direct]), "specific" + + def parse_plan(raw: str, original: str) -> tuple[list[SubQuery], str]: """Robustes JSON-Parsing; jeder Fehler → [Originalfrage]. Liefert (Sub-Queries, Fragetyp 'survey' | 'specific').""" @@ -107,6 +171,10 @@ def plan_queries( """Liefert (Sub-Queries, geplant?, Fragetyp) — Call-/Parse-Fehler → Original als Einzel-Query, Typ 'specific'. Der Planer-Call ist klein (Frage ohne Kontext, kurzes num_predict); Temperature 0.""" + deterministic = decision_support_plan(question, default_year=str(date.today().year)) + if deterministic: + subs, qtype = deterministic + return subs, True, qtype if not should_plan(question): return [SubQuery(text=question)], False, "specific" prompt = PLANNER_PROMPT.format(question=question.strip()) diff --git a/agent/retrieve.py b/agent/retrieve.py index b7ed01b..38e9f02 100644 --- a/agent/retrieve.py +++ b/agent/retrieve.py @@ -13,7 +13,7 @@ import numpy as np from .config import Config from .normalize import fts_query from .ollama_client import OllamaClient -from .query_planner import SubQuery +from .query_planner import SubQuery, decision_support_plan @dataclass @@ -386,8 +386,18 @@ class Retriever: return main + extra def search(self, question: str, n_entries: int | None = None) -> list[ChunkResult]: - """Liefert die Top-Kontextblöcke (Hauptretrieval + cross_ref-Erweiterung).""" + """Liefert die Top-Kontextblöcke (Hauptretrieval + cross_ref-Erweiterung). + + Gestaltungsfragen zu zusätzlichen Arbeitnehmerleistungen werden + deterministisch in Direktzahlung und Alternativen zerlegt. Das gilt + auch für den Offline-Retrieval-Eval, der keinen LLM-Planer aufruft. + """ n = n_entries or self.cfg.context_blocks + latest_year = str(self._stand_max)[:4] + deterministic = decision_support_plan(question, default_year=latest_year) + if deterministic: + sub_queries, _qtype = deterministic + return self.search_multi(sub_queries, n_entries=n) fused, bm_all, dn_all = self._fuse_queries( [SubQuery(text=question)], self.cfg.candidate_pool ) @@ -403,7 +413,11 @@ class Retriever: mehreren Sub-Queries mittelgut matchen). Temporal-Intent: kv- Einträge im gefragten Geltungsjahr erhalten temporal_boost.""" n = n_entries or self.cfg.context_blocks - if len(sub_queries) == 1: + if ( + len(sub_queries) == 1 + and sub_queries[0].scope is None + and sub_queries[0].stand_year is None + ): return self.search(sub_queries[0].text, n_entries=n) pool = self.cfg.candidate_pool fused_total: dict[int, float] = {} diff --git a/tests/test_generate.py b/tests/test_generate.py index 0d5b081..03bb302 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -16,6 +16,7 @@ from agent.generate import ( strip_think, trim_results, validate_answer, + validate_decision_support_answer, ) @@ -42,6 +43,53 @@ def test_system_prompt_requires_nonofficial_source_disclosure(): assert "nicht amtlich" in SYSTEM_PROMPT +def test_system_prompt_requires_contextual_decision_support(): + assert "keine Option pauschal zum Sieger" in SYSTEM_PROMPT + assert "frei verfügbare Barzahlung" in SYSTEM_PROMPT + assert "systematisch in einen Katalog abgabenfreier Bezüge" in SYSTEM_PROMPT + assert "[wk-akt-04] und [lb-sva-03]" in SYSTEM_PROMPT + assert "Bezeichne keine der widersprechenden Quellen als" in SYSTEM_PROMPT + assert "genau EINE gezielte" in SYSTEM_PROMPT + assert "Regelnummern niemals" in SYSTEM_PROMPT + assert "spekuliere nicht mit Trainingswissen" in SYSTEM_PROMPT + + +class TestDecisionSupportValidation: + question = ( + "Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. " + "Was ist die günstigste Lösung?" + ) + allowed = ["wk-akt-04", "lb-sva-03"] + + def test_requires_conflict_when_both_sources_are_in_context(self): + violations = validate_decision_support_answer( + self.question, "Nur lohnsteuerfrei [wk-akt-04].", self.allowed + ) + assert violations and "Quellenkonflikt" in violations[0] + + def test_accepts_explicit_conflict(self): + answer = ( + "⚠ [lb-sva-03] ordnet die Prämie in den Katalog beitragsfreier " + "Bezüge ein; [wk-akt-04] nennt sie SV- und BV-pflichtig." + ) + assert validate_decision_support_answer(self.question, answer, self.allowed) == [] + + def test_rejects_reversed_source_roles_and_prompt_leakage(self): + answer = ( + "⚠ [wk-akt-04] und [lb-sva-03] ordnen die Prämie als beitragsfrei " + "ein; gemäß Regel 11 ist die aktuellere News-Quelle maßgeblich." + ) + violations = validate_decision_support_answer(self.question, answer, self.allowed) + assert any("Rollen" in item for item in violations) + assert any("internen Regeln" in item for item in violations) + assert any("priorisiere" in item for item in violations) + + def test_does_not_require_missing_source(self): + assert validate_decision_support_answer( + self.question, "Lohnsteuerfrei [wk-akt-04].", ["wk-akt-04"] + ) == [] + + class TestRefusalDetection: def test_refusal_phrase(self): assert looks_like_refusal("Dazu enthält die Wissensbasis keine Aussage.") diff --git a/tests/test_query_planner.py b/tests/test_query_planner.py index 86a435a..62e8e05 100644 --- a/tests/test_query_planner.py +++ b/tests/test_query_planner.py @@ -6,7 +6,13 @@ import pytest from agent.config import Config from agent.generate import answer_question -from agent.query_planner import SubQuery, parse_plan, plan_queries, should_plan +from agent.query_planner import ( + SubQuery, + decision_support_plan, + parse_plan, + plan_queries, + should_plan, +) from tests.conftest import FakeOllama @@ -20,12 +26,45 @@ def test_should_plan_gate(): "Wie wird die Überstundenpauschale behandelt und wie wirkt sie sich " "auf die Sozialversicherung und die Lohnsteuer aus?" ) + # Gestaltungsfrage -> planen, auch wenn sie unter der Längenschwelle bleibt + assert should_plan( + "Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. " + "Was ist die günstigste Lösung?" + ) # einfach -> Single-Shot assert not should_plan("Wie viele Werktage Urlaub stehen Arbeitnehmern zu?") assert not should_plan("Was ist Altersteilzeit?") assert not should_plan("Wie hoch ist der KV-Mindestlohn im Friseurgewerbe?") +def test_decision_support_plan_splits_direct_payment_and_alternatives(): + planned = decision_support_plan( + "Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. " + "Was ist die günstigste Lösung?", + default_year="2026", + ) + assert planned is not None + subs, qtype = planned + assert qtype == "specific" + assert len(subs) == 3 + assert all(s.scope == "gesetz" for s in subs) + assert all(s.stand_year == "2026" for s in subs) + assert "Mitarbeiterprämie 2026" in subs[0].text + assert "Zukunftssicherung" in subs[1].text + assert "Mahlzeiten" in subs[2].text + + +def test_decision_support_plan_keeps_explicit_year_and_specific_intent(): + subs, qtype = decision_support_plan( + "Kann ich 2025 jedem Mitarbeiter einfach 500 Euro steuerfrei auszahlen?", + default_year="2026", + ) + assert qtype == "specific" + assert len(subs) == 1 + assert subs[0].stand_year == "2025" + assert "Mitarbeiterprämie 2025" in subs[0].text + + def test_parse_plan_valid_and_fallback(): subs, qtype = parse_plan( 'Vorab: {"type": "specific", "queries": [{"text": "mindestlohn friseur", "stand_year": "2024"}, '