| """Substitutionsausschluss nachschlagen statt danach suchen. |
| |
| Teil B der AM-RL-Anlage VII listet die Wirkstoffe, deren Ersetzung durch ein |
| wirkstoffgleiches Arzneimittel nach § 129 Absatz 1a Satz 2 SGB V ausgeschlossen |
| ist. Diese Liste wird hier **nachgeschlagen**, nicht durchsucht, und der Grund |
| ist keine Bequemlichkeit: |
| |
| * Die für die Apotheke entscheidende Auskunft ist häufig die *negative* — „steht |
| nicht drauf, also darf ausgetauscht werden". Ein Vektor-Retrieval kann |
| Abwesenheit nicht belegen: es liefert immer die k ähnlichsten Treffer, und |
| „nicht gefunden" heißt dort nie „nicht enthalten". |
| * Bei Wirkstoffnamen ist semantische Nähe aktiv gefährlich. Ciclosporin und |
| Ciclopirox stehen sich im Embedding nah und haben nichts miteinander zu tun. |
| |
| Zwei Dinge, die dieses Modul bewusst **nicht** tut: |
| |
| * **Es urteilt nicht über Retaxationsrisiko.** Zurück kommt der Listenstatus mit |
| Fundstelle — eine Normtatsache. Welche Abgabe- und Abrechnungsfolge daraus |
| erwächst, steht im Rahmenvertrag und bleibt Sache des RAG-Teils. Die |
| Verordnungsverantwortung des Arztes und die Prüfpflicht der Apotheke sind nicht |
| dasselbe, und ein Werkzeug, das beides vermischt, wäre fachlich falsch. |
| * **Es rät nicht.** Ein Eintrag wird nur gemeldet, wenn der Wirkstoff wörtlich |
| oder über ein kuratiertes Synonym getroffen wurde. Fuzzy-Matching auf |
| Wirkstoffnamen ist genau der Fehler, den das Retrieval schon macht. |
| |
| Vier Fälle sind strikt getrennt, weil sie unterschiedliche Rechtsfolgen haben — |
| ein „steht auf der Liste, also verboten" wäre bei den beiden mittleren falsch: |
| |
| ausschluss Austausch ausgeschlossen. |
| ausschluss_bedingt Nur unter der genannten Bedingung, z. B. |
| Everolimus nur bis 1 mg Wirkstoffgehalt. |
| ausschluss_zwischen_varianten Nur zwischen den genannten Varianten, z. B. |
| Buprenorphin-Pflaster unterschiedlicher |
| Applikationshöchstdauer. Innerhalb derselben |
| Variante bleibt der Austausch zulässig. |
| nicht_gelistet Kein gelisteter Wirkstoff in der Frage. |
| |
| Der `stand` ist Pflichtangabe jedes Befundes, nicht Fußnote: eine veraltete |
| Liste, die selbstsicher antwortet, ist schlimmer als keine. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import logging |
| import re |
| from dataclasses import dataclass, field |
| from datetime import date, datetime |
| from pathlib import Path |
| from typing import Any, Dict, List, Optional, Sequence |
|
|
| from answer_schema import SHORT_ANSWER_HEADING, split_sections |
|
|
| logger = logging.getLogger(__name__) |
|
|
| _CACHE: Dict[str, Dict[str, Any]] = {} |
|
|
| |
| |
| |
| |
| VERALTET_AB_TAGEN = 180 |
|
|
| |
| |
| |
| |
| AUSTAUSCH_SIGNALE: tuple[str, ...] = ( |
| "aut idem", |
| "aut-idem", |
| |
| |
| "tausch", |
| "ersetzen", |
| "ersetzt", |
| "ersetzung", |
| "substitut", |
| "wirkstoffgleich", |
| "importarzneimittel", |
| "rabattvertrag", |
| "rabattarzneimittel", |
| ) |
|
|
| |
| |
| |
| |
| |
| DARREICHUNGSFORMEN: tuple[str, ...] = ( |
| "lösung zum einnehmen", "transdermale pflaster", "hartkapseln, retardiert", |
| "retardtabletten", "filmtabletten", "brausetabletten", "schmelztabletten", |
| "weichkapseln", "hartkapseln", "infusionslösung", "injektionslösung", |
| "augentropfen", "augensalbe", "zäpfchen", "suppositorien", "granulat", |
| "tabletten", "kapseln", "pflaster", "dragees", "tropfen", "salbe", "creme", |
| "gel", "saft", "sirup", "pulver", "spray", "inhalat", "ampullen", |
| ) |
|
|
| |
| |
| |
| |
| INN_ENDUNGEN: tuple[str, ...] = ( |
| "cillin", "mycin", "floxacin", "prazol", "sartan", "pril", "olol", "statin", |
| "dipin", "tidin", "azepam", "barbital", "phyllin", "codon", "morphin", |
| "fentanil", "tinib", "zumab", "ximab", "umab", "cyclin", "conazol", |
| "gliptin", "glitazon", "formin", "parin", "coumon", "xaban", "gatran", |
| "setron", "triptan", "profen", "oxicam", "olimus", "sporin", "limus", |
| "thyroxin", "valproat", "phenidat", "toin", "midon", "oxin", "pam", "zepin", |
| ) |
|
|
| |
| |
| KEINE_WIRKSTOFFE: frozenset[str] = frozenset( |
| { |
| "apotheke", "arzneimittel", "fertigarzneimittel", "rabattvertrag", |
| "rabattverträge", "substitutionsausschluss", "substitutionsausschlussliste", |
| "darreichungsform", "darreichungsformen", "wirkstoff", "krankenkasse", |
| "retaxation", "verordnung", "versicherte", "anlage", "richtlinie", |
| "rahmenvertrag", "tabletten", "retardtabletten", "hartkapseln", |
| "weichkapseln", "pflaster", "kapseln", "lösung", "import", |
| "importarzneimittel", "generikum", "generika", "packung", |
| } |
| ) |
|
|
|
|
| @dataclass(frozen=True) |
| class Treffer: |
| """Ein Listeneintrag, wie er für die Antwort gebraucht wird.""" |
|
|
| wirkstoff: str |
| darreichungsform: str |
| regel: str |
| bedingung: Optional[str] |
| hinweis: Optional[str] |
| seite: Optional[int] |
|
|
| @property |
| def status(self) -> str: |
| if self.regel == "ausschluss_zwischen_varianten": |
| return "ausschluss_zwischen_varianten" |
| return "ausschluss_bedingt" if self.bedingung else "ausschluss" |
|
|
|
|
| @dataclass |
| class Befund: |
| """Ergebnis einer Prüfung. `status` ist der Gesamtstatus über alle Treffer.""" |
|
|
| status: str |
| gefragter_wirkstoff: Optional[str] = None |
| treffer: List[Treffer] = field(default_factory=list) |
| gefragte_darreichungsform: Optional[str] = None |
| |
| form_bezug: str = "nicht_genannt" |
| stand: str = "" |
| stand_iso: str = "" |
| veraltet: bool = False |
| alter_tage: Optional[int] = None |
| fundstelle: str = "" |
| url: str = "" |
| grund: str = "" |
|
|
| @property |
| def ist_belastbar(self) -> bool: |
| """True, wenn der Befund eine Auskunft trägt (positiv oder negativ).""" |
| return self.status in { |
| "ausschluss", |
| "ausschluss_bedingt", |
| "ausschluss_zwischen_varianten", |
| "nicht_gelistet", |
| } |
|
|
| def to_dict(self) -> Dict[str, Any]: |
| return { |
| "status": self.status, |
| "gefragter_wirkstoff": self.gefragter_wirkstoff, |
| "treffer": [ |
| { |
| "wirkstoff": t.wirkstoff, |
| "darreichungsform": t.darreichungsform, |
| "regel": t.regel, |
| "bedingung": t.bedingung, |
| "seite": t.seite, |
| } |
| for t in self.treffer |
| ], |
| "gefragte_darreichungsform": self.gefragte_darreichungsform, |
| "form_bezug": self.form_bezug, |
| "stand": self.stand, |
| "veraltet": self.veraltet, |
| "alter_tage": self.alter_tage, |
| "fundstelle": self.fundstelle, |
| "url": self.url, |
| "grund": self.grund, |
| } |
|
|
|
|
| |
| |
| |
|
|
| def load_liste(path: Path) -> Dict[str, Any]: |
| """Snapshot lesen und cachen. Eine fehlende Datei ist kein Absturz.""" |
| key = str(path) |
| if key in _CACHE: |
| return _CACHE[key] |
|
|
| data: Dict[str, Any] = {} |
| try: |
| if path.exists(): |
| data = json.loads(path.read_text(encoding="utf-8")) |
| except Exception as exc: |
| logger.warning("Substitutionsausschlussliste nicht lesbar: %s (%s)", path, exc) |
| data = {} |
|
|
| _CACHE[key] = data |
| return data |
|
|
|
|
| _MONATE = { |
| "januar": 1, "februar": 2, "märz": 3, "maerz": 3, "april": 4, "mai": 5, |
| "juni": 6, "juli": 7, "august": 8, "september": 9, "oktober": 10, |
| "november": 11, "dezember": 12, |
| } |
|
|
|
|
| def _stand_iso(stand: str) -> str: |
| """'15. Mai 2026' -> '2026-05-15'. Leer, wenn nicht deutbar.""" |
| text = " ".join(str(stand or "").split()) |
| match = re.match(r"^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]+)\s*(\d{4})$", text) |
| if match: |
| monat = _MONATE.get(match.group(2).lower()) |
| if monat: |
| return f"{int(match.group(3)):04d}-{monat:02d}-{int(match.group(1)):02d}" |
| match = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", text) |
| return text if match else "" |
|
|
|
|
| |
| |
| |
|
|
| def fragt_nach_austausch(question: str) -> bool: |
| haystack = " ".join(str(question or "").lower().split()) |
| return any(signal in haystack for signal in AUSTAUSCH_SIGNALE) |
|
|
|
|
| def _namen_index(daten: Dict[str, Any]) -> List[tuple[str, str]]: |
| """(suchbegriff, kanonischer Wirkstoff), längste Begriffe zuerst. |
| |
| Die Sortierung entscheidet über die Richtigkeit: „Levothyroxin-Natrium + |
| Kaliumiodid (fixe Kombination)" muss vor „Levothyroxin-Natrium" geprüft |
| werden, sonst gewinnt der kürzere Name und die fixe Kombination fällt weg. |
| """ |
| paare: List[tuple[str, str]] = [] |
| for eintrag in daten.get("eintraege") or []: |
| name = str(eintrag.get("wirkstoff") or "").strip() |
| if not name: |
| continue |
| paare.append((name, name)) |
| for synonym in eintrag.get("synonyme") or []: |
| if str(synonym).strip(): |
| paare.append((str(synonym).strip(), name)) |
| |
| |
| for name, kanonisch in list(paare): |
| ohne = re.sub(r"\s*\([^)]*\)\s*", " ", name).strip() |
| if ohne and ohne != name: |
| paare.append((ohne, kanonisch)) |
| return sorted(set(paare), key=lambda p: -len(p[0])) |
|
|
|
|
| def erkannte_wirkstoffe(question: str, daten: Dict[str, Any]) -> List[str]: |
| """Gelistete Wirkstoffe, die die Frage nennt — kanonisch, ohne Dubletten.""" |
| haystack = " ".join(str(question or "").lower().split()) |
| gefunden: List[str] = [] |
| verbraucht: List[tuple[int, int]] = [] |
|
|
| for begriff, kanonisch in _namen_index(daten): |
| for match in re.finditer(rf"(?<![\wäöüß]){re.escape(begriff.lower())}(?![\wäöüß])", haystack): |
| span = (match.start(), match.end()) |
| |
| |
| if any(s <= span[0] and span[1] <= e for s, e in verbraucht): |
| continue |
| verbraucht.append(span) |
| if kanonisch not in gefunden: |
| gefunden.append(kanonisch) |
| return gefunden |
|
|
|
|
| def wirkstoff_kandidat(question: str) -> Optional[str]: |
| """Ein wirkstoffartiges Wort aus der Frage, nur zur Formulierung. |
| |
| Dient allein dazu, eine Negativauskunft konkret zu benennen. Findet die |
| Heuristik nichts, wird allgemeiner formuliert — nie falscher. |
| """ |
| for wort in re.findall(r"\b([A-ZÄÖÜ][\wäöüß\-]{5,})\b", str(question or "")): |
| if wort.lower() in KEINE_WIRKSTOFFE: |
| continue |
| if wort.lower().endswith(INN_ENDUNGEN): |
| return wort |
| return None |
|
|
|
|
| def genannte_darreichungsform(question: str) -> Optional[str]: |
| """Die spezifischste Darreichungsform, die die Frage nennt.""" |
| haystack = " ".join(str(question or "").lower().split()) |
| for form in sorted(DARREICHUNGSFORMEN, key=len, reverse=True): |
| if form in haystack: |
| return form |
| return None |
|
|
|
|
| def _form_bezug(genannt: Optional[str], gelistet: Sequence[str]) -> str: |
| """Verhältnis der erfragten zur gelisteten Darreichungsform. |
| |
| Drei Fälle mit verschiedener Rechtsfolge, und die Mitte ist die wichtige: |
| |
| * ``passend`` — identisch, der Eintrag greift. |
| * ``unspezifischer`` — die Frage nennt „Tabletten", die Liste |
| „Retardtabletten". Nicht dasselbe: eine unretardierte Tablette desselben |
| Wirkstoffs ist nicht vom Ausschluss erfasst. Wird offengelegt statt |
| entschieden. |
| * ``abweichend`` — kein Bezug, z. B. Infusionslösung gegen „Lösung zum |
| Einnehmen". Der Eintrag trägt die Frage dann nicht. |
| """ |
| if not genannt: |
| return "nicht_genannt" |
|
|
| gefragt = " ".join(genannt.lower().split()) |
| formen = [" ".join(str(f or "").lower().split()) for f in gelistet] |
|
|
| if any(gefragt == f for f in formen): |
| return "passend" |
| if any(gefragt in f or f in gefragt for f in formen): |
| return "unspezifischer" |
| return "abweichend" |
|
|
|
|
| |
| |
| |
|
|
| def pruefe( |
| question: str = "", |
| *, |
| path: Path, |
| wirkstoff: Optional[str] = None, |
| darreichungsform: Optional[str] = None, |
| kandidat: Optional[str] = None, |
| heute: Optional[date] = None, |
| ) -> Befund: |
| """Teil B für die Frage (oder den übergebenen Wirkstoff) nachschlagen. |
| |
| `kandidat` ist ein Wirkstoffname, den ein anderer Nachschlag bereits sicher |
| erkannt hat — Teil A der Anlage führt rund 170 Wirkstoffe, Teil B zwanzig. |
| Ohne diese Übergabe könnte für „Ambroxol" keine Negativauskunft ergehen, |
| obwohl feststeht, dass es ein Wirkstoff ist: die Endungsheuristik in |
| `wirkstoff_kandidat` erkennt ihn nicht, und der Befund fiele auf |
| „nicht prüfbar" zurück, statt „steht nicht auf der Ausschlussliste" zu sagen. |
| """ |
| daten = load_liste(path) |
| if not daten or not (daten.get("eintraege") or []): |
| return Befund(status="nicht_pruefbar", grund="snapshot_fehlt_oder_leer") |
|
|
| quelle = daten.get("quelle") or {} |
| stand = str(quelle.get("stand") or "") |
| stand_iso = _stand_iso(stand) |
| fundstelle = f"{quelle.get('dokument', 'AM-RL Anlage VII')} Teil B" |
| url = str(quelle.get("url") or "") |
|
|
| if not stand: |
| return Befund(status="nicht_pruefbar", grund="stand_unbekannt", fundstelle=fundstelle, url=url) |
|
|
| alter_tage: Optional[int] = None |
| veraltet = False |
| if stand_iso: |
| try: |
| gemessen = (heute or date.today()) - datetime.strptime(stand_iso, "%Y-%m-%d").date() |
| alter_tage = max(0, gemessen.days) |
| veraltet = alter_tage > VERALTET_AB_TAGEN |
| except ValueError: |
| alter_tage = None |
|
|
| def _fertig(befund: Befund) -> Befund: |
| befund.stand = stand |
| befund.stand_iso = stand_iso |
| befund.alter_tage = alter_tage |
| befund.veraltet = veraltet |
| befund.fundstelle = fundstelle |
| befund.url = url |
| return befund |
|
|
| if wirkstoff: |
| namen = erkannte_wirkstoffe(wirkstoff, daten) |
| gefragt = wirkstoff.strip() |
| else: |
| if not fragt_nach_austausch(question): |
| return _fertig(Befund(status="nicht_einschlaegig", grund="frage_ohne_austauschbezug")) |
| namen = erkannte_wirkstoffe(question, daten) |
| gefragt = namen[0] if namen else (wirkstoff_kandidat(question) or (kandidat or "").strip()) |
|
|
| if not namen: |
| if not gefragt: |
| |
| |
| |
| |
| return _fertig(Befund(status="nicht_pruefbar", grund="kein_wirkstoff_erkannt")) |
| return _fertig( |
| Befund( |
| status="nicht_gelistet", |
| gefragter_wirkstoff=gefragt, |
| grund="wirkstoff_nicht_in_teil_b", |
| ) |
| ) |
|
|
| treffer = [ |
| Treffer( |
| wirkstoff=str(e.get("wirkstoff") or ""), |
| darreichungsform=str(e.get("darreichungsform") or ""), |
| regel=str(e.get("regel") or "ausschluss"), |
| bedingung=(str(e["bedingung"]) if e.get("bedingung") else None), |
| hinweis=(str(e["hinweis"]) if e.get("hinweis") else None), |
| seite=e.get("seite"), |
| ) |
| for e in daten["eintraege"] |
| if str(e.get("wirkstoff") or "") in namen |
| ] |
|
|
| gefragte_form = darreichungsform or genannte_darreichungsform(question) |
| bezug = _form_bezug(gefragte_form, [t.darreichungsform for t in treffer]) |
|
|
| if bezug == "passend": |
| gefragt_norm = " ".join(str(gefragte_form).lower().split()) |
| treffer = [t for t in treffer if " ".join(t.darreichungsform.lower().split()) == gefragt_norm] or treffer |
|
|
| rangfolge = ("ausschluss", "ausschluss_bedingt", "ausschluss_zwischen_varianten") |
| status = min((t.status for t in treffer), key=rangfolge.index) |
|
|
| return _fertig( |
| Befund( |
| status=status, |
| gefragter_wirkstoff=namen[0], |
| treffer=treffer, |
| gefragte_darreichungsform=gefragte_form, |
| form_bezug=bezug, |
| ) |
| ) |
|
|
|
|
| |
| |
| |
|
|
| _EINLEITUNG = "Substitutionsausschluss — deterministische Listenprüfung" |
|
|
|
|
| def _gross(text: Optional[str]) -> str: |
| """Erstes Zeichen groß. Das Formularvokabular ist kleingeschrieben, die |
| Ausgabe geht an einen Nutzer.""" |
| value = str(text or "").strip() |
| return value[:1].upper() + value[1:] if value else "" |
|
|
|
|
| def befund_block(befund: Befund) -> str: |
| """Der Befund als Textblock, der der Antwort vorangestellt wird. |
| |
| Bewusst von Code formuliert und nicht vom Modell: der Wert dieser Prüfung |
| liegt in ihrer Verlässlichkeit, und ein Umweg über die Sprachgenerierung |
| gäbe genau die wieder her, die sie beseitigt. |
| """ |
| if not befund.ist_belastbar: |
| return "" |
|
|
| stand = f"Stand {befund.stand}" |
| kopf = f"{_EINLEITUNG} ({befund.fundstelle}, {stand}):" |
| zeilen: List[str] = [kopf] |
|
|
| if befund.status == "nicht_gelistet": |
| if befund.gefragter_wirkstoff: |
| zeilen.append( |
| f"„{befund.gefragter_wirkstoff}“ ist in Teil B nicht gelistet. Ein " |
| "Substitutionsausschluss nach § 129 Absatz 1a Satz 2 SGB V besteht dafür nicht." |
| ) |
| else: |
| zeilen.append( |
| "Die Frage nennt keinen der in Teil B gelisteten Wirkstoffe. Für die " |
| "genannten Arzneimittel besteht danach kein Substitutionsausschluss nach " |
| "§ 129 Absatz 1a Satz 2 SGB V." |
| ) |
| zeilen.append( |
| "Die Liste ist wirkstoffbezogen: bei einem Handelsnamen ist der Wirkstoff (INN) " |
| "zu prüfen, bevor auf diese Auskunft abgestellt wird." |
| ) |
| else: |
| formen = "; ".join(dict.fromkeys(t.darreichungsform for t in befund.treffer if t.darreichungsform)) |
| zeilen.append(f"{befund.gefragter_wirkstoff} ist in Teil B gelistet — {formen}.") |
|
|
| |
| |
| if befund.form_bezug == "abweichend": |
| zeilen.append( |
| f"Die Frage nennt „{_gross(befund.gefragte_darreichungsform)}“; diese Form " |
| "führt die Liste für diesen Wirkstoff nicht. Die folgende Rechtsfolge gilt " |
| "deshalb nur für die oben genannten Formen. Die Vorbemerkung zu Teil B " |
| "erfasst allerdings auch nicht aufgeführte Bezeichnungen, soweit sie den " |
| "definitorischen Voraussetzungen der gelisteten Standard Terms entsprechen; " |
| "das ist gesondert zu prüfen." |
| ) |
|
|
| if befund.status == "ausschluss": |
| zeilen.append( |
| "Die Ersetzung durch ein wirkstoffgleiches Arzneimittel ist damit nach " |
| "§ 129 Absatz 1a Satz 2 SGB V ausgeschlossen." |
| ) |
| elif befund.status == "ausschluss_bedingt": |
| bedingungen = "; ".join(dict.fromkeys(t.bedingung or "" for t in befund.treffer if t.bedingung)) |
| zeilen.append( |
| "Der Ausschluss gilt nur unter der Einschränkung der Liste: " |
| f"{bedingungen}. Ob sie im konkreten Fall erfüllt ist, ist am Präparat zu prüfen." |
| ) |
| else: |
| bedingungen = "; ".join(dict.fromkeys(t.bedingung or "" for t in befund.treffer if t.bedingung)) |
| zeilen.append( |
| "Der Ausschluss ist hier nicht absolut: ausgeschlossen ist nur der Austausch " |
| f"zwischen unterschiedlichen Varianten — {bedingungen} Innerhalb derselben " |
| "Variante bleibt die Ersetzung zulässig." |
| ) |
|
|
| hinweise = [t.hinweis for t in befund.treffer if t.hinweis] |
| if hinweise: |
| zeilen.append(f"Zum Wirkstoff: {hinweise[0]}.") |
|
|
| if befund.form_bezug == "unspezifischer": |
| zeilen.append( |
| f"Die Liste benennt die Form genauer als die Frage („{_gross(befund.gefragte_darreichungsform)}“ " |
| f"gegenüber „{formen}“). Ob das verordnete Präparat unter den gelisteten " |
| "Eintrag fällt, ist am Präparat zu prüfen." |
| ) |
|
|
| if befund.veraltet: |
| zeilen.append( |
| f"Achtung Aktualität: Dieser Auszug ist {befund.alter_tage} Tage alt. Der G-BA " |
| "ergänzt Teil B durch Beschluss; vor einer Entscheidung ist die geltende Fassung " |
| "abzugleichen." |
| ) |
| if befund.url: |
| zeilen.append(f"Quelle: {befund.url}") |
|
|
| return "\n".join(zeilen) |
|
|
|
|
| def verdikt(befund: Befund) -> str: |
| """Die Antwort auf die Frage in einem Satz, aus den Daten formuliert.""" |
| if not befund.ist_belastbar: |
| return "" |
|
|
| if befund.status == "nicht_gelistet": |
| wer = f"„{befund.gefragter_wirkstoff}“" if befund.gefragter_wirkstoff else "Der erfragte Wirkstoff" |
| return ( |
| f"{wer} steht nicht in Teil B der AM-RL-Anlage VII; ein Substitutionsausschluss " |
| f"nach § 129 Absatz 1a Satz 2 SGB V besteht dafür nicht (Listenprüfung oben, " |
| f"Stand {befund.stand})." |
| ) |
|
|
| formen = "; ".join(dict.fromkeys(t.darreichungsform for t in befund.treffer if t.darreichungsform)) |
| bedingungen = "; ".join(dict.fromkeys(t.bedingung or "" for t in befund.treffer if t.bedingung)) |
|
|
| if befund.status == "ausschluss_zwischen_varianten": |
| kern = ( |
| f"Nur eingeschränkt: {befund.gefragter_wirkstoff} ({formen}) ist in Teil B der " |
| f"AM-RL-Anlage VII gelistet, ausgeschlossen ist aber allein der Austausch " |
| f"zwischen unterschiedlichen Varianten — {bedingungen} Innerhalb derselben " |
| f"Variante bleibt die Ersetzung zulässig" |
| ) |
| elif befund.status == "ausschluss_bedingt": |
| kern = ( |
| f"Nur bedingt: {befund.gefragter_wirkstoff} ({formen}) ist in Teil B der " |
| f"AM-RL-Anlage VII gelistet, der Ausschluss greift jedoch nur {bedingungen}" |
| ) |
| else: |
| kern = ( |
| f"Nein. {befund.gefragter_wirkstoff} ({formen}) ist in Teil B der AM-RL-Anlage VII " |
| f"gelistet; die Ersetzung durch ein wirkstoffgleiches Arzneimittel ist nach " |
| f"§ 129 Absatz 1a Satz 2 SGB V ausgeschlossen" |
| ) |
|
|
| if befund.form_bezug == "abweichend": |
| kern += ( |
| f". Das gilt für die gelisteten Formen — die erfragte Form " |
| f"„{_gross(befund.gefragte_darreichungsform)}“ führt die Liste nicht" |
| ) |
|
|
| return f"{kern} (Listenprüfung oben, Stand {befund.stand})." |
|
|
|
|
| def antwort_mit_befunden(answer: str, *, bloecke: Sequence[str], verdikt: str = "") -> str: |
| """Listenbefunde voranstellen und die Kurzantwort daran angleichen. |
| |
| Der gemeinsame Mechanismus für beide Teile der Anlage VII. Warum die |
| Kurzantwort ersetzt wird und nicht nur ein Block davorgesetzt: das Modell |
| beantwortet dieselbe Frage darunter aus § 129 SGB V noch einmal und kommt |
| zur allgemeinen Regel, weil ihm die Liste fehlt, die die Ausnahme trägt. |
| Gemessen an der ersten Livefrage stand „ist ausgeschlossen" direkt über „ist |
| möglich". Beide Sätze stimmen in ihrem Bezugsrahmen, aber nichts im Text |
| markiert den Wechsel — dieselbe Lage, die |
| `corpus_amendments.reconcile_negative_answer` für spätere |
| Änderungsvereinbarungen auflöst, und sie wird hier genauso aufgelöst. |
| """ |
| vorhanden = [b for b in bloecke if b and b.strip()] |
| if not vorhanden: |
| return answer |
|
|
| text = (answer or "").strip() |
|
|
| |
| |
| |
| |
| |
| if text and verdikt: |
| vorhanden = list(vorhanden) |
| vorhanden[-1] += ( |
| "\n\nDie Listenprüfung entscheidet die Zugehörigkeit abschließend. Die " |
| "nachfolgenden Abschnitte geben die allgemeine Regelung wieder und stehen " |
| "unter diesem Befund." |
| ) |
|
|
| if text and verdikt: |
| blocks = split_sections(text) |
| if any(heading == SHORT_ANSWER_HEADING for heading, _ in blocks): |
| teile: List[str] = [] |
| for heading, body_lines in blocks: |
| body = "\n".join(body_lines).strip() |
| if heading is None: |
| if body: |
| teile.append(body) |
| elif heading == SHORT_ANSWER_HEADING: |
| teile.append(f"{heading}:\n{verdikt}") |
| else: |
| teile.append(f"{heading}:\n{body}" if body else f"{heading}:") |
| text = "\n\n".join(t for t in teile if t.strip()) |
|
|
| kopf = "\n\n".join(vorhanden) |
| return f"{kopf}\n\n{text}" if text else kopf |
|
|
|
|
| def in_antwort_einsetzen(answer: str, befund: Befund) -> str: |
| """Befund voranstellen und die Kurzantwort des Modells daran angleichen. |
| |
| Beides gehört zusammen. Der Block allein reicht nicht: das Modell |
| beantwortet dieselbe Frage darunter aus Rahmenvertrag und Gesetz noch einmal |
| und kommt dabei zur allgemeinen Regel („Ersetzung ist vorzunehmen, wenn …"), |
| weil ihm die Liste fehlt, die die Ausnahme trägt. Gemessen an der ersten |
| Livefrage stand dann „ist ausgeschlossen" direkt über „ist möglich". |
| |
| Beide Sätze sind in ihrem Bezugsrahmen richtig, aber nichts im Text markiert |
| den Wechsel — dieselbe Lage, die `corpus_amendments.reconcile_negative_answer` |
| für spätere Änderungsvereinbarungen auflöst, und sie wird hier genauso |
| aufgelöst: die deterministische Prüfung setzt die Kurzantwort, die übrigen |
| Abschnitte des Modells bleiben als Einordnung stehen. |
| """ |
| return antwort_mit_befunden(answer, bloecke=[befund_block(befund)], verdikt=verdikt(befund)) |
|
|
|
|
| |
| def voranstellen(answer: str, block: str) -> str: |
| if not block: |
| return answer |
| text = (answer or "").strip() |
| return f"{block}\n\n{text}" if text else block |
|
|