Files
ElebobaandClaude Opus 5.5 cdd5ad9cde
CI / ruff · pytest (pull_request) Failing after 2m33s
feat: presentation demo mode — one-click private demo patient, screening on/off switch
- POST /auth/demo/fresh + «Демо: войти как Андрей» on the welcome screen: every sign-in gets a fresh private
  copy of veteran Andrey (consent, prescription, ЕМИАС record) and lands in the chat — no codes, nobody's
  crisis or half-done questionnaire leaks into another viewer.
- Initial screening switch: SCREENING_ENABLED env default + /demo/settings + a toggle in the demo console;
  off → after consent straight to the chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:01:05 +03:00

103 lines
3.6 KiB
Python

"""ЕМИАС medical record mock: diagnoses, comorbidities, allergies and other specialists' prescriptions.
Prod: the regional МИС / ЕГИСЗ adapter replaces `EmiasRecordSource` (same `get(token)` contract). Demo: synthetic
records from `data/emias/records.yaml` keyed by synthetic SNILS, converted to tokens with `pseudo_id()` at load
time — so the record handed to the chat agent carries no SNILS, name or phone (guardrail #1). The current
psychiatric prescription comes from the МИС mock (`integrations.prescriptions`).
"""
import logging
from functools import lru_cache
from pathlib import Path
import yaml
from pydantic import BaseModel, Field
from app.config import get_settings
from app.core.privacy import pseudo_id
from app.integrations.base import integrations
log = logging.getLogger(__name__)
class Diagnosis(BaseModel):
code: str
name: str
class Condition(BaseModel):
name: str
tags: list[str] = Field(default_factory=list)
class OtherMedication(BaseModel):
drug: str
how: str = ""
prescribed_by: str = ""
classes: list[str] = Field(default_factory=list)
class CurrentPrescription(BaseModel):
drug: str
dose: str
instructions: str = ""
class PatientRecord(BaseModel):
"""Clinical-only view of the record (no identifiers): what the chat agent may see."""
diagnoses: list[Diagnosis] = Field(default_factory=list)
conditions: list[Condition] = Field(default_factory=list)
allergies: list[Condition] = Field(default_factory=list)
other_medications: list[OtherMedication] = Field(default_factory=list)
pregnancy: dict[str, str] | None = None
current_prescriptions: list[CurrentPrescription] = Field(default_factory=list)
def default_records_path() -> Path:
return get_settings().data_dir / "emias" / "records.yaml"
@lru_cache
def _load(path: Path) -> dict[str, PatientRecord]:
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
out: dict[str, PatientRecord] = {}
for snils, rec in (raw.get("records") or {}).items():
aliases = rec.pop("tokens", []) if isinstance(rec, dict) else []
record = PatientRecord.model_validate(rec)
out[pseudo_id(snils)] = record
for token in aliases: # demo tokens used by the patient web app (e.g. tok-andrey) share the record
out[token] = record
return out
class EmiasRecordSource:
def __init__(self, path: Path | None = None) -> None:
self._path = path
async def get(self, token: str) -> PatientRecord | None:
try:
records = _load(self._path or default_records_path())
base = records.get(token)
if base is None and "-" in token:
# Fresh demo copies (POST /auth/demo/fresh): "tok-andrey-1a2b3c4d" → the "tok-andrey" record.
base = records.get(token.rsplit("-", 1)[0])
except (OSError, yaml.YAMLError, ValueError) as exc:
log.error("emias records unavailable: %s", type(exc).__name__)
base = None
record = base.model_copy(deep=True) if base is not None else PatientRecord()
try:
rx = await integrations.prescriptions.get_prescriptions(token)
except Exception as exc: # noqa: BLE001 — the МИС being down must not break the chat
log.error("prescriptions unavailable: %s", type(exc).__name__)
rx = []
record.current_prescriptions = [
CurrentPrescription(drug=p.drug, dose=p.dose, instructions=p.instructions) for p in rx
]
if base is None and not rx:
return None
return record
emias_records = EmiasRecordSource()