BAV_KI / src /amrl_substitution.py
AlixJabda's picture
Aktualisierung der AM-RL-Module und Retrieval-Logik
d80690d
Raw
History Blame Contribute Delete
28.8 kB
"""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"(?<![\wäöüß]){re.escape(begriff.lower())}(?![\wäöüß])", haystack):
span = (match.start(), match.end())
# Ein längerer Name hat diese Stelle schon belegt: „Levothyroxin-
# Natrium" darf nicht zusätzlich als „Levothyroxin" zählen.
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"
# ---------------------------------------------------------------------------
# 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