Spaces:
Sleeping
Sleeping
| """ | |
| 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("&", "&") | |
| .replace("<", "<") | |
| .replace(">", ">") | |
| ) | |
| 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>""" | |