Cardiosense-AG's picture
Update src/store.py
97f9c3e verified
raw
history blame
8.73 kB
# src/store.py
# -----------------------------------------------------------------------------
# Case persistence + seed data for Phase-3+ Simplified Build
# Compatible with paths.py where cases_dir may be a function or Path variable
# -----------------------------------------------------------------------------
from __future__ import annotations
import json
import shutil
from pathlib import Path
from typing import Any, Dict, List, Optional
from . import paths # import module (not paths_dict)
# ------------------------------ Core I/O -------------------------------------
def _cases_dir() -> Path:
"""Return path to the cases directory defined in paths.py (function or var)."""
d = getattr(paths, "CASES_DIR", None) or getattr(paths, "cases_dir", None)
# If callable (e.g., def cases_dir(): ...), invoke it
if callable(d):
d = d()
# Fallback default
if d is None:
d = "data/cases"
p = Path(d)
p.mkdir(parents=True, exist_ok=True)
return p
def _case_path(case_id: str) -> Path:
return _cases_dir() / f"{case_id}.json"
def list_cases() -> List[Dict[str, Any]]:
"""List all cases with basic metadata for selector."""
out: List[Dict[str, Any]] = []
for f in sorted(_cases_dir().glob("*.json")):
try:
data = json.loads(f.read_text(encoding="utf-8"))
out.append({
"case_id": data.get("case_id"),
"patient_name": data.get("patient", {}).get("name", ""),
"status": data.get("status", "draft"),
"specialty": data.get("consult", {}).get("specialty", ""),
})
except Exception:
continue
return out
def read_case(case_id: str) -> Optional[Dict[str, Any]]:
p = _case_path(case_id)
if not p.exists():
return None
try:
return json.loads(p.read_text(encoding="utf-8"))
except Exception:
return None
def write_case(case: Dict[str, Any]) -> None:
"""Persist one case dict to JSON (overwrite safe)."""
cid = case.get("case_id")
if not cid:
raise ValueError("Case missing 'case_id'")
p = _case_path(cid)
p.write_text(json.dumps(case, indent=2, ensure_ascii=False), encoding="utf-8")
def update_case(case_id: str, patch: Dict[str, Any]) -> None:
"""Merge patch into existing case and save."""
existing = read_case(case_id) or {}
for k, v in patch.items():
if isinstance(v, dict) and isinstance(existing.get(k), dict):
existing[k].update(v)
else:
existing[k] = v
write_case(existing)
def set_status(case_id: str, status: str) -> None:
c = read_case(case_id)
if not c:
return
c["status"] = status
write_case(c)
def new_case_id() -> str:
"""Generate next incremental EC-### style ID."""
existing = [f.stem for f in _cases_dir().glob("*.json") if f.stem.startswith("EC-")]
nums = [int(x.split("-")[1]) for x in existing if len(x.split("-")) > 1 and x.split("-")[1].isdigit()]
nxt = max(nums) + 1 if nums else 3
return f"EC-{nxt:03d}"
# ------------------------------ Seeding --------------------------------------
def seed_cases(reset: bool = False) -> None:
"""
Create baseline demo cases (clears directory if reset=True).
Adds 3 extra PCP draft cardiology cases for heart failure.
"""
cases_dir = _cases_dir()
if reset:
shutil.rmtree(cases_dir, ignore_errors=True)
cases_dir.mkdir(parents=True, exist_ok=True)
base_cases = [
{
"case_id": "EC-001",
"status": "draft",
"patient": {"name": "John Doe", "age": 68, "sex": "M"},
"consult": {
"specialty": "Cardiology",
"question": "Medication optimization for stable ischemic heart disease.",
"summary": "Stable angina and hypertension. On beta-blocker and ACE inhibitor.",
"medications": "Metoprolol, Lisinopril, ASA 81 mg.",
"labs": "Cr 1.0 mg/dL, eGFR 70, LDL 88.",
"consent_obtained": True,
},
"soap_draft": {"subjective": "", "objective": "", "assessment": "", "plan": ""},
"billing": {"minutes": 5, "spoke": False, "cpt_code": None, "attested": False},
"explainability": {"baseline": {"assessment_hash": "", "plan_hash": ""}, "guideline_rationale": []},
},
{
"case_id": "EC-002",
"status": "draft",
"patient": {"name": "Jane Smith", "age": 72, "sex": "F"},
"consult": {
"specialty": "Endocrinology",
"question": "Diabetes control on basal/bolus insulin.",
"summary": "A1c 8.2%. Recurrent nocturnal hypoglycemia.",
"medications": "Insulin glargine 20 U HS, Lispro with meals.",
"labs": "Cr 0.9 mg/dL, eGFR 75.",
"consent_obtained": True,
},
"soap_draft": {"subjective": "", "objective": "", "assessment": "", "plan": ""},
"billing": {"minutes": 5, "spoke": False, "cpt_code": None, "attested": False},
"explainability": {"baseline": {"assessment_hash": "", "plan_hash": ""}, "guideline_rationale": []},
},
]
hf_cases = [
{
"case_id": "HF-101",
"status": "draft",
"patient": {"name": "Robert Miller", "age": 74, "sex": "M"},
"consult": {
"specialty": "Cardiology",
"question": "Optimize GDMT for HFrEF (EF 30%).",
"summary": (
"Stable NYHA III HFrEF (EF 30%) post-MI. BP 110/70 mmHg, HR 68. "
"Mild exertional dyspnea, no edema. On carvedilol 12.5 mg BID, "
"lisinopril 20 mg QD, furosemide 20 mg PRN."
),
"medications": "Carvedilol 12.5 mg BID, Lisinopril 20 mg QD, Furosemide 20 mg PRN.",
"labs": "Na 138, K 4.6, Cr 1.1, BNP 420 pg/mL.",
"consent_obtained": True,
},
"soap_draft": {"subjective": "", "objective": "", "assessment": "", "plan": ""},
"billing": {"minutes": 5, "spoke": False, "cpt_code": None, "attested": False},
"explainability": {"baseline": {"assessment_hash": "", "plan_hash": ""}, "guideline_rationale": []},
},
{
"case_id": "HF-102",
"status": "draft",
"patient": {"name": "Linda Chen", "age": 69, "sex": "F"},
"consult": {
"specialty": "Cardiology",
"question": "Evaluate possible HFpEF with exertional dyspnea.",
"summary": (
"Progressive exertional SOB, preserved EF 60%. Mild concentric LVH on echo. "
"BNP 180 pg/mL. Considering SGLT2 inhibitor initiation."
),
"medications": "Amlodipine 5 mg QD, HCTZ 12.5 mg QD.",
"labs": "Na 140, K 4.2, Cr 0.9, BNP 180 pg/mL.",
"consent_obtained": True,
},
"soap_draft": {"subjective": "", "objective": "", "assessment": "", "plan": ""},
"billing": {"minutes": 5, "spoke": False, "cpt_code": None, "attested": False},
"explainability": {"baseline": {"assessment_hash": "", "plan_hash": ""}, "guideline_rationale": []},
},
{
"case_id": "HF-103",
"status": "draft",
"patient": {"name": "Carlos Ramirez", "age": 81, "sex": "M"},
"consult": {
"specialty": "Cardiology",
"question": "Diuretic titration for volume overload in chronic HFrEF.",
"summary": (
"Chronic HFrEF (EF 25%) with mild lower-extremity edema and 3 lb weight gain. "
"Currently on furosemide 40 mg QD and spironolactone 25 mg QD."
),
"medications": "Furosemide 40 mg QD, Spironolactone 25 mg QD, Metoprolol 50 mg BID.",
"labs": "Na 136, K 4.8, Cr 1.4, BNP 520 pg/mL.",
"consent_obtained": True,
},
"soap_draft": {"subjective": "", "objective": "", "assessment": "", "plan": ""},
"billing": {"minutes": 5, "spoke": False, "cpt_code": None, "attested": False},
"explainability": {"baseline": {"assessment_hash": "", "plan_hash": ""}, "guideline_rationale": []},
},
]
all_cases = base_cases + hf_cases
for c in all_cases:
path = _case_path(c["case_id"])
if not path.exists():
path.write_text(json.dumps(c, indent=2, ensure_ascii=False), encoding="utf-8")