AidAILine / doc_forms.py
J-Barrert's picture
Upload 14 files
585cfd5 verified
Raw
History Blame Contribute Delete
20.1 kB
"""
doc_forms.py β€” Doctor visit template generator.
Pulls current medications and allergies from their respective trackers,
combines with the active profile from the multi-profile engine, and
generates a formatted visit summary suitable for printing or sharing.
"""
from __future__ import annotations
from datetime import datetime
from typing import Iterable, List, Optional, Tuple
from profiles import get_profile, get_profiles, compose_address, address_parts
from med_tracker import get_medications
from food_tracker import get_food
# ── Profile resolution ───────────────────────────────────────────────────────
def _resolve_profile(profile_id: str = "") -> Tuple[dict, str]:
"""Resolve the active profile dict from the multi-profile engine.
Returns (profile_dict, note). The note is a non-empty warning string
when the resolver had to fall back (empty id, id not found, or no
profiles exist). Caller should render the note so the user knows
the brief is incomplete.
Fallback order:
1. profile_id (if provided and found)
2. first available profile
3. empty dict (no profiles exist)
"""
if profile_id:
p = get_profile(profile_id)
if p:
return p, ""
all_p = get_profiles() or []
if all_p:
first = all_p[0]
label = first.get("label") or first.get("full_name") or "first available"
return first, f"⚠️ No profile selected β€” using '{label}'."
return {}, "⚠️ No profiles exist. Create one in βš™οΈ Settings & Profiles."
def _resolve_emergency_contacts(profile: dict) -> list[dict]:
"""Return the list of emergency contacts to render in the brief.
Prefers the new `emergency_contacts` list. Falls back to the legacy
single-contact fields if the list is empty (so old profiles still
render correctly). Filters out entries that have neither a name
nor a phone.
"""
raw = profile.get("emergency_contacts") or []
if isinstance(raw, list) and raw:
return [c for c in raw
if isinstance(c, dict) and (c.get("name") or c.get("phone"))]
# Legacy single-contact fallback
name = (profile.get("emergency_contact_name") or "").strip()
phone = (profile.get("emergency_contact_phone") or "").strip()
if name or phone:
return [{"name": name, "phone": phone, "relationship": ""}]
return []
# ── Template renderer ─────────────────────────────────────────────────────────
def generate_visit_template(reason_for_visit: str = "", profile_id: str = "") -> str:
"""
Generate a formatted doctor-visit summary as plain text.
reason_for_visit: Free text entered by the caregiver for this visit.
profile_id: UUID of the active patient profile. Falls back to the
first available profile if empty/invalid.
Returns a string suitable for display in a Gradio Textbox or export.
"""
p, note = _resolve_profile(profile_id)
meds = get_medications(filter="current", profile_id=profile_id or None)
food_data = get_food(profile_id=profile_id or None)
allergies = [e["name"] for e in food_data.get("allergies", [])]
today = datetime.now().strftime("%B %d, %Y")
lines = []
# ── Header ──────────────────────────────────────────────────────────────
lines.append("=" * 60)
lines.append(" PRE-VISIT FORM")
lines.append("=" * 60)
lines.append(f"Date: {today}")
if note:
lines.append(note)
lines.append("")
# ── Patient Information (from active profile) ────────────────────────────
lines.append("PATIENT INFORMATION")
lines.append("-" * 40)
lines.append(f"Name: {p.get('full_name') or '(Not set β€” add in Settings & Profiles)'}")
lines.append(f"Date of Birth: {p.get('dob') or '(Not set β€” add in Settings & Profiles)'}")
_patient_addr = compose_address(*address_parts(p)) or "(Not set β€” add in Settings & Profiles)"
lines.append(f"Address: {_patient_addr.replace(chr(10), ', ')}")
lines.append(f"Phone: {p.get('phone_number') or '(Not set β€” add in Settings & Profiles)'}")
lines.append(f"Email: {p.get('email') or '(Not set β€” add in Settings & Profiles)'}")
# Insurance composite
ins_provider = p.get("insurance_provider") or ""
ins_policy = p.get("policy_id") or ""
ins_group = p.get("group_id") or ""
if ins_provider or ins_policy or ins_group:
ins_line = f"Insurance: {ins_provider or '(provider not set)'}"
if ins_policy or ins_group:
ins_line += f" [Policy: {ins_policy or 'β€”'} / Group: {ins_group or 'β€”'}]"
lines.append(ins_line)
else:
lines.append("Insurance: (Not set β€” add in Settings & Profiles)")
lines.append("")
# ── Primary Care Provider ────────────────────────────────────────────────
lines.append("PRIMARY CARE PROVIDER")
lines.append("-" * 40)
if p.get("pcp_name") or p.get("pcp_clinic"):
lines.append(f"Provider: {p.get('pcp_name') or '(Not set)'}")
lines.append(f"Clinic: {p.get('pcp_clinic') or '(Not set)'}")
_pcp_addr = compose_address(*address_parts(p, pcp=True)) or "(Not set)"
lines.append(f"Address: {_pcp_addr.replace(chr(10), ', ')}")
lines.append(f"Phone: {p.get('pcp_phone') or '(Not set)'}")
else:
lines.append("(Not set β€” add in Settings & Profiles)")
lines.append("")
# ── Emergency Contact(s) ────────────────────────────────────────────────
lines.append("EMERGENCY CONTACT(S)")
lines.append("-" * 40)
contacts = _resolve_emergency_contacts(p)
if contacts:
for i, c in enumerate(contacts, start=1):
rel = c.get("relationship") or ""
rel_suffix = f" ({rel})" if rel else ""
lines.append(f"Contact {i}: {c.get('name') or '(name not set)'}{rel_suffix}")
lines.append(f"Phone: {c.get('phone') or '(phone not set)'}")
if i < len(contacts):
lines.append("")
else:
lines.append("(Not set β€” add in Settings & Profiles)")
lines.append("")
# ── Reason for Visit ─────────────────────────────────────────────────────
lines.append("REASON FOR VISIT")
lines.append("-" * 40)
if reason_for_visit.strip():
lines.append(reason_for_visit.strip())
else:
lines.append("(Enter reason for visit in the field above)")
lines.append("")
# ── Current Medications ───────────────────────────────────────────────────
lines.append("CURRENT MEDICATIONS")
lines.append("-" * 40)
if meds:
for med in meds:
med_line = f"β€’ {med['name']}"
if med.get("dosage"):
med_line += f" β€” {med['dosage']}"
if med.get("frequency"):
med_line += f" ({med['frequency']})"
lines.append(med_line)
if med.get("side_effects"):
lines.append(f" Side effects: {med['side_effects']}")
if med.get("personal_notes"):
lines.append(f" Notes: {med['personal_notes']}")
else:
lines.append("(No current medications on file)")
lines.append("")
# ── Known Allergies ───────────────────────────────────────────────────────
lines.append("KNOWN ALLERGIES")
lines.append("-" * 40)
if allergies:
for allergy in allergies:
lines.append(f"⚠ {allergy}")
else:
lines.append("(No known allergies on file)")
lines.append("")
# ── Doctor's Notes ────────────────────────────────────────────────────────
lines.append("DOCTOR'S NOTES")
lines.append("-" * 40)
lines.append("")
lines.append("")
lines.append("")
lines.append("")
lines.append("(space for handwritten or dictated notes)")
lines.append("")
# ── Follow-Up ─────────────────────────────────────────────────────────────
lines.append("FOLLOW-UP / NEXT STEPS")
lines.append("-" * 40)
lines.append("")
lines.append("")
lines.append("")
lines.append("=" * 60)
lines.append("Generated by AidAiLine")
lines.append("Build Small Hackathon β€” Backyard AI Track")
lines.append("=" * 60)
return "\n".join(lines)
def generate_visit_brief_markdown(
profile_id: str = "",
clinic_doctor: str = "",
reason: str = "",
concerns: str = "",
symptoms: Optional[Iterable[str]] = None,
symptom_other: str = "",
symptom_notes: str = "",
questions: Optional[Iterable[str]] = None,
inc_meds: bool = True,
inc_allergies: bool = True,
inc_history: bool = True,
inc_insurance: bool = True,
) -> str:
"""
Compile a professional markdown pre-visit brief from form inputs +
auto-pulled data from the active profile (multi-profile engine) and
the medications/food trackers.
profile_id: UUID of the active patient profile. Falls back to the
first available profile if empty/invalid.
Care Mode and Caregiver Name are read from the profile itself
(moved off the appointment form so they belong to the
patient, not the visit).
symptoms: list of checked symptoms (may contain "Other")
symptom_other: free-text companion when "Other" is checked
questions: list of question strings (one per bullet)
inc_insurance: when False, hides the Insurance row in Demographics
(other Patient Demographics fields still render)
"""
symptoms = list(symptoms or [])
questions = list(questions or [])
p, profile_note = _resolve_profile(profile_id)
care_mode = p.get("care_mode", "Managing someone else's care") or "Managing someone else's care"
caregiver_name = p.get("caregiver_name", "") or ""
is_self_care = care_mode == "Managing my own care"
today = datetime.now().strftime("%B %d, %Y")
L: List[str] = []
# ── Header ────────────────────────────────────────────────────────────
L.append("# πŸ₯ Patient Pre-Visit Intake Brief")
L.append("")
L.append(f"**Date:** {today}")
L.append("")
if profile_note:
L.append(f"_{profile_note}_")
L.append("")
L.append("---")
L.append("")
# ── Patient Demographics (from active profile) ────────────────────────
L.append("## πŸ‘€ Patient Demographics")
L.append("")
L.append(f"- **Legal Name:** {p.get('full_name') or '_(Not set β€” add in Settings & Profiles)_'}")
L.append(f"- **Date of Birth:** {p.get('dob') or '_(Not set β€” add in Settings & Profiles)_'}")
_patient_addr = compose_address(*address_parts(p)) or "_(Not set β€” add in Settings & Profiles)_"
L.append(f"- **Address:** {_patient_addr.replace(chr(10), ', ')}")
L.append(f"- **Phone:** {p.get('phone_number') or '_(Not set β€” add in Settings & Profiles)_'}")
L.append(f"- **Email:** {p.get('email') or '_(Not set β€” add in Settings & Profiles)_'}")
if inc_insurance:
ins_provider = p.get("insurance_provider") or ""
ins_policy = p.get("policy_id") or ""
ins_group = p.get("group_id") or ""
if ins_provider or ins_policy or ins_group:
L.append(f"- **Insurance:** {ins_provider or '_(provider not set)_'}")
if ins_policy or ins_group:
L.append(f" - Policy #: {ins_policy or '_(not set)_'}")
L.append(f" - Group #: {ins_group or '_(not set)_'}")
else:
L.append("- **Insurance:** _(Not set β€” add in Settings & Profiles)_")
L.append("")
# ── Primary Care Provider ────────────────────────────────────────────
L.append("## 🩺 Primary Care Provider")
L.append("")
if p.get("pcp_name") or p.get("pcp_clinic"):
L.append(f"- **Provider:** {p.get('pcp_name') or '_(Not set)_'}")
L.append(f"- **Clinic:** {p.get('pcp_clinic') or '_(Not set)_'}")
_pcp_addr = compose_address(*address_parts(p, pcp=True)) or "_(Not set)_"
L.append(f"- **Address:** {_pcp_addr.replace(chr(10), ', ')}")
L.append(f"- **Phone:** {p.get('pcp_phone') or '_(Not set)_'}")
else:
L.append("_(Not set β€” add in Settings & Profiles)_")
L.append("")
# ── Emergency Contact(s) ────────────────────────────────────────────────
L.append("## πŸ†˜ Emergency Contact(s)")
L.append("")
contacts = _resolve_emergency_contacts(p)
if contacts:
for i, c in enumerate(contacts, start=1):
rel = c.get("relationship") or ""
rel_suffix = f" *({rel})*" if rel else ""
L.append(f"- **Contact {i} β€” {c.get('name') or '_(name not set)_'}**{rel_suffix}")
L.append(f" - Phone: {c.get('phone') or '_(phone not set)_'}")
if i < len(contacts):
L.append("")
else:
L.append("_(Not set β€” add in Settings & Profiles)_")
L.append("")
# ── Caregiver + Targeted Clinic (visit meta) ─────────────────────────
L.append("## πŸ§‘β€βš•οΈ Visit Meta")
L.append("")
if not is_self_care:
L.append(f"- **Caregiver:** {caregiver_name.strip() or '_(Not provided)_'}")
L.append(f"- **Targeted Clinic / Doctor:** {clinic_doctor.strip() or '_(Not specified)_'}")
L.append("")
L.append("---")
L.append("")
# ── Section A: Clinical Target ───────────────────────────────────────
L.append("## 🎯 Section A: Clinical Target")
L.append("")
L.append("> **Reason for Visit:** " + (reason or "_(Not specified)_"))
L.append(">")
if concerns.strip():
for line in concerns.splitlines():
line = line.strip()
if line:
L.append(f"> β€’ {line}")
L.append("")
L.append("---")
L.append("")
# ── Section B: Current Care Summary ──────────────────────────────────
L.append("## πŸ’Š Section B: Current Care Summary")
L.append("")
if inc_meds:
meds = get_medications(filter="current", profile_id=profile_id or None)
L.append("### Current Medications")
L.append("")
if meds:
for m in meds:
line = f"- **{m['name']}**"
if m.get("dosage"):
line += f" β€” {m['dosage']}"
if m.get("frequency"):
line += f" *({m['frequency']})*"
L.append(line)
if m.get("side_effects"):
L.append(f" - Side effects: {m['side_effects']}")
if m.get("personal_notes"):
L.append(f" - Notes: {m['personal_notes']}")
else:
L.append("_(No current medications on file)_")
L.append("")
if inc_allergies:
food_data = get_food(profile_id=profile_id or None)
allergies = food_data.get("allergies", [])
aversions = food_data.get("disliked_foods", [])
L.append("### Allergies & Food Aversions")
L.append("")
if allergies or aversions:
for a in allergies:
sev = a.get("severity", "β€”")
L.append(f"- ⚠️ **{a['name']}** β€” severity: `{sev}`")
for a in aversions:
L.append(f"- ❌ {a['name']}")
else:
L.append("_(No known allergies or aversions on file)_")
L.append("")
if inc_history:
L.append("### Recent Vitals / History")
L.append("")
L.append("_For full lab results and clinical notes, see the **πŸ“„ Documents** tab._")
L.append("")
L.append("---")
L.append("")
# ── Section C: Observations ─────────────────────────────────────────
section_c_title = (
"## πŸ‘€ Section C: Your Observations"
if is_self_care
else "## πŸ‘€ Section C: Caregiver Observations"
)
L.append(section_c_title)
L.append("")
if symptoms or symptom_other.strip():
L.append("### Symptom Tracker (checked)")
L.append("")
for s in symptoms:
if s == "Other":
if symptom_other.strip():
L.append(f"- ☐ **Other:** {symptom_other.strip()}")
else:
L.append(f"- ☐ Other")
else:
L.append(f"- ☐ {s}")
L.append("")
else:
L.append("### Symptom Tracker")
L.append("")
L.append("_(No symptoms flagged this visit)_")
L.append("")
if symptom_notes.strip():
L.append("### Symptom Notes (severity / duration / triggers)")
L.append("")
for line in symptom_notes.splitlines():
line = line.strip()
if line:
L.append(f"> {line}")
L.append("")
if questions:
L.append("### ❓ Questions for the Doctor")
L.append("")
for i, q in enumerate(questions, start=1):
L.append(f"{i}. {q}")
L.append("")
L.append("---")
L.append("")
L.append("_Generated by AidAiLine β€” Build Small Hackathon._")
return "\n".join(L)
def generate_visit_template_html(reason_for_visit: str = "", profile_id: str = "") -> str:
"""
Generate an HTML version of the visit template for print/preview.
Wraps the plain-text version in print-friendly HTML.
"""
plain = generate_visit_template(reason_for_visit, profile_id=profile_id)
escaped = (
plain
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pre-Visit Form</title>
<style>
body {{
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #1a202c;
max-width: 700px;
margin: 40px auto;
padding: 0 20px;
}}
pre {{
white-space: pre-wrap;
word-wrap: break-word;
font-family: inherit;
}}
@media print {{
body {{ margin: 20px; }}
}}
</style>
</head>
<body>
<pre>{escaped}</pre>
<script>
// Auto-print if opened in print mode
if (window.location.search.includes('print=1')) {{
window.print();
}}
</script>
</body>
</html>"""