"""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]] = {} # Ab hier gilt der Snapshot als überholt und der Befund sagt das. Der G-BA # beschließt Ergänzungen unregelmäßig, in der Praxis wenige Male im Jahr; ein # halbes Jahr ohne Abgleich ist die Grenze, ab der eine Auskunft ohne Warnung # nicht mehr vertretbar ist. VERALTET_AB_TAGEN = 180 # Die Frage muss überhaupt vom Austausch handeln. Regelbasiert wie # `corpus_router`: eine Substring-Prüfung entscheidet das exakt, ein # Modellaufruf brächte Latenz und Nichtdeterminismus an eine Stelle, an der # Verlässlichkeit der ganze Zweck ist. AUSTAUSCH_SIGNALE: tuple[str, ...] = ( "aut idem", "aut-idem", # "tausch" statt "austausch": es trägt auch "ausgetauscht" (aus-ge-TAUSCH-t), # das die längere Form verfehlt hätte. "tausch", "ersetzen", "ersetzt", "ersetzung", "substitut", "wirkstoffgleich", "importarzneimittel", "rabattvertrag", "rabattarzneimittel", ) # Darreichungsformen, die in einer Frage vorkommen können. Gebraucht, um zu # erkennen, dass nach einer *anderen* Form gefragt wurde als die Liste führt: # Ciclosporin steht dort mit „Lösung zum Einnehmen" und „Weichkapseln" — eine # Infusionslösung ist davon nicht erfasst, und ein pauschales „ausgeschlossen" # wäre an dieser Stelle schlicht falsch. 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", ) # Endungen, an denen ein Wirkstoffname erkennbar ist. Nur dafür da, eine # Negativauskunft konkret zu machen („für ‚Simvastatin' ist nichts gelistet") # statt bloß allgemein. Eine Lücke in dieser Liste kostet nur die konkrete # Nennung, nie die Richtigkeit: ohne Kandidaten wird allgemeiner formuliert. 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", ) # Großgeschriebene Wörter, die in solchen Fragen laufend vorkommen und keine # Wirkstoffe sind. 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 # passend | unspezifischer | abweichend | nicht_genannt — siehe `_form_bezug` 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, } # --------------------------------------------------------------------------- # Datenzugriff # --------------------------------------------------------------------------- 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: # noqa: BLE001 - ein defekter Snapshot darf keine Antwort verhindern. 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 "" # --------------------------------------------------------------------------- # Erkennung in der Frage # --------------------------------------------------------------------------- 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)) # Klammerzusätze sind in einer Frage nie wörtlich enthalten; der Name ohne # sie bleibt trotzdem suchbar. 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"(? 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" # --------------------------------------------------------------------------- # Prüfung # --------------------------------------------------------------------------- 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: # Ohne erkennbaren Wirkstoff gibt es nichts nachzuschlagen. Eine # Auskunft „nichts gelistet" wäre hier eine Aussage über eine Frage, # die gar keinen Wirkstoff nennt. Der Reichweiten-Hinweis zu den # AM-RL-Anlagen greift in diesem Fall und sagt das Richtige. 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, ) ) # --------------------------------------------------------------------------- # Darstellung # --------------------------------------------------------------------------- _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}.") # Die Einschränkung steht VOR der Rechtsfolge. Andersherum behauptet der # Block erst einen Ausschluss und nimmt ihn im nächsten Satz zurück. 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() # Das Verhältnis der beiden Teile explizit machen. Die Kurzantwort unten # wird angeglichen, die übrigen Abschnitte bleiben Modelltext — und der # hedgt regelmäßig ("die Quellen liefern keine direkte Antwort", gemessen # bei der Ambroxol-Frage), weil ihm die Liste fehlt. Ohne diesen Satz liest # sich die Einordnung als Widerruf des Befundes drei Absätze darüber. 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)) # Rückwärtskompatibler Name für Aufrufer, die nur den Block voranstellen wollen. 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