Files
aza/AzA march 2026/aza_bibliothek.py

366 lines
13 KiB
Python
Raw Normal View History

2026-06-10 22:55:03 +02:00
# -*- coding: utf-8 -*-
"""AzA Bibliothek — eigene vs. öffentliche Begriffe/Medikamente (getrennt von Doku-Prompts).
Doku-Prompts: aza_doku_vorlagen.py / aza_document_templates.json
Wörter/Medikamente: korrekturen.json + öffentliche Starterliste
"""
from __future__ import annotations
import copy
import json
import os
import re
import time
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
from aza_config import get_writable_data_dir
LIBRARY_SCHEMA_VERSION = 1
# UI-Kategorie → Persistenz-Kategorie (korrekturen.json)
CATEGORY_TO_STORE = {
"medication": "medikamente",
"substance": "wirkstoffe",
"diagnosis": "diagnosen",
"medical_term": "begriffe",
"person": "personen",
"correction": "begriffe",
"practice_term": "begriffe",
}
STORE_TO_CATEGORY = {v: k for k, v in CATEGORY_TO_STORE.items()}
CATEGORY_LABELS = {
"medication": "Medikamente",
"substance": "Wirkstoffe",
"diagnosis": "Diagnosen",
"medical_term": "Medizinische Begriffe",
"person": "Personen / Signaturen",
"correction": "Korrekturen",
"practice_term": "Praxisbegriffe",
"doku_prompt": "Doku-Prompts",
}
PRIVATE_UI_CATEGORIES = (
"medication", "medical_term", "person", "correction",
)
PUBLIC_UI_CATEGORIES = ("medication", "medical_term", "correction")
def _utc_now() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _public_cache_path() -> str:
return os.path.join(get_writable_data_dir(), "bibliothek_public_cache.json")
def merge_public_korrekturen(private: dict) -> dict:
"""Private Korrekturen + öffentliche Fallbacks. Private Einträge haben Vorrang."""
merged = copy.deepcopy(private) if isinstance(private, dict) else {}
try:
from aza_public_medication_terms import get_public_korrektur_mappings
public = get_public_korrektur_mappings()
except Exception:
public = {}
inactive = merged.get("_inactive")
if not isinstance(inactive, dict):
inactive = {}
merged["_inactive"] = inactive
for cat, mapping in public.items():
if not isinstance(mapping, dict):
continue
tgt = merged.setdefault(cat, {})
if not isinstance(tgt, dict):
tgt = {}
merged[cat] = tgt
inact = inactive.get(cat)
inact_set = set(inact) if isinstance(inact, (list, set, tuple)) else set()
for falsch, richtig in mapping.items():
if falsch in tgt or falsch in inact_set:
continue
tgt[falsch] = richtig
return merged
def load_korrekturen_for_apply() -> dict:
from aza_persistence import load_korrekturen
return merge_public_korrekturen(load_korrekturen())
def list_private_correction_rows(korrekturen: dict) -> List[Dict[str, Any]]:
"""Flache Liste privater Korrekturzeilen für UI."""
rows: List[Dict[str, Any]] = []
inactive = korrekturen.get("_inactive") if isinstance(korrekturen.get("_inactive"), dict) else {}
meta = korrekturen.get("_metadata") if isinstance(korrekturen.get("_metadata"), dict) else {}
adopted = meta.get("adopted_sources") if isinstance(meta.get("adopted_sources"), dict) else {}
for store_cat, mapping in (korrekturen or {}).items():
if not store_cat or str(store_cat).startswith("_"):
continue
if not isinstance(mapping, dict):
continue
ui_cat = STORE_TO_CATEGORY.get(store_cat, "medical_term")
inact = set(inactive.get(store_cat) or [])
for falsch, richtig in mapping.items():
src = adopted.get(f"{store_cat}:{falsch}", "Eigene Bibliothek")
rows.append({
"item_id": f"priv_{store_cat}_{falsch}",
"scope": "private",
"category": ui_cat,
"store_category": store_cat,
"term": falsch,
"preferred_spelling": richtig,
"variants": [falsch],
"active": falsch not in inact,
"source": src,
"updated_at": meta.get("updated_at", ""),
})
rows.sort(key=lambda r: (r.get("category", ""), r.get("term", "").lower()))
return rows
def list_public_entries(*, category: Optional[str] = None) -> List[Dict[str, Any]]:
try:
from aza_public_medication_terms import get_public_library_entries
items = list(get_public_library_entries())
except Exception:
items = []
cached = _load_public_cache()
for it in cached:
if isinstance(it, dict) and it.get("scope") == "public":
items.append(it)
if category:
items = [i for i in items if i.get("category") == category]
items.sort(key=lambda r: (r.get("category", ""), r.get("term", "").lower()))
return items
def _load_public_cache() -> List[Dict[str, Any]]:
try:
path = _public_cache_path()
if os.path.isfile(path):
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
if isinstance(data, list):
return data
if isinstance(data, dict) and isinstance(data.get("items"), list):
return data["items"]
except Exception:
pass
return []
def save_public_cache(items: List[Dict[str, Any]]) -> None:
path = _public_cache_path()
tmp = path + ".tmp"
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(tmp, "w", encoding="utf-8") as fh:
json.dump({"schema_version": LIBRARY_SCHEMA_VERSION, "items": items}, fh,
ensure_ascii=False, indent=2)
os.replace(tmp, path)
except Exception:
try:
if os.path.isfile(tmp):
os.remove(tmp)
except Exception:
pass
def adopt_public_entry(entry: dict) -> Tuple[bool, str]:
"""Übernimmt öffentlichen Eintrag als private Kopie in korrekturen.json."""
from aza_persistence import load_korrekturen, save_korrekturen, add_korrektur_to_bibliothek
if not isinstance(entry, dict):
return False, "Ungültiger Eintrag"
variants = entry.get("variants") or []
preferred = (entry.get("preferred_spelling") or entry.get("term") or "").strip()
cat = entry.get("category") or "medication"
store_cat = CATEGORY_TO_STORE.get(cat, "begriffe")
data = load_korrekturen()
meta = data.setdefault("_metadata", {})
if not isinstance(meta, dict):
meta = {}
data["_metadata"] = meta
adopted = meta.setdefault("adopted_sources", {})
if not isinstance(adopted, dict):
adopted = {}
meta["adopted_sources"] = adopted
added = 0
if variants and preferred:
for v in variants:
v = str(v).strip()
if v and v.lower() != preferred.lower():
add_korrektur_to_bibliothek(data, store_cat, v, preferred)
adopted[f"{store_cat}:{v}"] = entry.get("source") or "Öffentliche Bibliothek"
added += 1
elif entry.get("term") and preferred and str(entry["term"]).lower() != preferred.lower():
add_korrektur_to_bibliothek(data, store_cat, str(entry["term"]), preferred)
adopted[f"{store_cat}:{entry['term']}"] = entry.get("source") or "Öffentliche Bibliothek"
added += 1
elif preferred and cat == "medication":
note_key = f"_ref_{preferred.lower()}"
refs = meta.setdefault("medication_refs", {})
if not isinstance(refs, dict):
refs = {}
meta["medication_refs"] = refs
if note_key not in refs:
refs[note_key] = {
"brand_name": preferred,
"active_substance": entry.get("active_substance"),
"source": entry.get("source") or "Öffentliche Bibliothek",
"adopted_at": _utc_now(),
}
added = 1
meta["updated_at"] = _utc_now()
if added:
save_korrekturen(data)
return True, "Als eigene Kopie übernommen."
return False, "Nichts zu übernehmen (bereits vorhanden oder keine Korrekturvariante)."
def save_private_correction(
store_category: str,
falsch: str,
richtig: str,
*,
active: bool = True,
) -> None:
from aza_persistence import load_korrekturen, save_korrekturen, add_korrektur_to_bibliothek
data = load_korrekturen()
add_korrektur_to_bibliothek(data, store_category, falsch, richtig)
inact = data.setdefault("_inactive", {})
if not isinstance(inact, dict):
inact = {}
data["_inactive"] = inact
cat_inact = set(inact.get(store_category) or [])
if active:
cat_inact.discard(falsch)
else:
cat_inact.add(falsch)
inact[store_category] = sorted(cat_inact)
meta = data.setdefault("_metadata", {})
if isinstance(meta, dict):
meta["updated_at"] = _utc_now()
save_korrekturen(data)
def delete_private_correction(store_category: str, falsch: str) -> None:
from aza_persistence import load_korrekturen, save_korrekturen
data = load_korrekturen()
mp = data.get(store_category)
if isinstance(mp, dict) and falsch in mp:
del mp[falsch]
inact = data.setdefault("_inactive", {})
if isinstance(inact, dict) and isinstance(inact.get(store_category), list):
inact[store_category] = [x for x in inact[store_category] if x != falsch]
meta = data.setdefault("_metadata", {})
if isinstance(meta, dict):
meta["updated_at"] = _utc_now()
save_korrekturen(data)
def toggle_private_correction_active(store_category: str, falsch: str) -> bool:
from aza_persistence import load_korrekturen, save_korrekturen
data = load_korrekturen()
mp = data.get(store_category)
if not isinstance(mp, dict) or falsch not in mp:
return False
inact = data.setdefault("_inactive", {})
if not isinstance(inact, dict):
inact = {}
data["_inactive"] = inact
cat_inact = set(inact.get(store_category) or [])
if falsch in cat_inact:
cat_inact.discard(falsch)
new_active = True
else:
cat_inact.add(falsch)
new_active = False
inact[store_category] = sorted(cat_inact)
save_korrekturen(data)
return new_active
def list_private_doku_prompts() -> List[Dict[str, Any]]:
"""Eigene Doku-Prompt-Vorlagen (nur Metadaten für Bibliothek-UI)."""
try:
from aza_doku_vorlagen import DOC_TYPES, _templates_json_path
path = _templates_json_path()
if not os.path.isfile(path):
return []
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
if not isinstance(data, dict):
return []
rows = []
templates = data.get("templates") if isinstance(data.get("templates"), dict) else {}
active = data.get("active") if isinstance(data.get("active"), dict) else {}
for dt_key, dt_label in DOC_TYPES:
for tpl in templates.get(dt_key) or []:
if not isinstance(tpl, dict):
continue
if tpl.get("is_system"):
continue
tid = tpl.get("id") or ""
rows.append({
"item_id": tid,
"scope": "private",
"category": "doku_prompt",
"doc_type": dt_key,
"doc_type_label": dt_label,
"term": tpl.get("name") or "Unbenannt",
"preferred_spelling": tpl.get("name") or "",
"active": active.get(dt_key) == tid,
"published": bool(tpl.get("published")),
"source": "Eigene Bibliothek",
"updated_at": tpl.get("updated_at") or "",
})
return rows
except Exception:
return []
def publish_payload_sanitize(entry: dict) -> Tuple[bool, str]:
"""Prüft Publish-Payload grob auf Patientendaten-Muster."""
blob = json.dumps(entry, ensure_ascii=False).lower()
forbidden = (
"patient_name", "geburtsdatum", "sozialversicherung",
"ahv", "versicherten", "transcript",
)
for f in forbidden:
if f in blob:
return False, f"Verdächtiges Feld: {f}"
for word in ("herr ", "frau ", "geb.", "geb "):
if word in blob and "beispiel" not in blob:
pass # nur Heuristik, kein harter Block
return True, ""
def serialize_term_for_sync(entry: dict) -> dict:
"""Sync-Serialisierung für private Begriffseinträge."""
return {
"id": entry.get("item_id") or str(uuid.uuid4()),
"item_type": "library_term",
"title": entry.get("category") or "medical_term",
"trigger": entry.get("term") or "",
"content": entry.get("preferred_spelling") or "",
"metadata": {
"scope": entry.get("scope", "private"),
"active": bool(entry.get("active", True)),
"language": entry.get("language", "de"),
"market_region": entry.get("market_region", "de-CH"),
"revision": int(entry.get("revision") or 1),
"source": entry.get("source", ""),
},
}