| """Mock Sprechstundenplan and prompts for Praxis-Briefing.""" |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
|
|
|
|
| @dataclass(frozen=True) |
| class BriefingPatient: |
| """One demo appointment on today's schedule.""" |
|
|
| time_label: str |
| name: str |
| age: int |
| reason: str |
|
|
|
|
| @dataclass(frozen=True) |
| class DemoQuestion: |
| """Suggested voice question for the GP demo.""" |
|
|
| prompt: str |
| log_label: str |
|
|
|
|
| TODAY_LABEL: str = "Mittwoch, 3. Juni 2026" |
|
|
| SCHEDULE: tuple[BriefingPatient, ...] = ( |
| BriefingPatient("08:30", "Laura Müller", 34, "Halsschmerzen seit drei Tagen"), |
| BriefingPatient("09:00", "Thomas Schmidt", 58, "Blutdruck-Kontrolle"), |
| BriefingPatient("09:30", "Ayşe Yilmaz", 72, "Knieschmerzen rechts"), |
| BriefingPatient("10:30", "Jonas Weber", 19, "Attest für Sport"), |
| ) |
|
|
| DEMO_QUESTIONS: tuple[DemoQuestion, ...] = ( |
| DemoQuestion("Wer ist mein nächster Patient?", "Nächster Patient"), |
| DemoQuestion("Wie alt ist Frau Müller?", "Alter Müller"), |
| DemoQuestion("Was ist das Anliegen von Herrn Schmidt?", "Anliegen Schmidt"), |
| DemoQuestion("Wann kommt Jonas Weber?", "Termin Weber"), |
| ) |
|
|
| BASE_SYSTEM_PROMPT: str = ( |
| "Du bist der Sprach-Assistent einer Hausarztpraxis. " |
| "Du hast den heutigen Terminplan vorliegen und vollen Zugriff darauf. " |
| "Antworte immer kurz auf Deutsch, in genau einem Satz, " |
| "nur mit Informationen aus dem Plan. " |
| "Sage niemals, dass du keinen Zugriff oder keine Liste hast." |
| ) |
|
|
|
|
| def format_schedule_markdown() -> str: |
| """Render the mock day list for the setup tab.""" |
| lines: list[str] = [ |
| f"### Sprechstundenplan — {TODAY_LABEL}", |
| "", |
| "| Zeit | Patientin / Patient | Alter | Anliegen |", |
| "| --- | --- | ---: | --- |", |
| ] |
| for slot in SCHEDULE: |
| lines.append( |
| f"| {slot.time_label} | {slot.name} | {slot.age} | {slot.reason} |" |
| ) |
| lines.extend( |
| [ |
| "", |
| "_Demo-Daten, keine echten Patienten. Kein Zugriff auf ein Praxissystem._", |
| ] |
| ) |
| return "\n".join(lines) |
|
|
|
|
| def format_schedule_plain() -> str: |
| """Compact schedule string for the system prompt.""" |
| parts: list[str] = [] |
| for slot in SCHEDULE: |
| parts.append( |
| f"{slot.time_label} {slot.name}, {slot.age}, {slot.reason}." |
| ) |
| return " ".join(parts) |
|
|
|
|
| def format_demo_questions_markdown() -> str: |
| """Show example questions the GP can ask by voice.""" |
| lines: list[str] = ["**Beispielfragen (frei formulierbar):**", ""] |
| for question in DEMO_QUESTIONS: |
| lines.append(f"- „{question.prompt}“") |
| return "\n".join(lines) |
|
|
|
|
| def format_schedule_html() -> str: |
| """Render today's schedule as a clean clinical table panel.""" |
| rows: list[str] = [] |
| for slot in SCHEDULE: |
| rows.append( |
| "<tr>" |
| f"<td class='sc-time'>{slot.time_label}</td>" |
| f"<td class='sc-name'>{slot.name}</td>" |
| f"<td class='sc-age'>{slot.age}</td>" |
| f"<td class='sc-reason'>{slot.reason}</td>" |
| "</tr>" |
| ) |
| return ( |
| "<section class='panel'>" |
| "<header class='panel-head'>Sprechstundenplan" |
| f"<span class='panel-meta'>{TODAY_LABEL}</span></header>" |
| "<table class='sched'>" |
| "<thead><tr><th>Zeit</th><th>Patient/in</th><th>Alter</th><th>Anliegen</th></tr></thead>" |
| f"<tbody>{''.join(rows)}</tbody>" |
| "</table>" |
| "<footer class='panel-note'>Demo-Daten · keine echten Patienten · " |
| "kein Zugriff auf ein Praxissystem</footer>" |
| "</section>" |
| ) |
|
|
|
|
| def format_demo_questions_html() -> str: |
| """Render example voice questions as calm outline chips.""" |
| chips: list[str] = [ |
| f"<span class='chip'>{question.prompt}</span>" for question in DEMO_QUESTIONS |
| ] |
| return ( |
| "<div class='ask'>" |
| "<div class='ask-label'>Beispielfragen</div>" |
| f"<div class='chips'>{''.join(chips)}</div>" |
| "</div>" |
| ) |
|
|
|
|
| def format_hero_html() -> str: |
| """EHR-style top bar: clinic mark, app name, today's date.""" |
| return ( |
| "<div class='topbar'>" |
| "<div class='brand'>" |
| "<span class='brand-logo' aria-hidden='true'>+</span>" |
| "<span class='brand-text'>" |
| "<span class='brand-name'>Praxis-Briefing</span>" |
| "<span class='brand-sub'>Sprechstunden-Assistent</span>" |
| "</span>" |
| "</div>" |
| f"<div class='top-meta'>{TODAY_LABEL}<span class='top-meta-tag'>Sprechstunde</span></div>" |
| "</div>" |
| ) |
|
|
|
|
| FEWSHOT_EXAMPLES: tuple[tuple[str, str], ...] = ( |
| ( |
| "Wer ist mein nächster Patient?", |
| "Ihr erster Termin ist um 08:30 Laura Müller, 34, mit Halsschmerzen.", |
| ), |
| ( |
| "Was ist das Anliegen von Herrn Schmidt?", |
| "Herr Schmidt kommt um 09:00 zur Blutdruck-Kontrolle.", |
| ), |
| ( |
| "Wann kommt Frau Yilmaz?", |
| "Frau Yilmaz kommt um 09:30 wegen Knieschmerzen rechts.", |
| ), |
| ( |
| "Wie alt ist Jonas Weber?", |
| "Jonas Weber ist 19 Jahre alt und kommt um 10:30 für ein Attest.", |
| ), |
| ) |
|
|
|
|
| def build_system_prompt() -> str: |
| """Behavioral prompt plus the full schedule as context.""" |
| return f"{BASE_SYSTEM_PROMPT} Heutiger Terminplan: {format_schedule_plain()}" |
|
|
|
|
| def build_user_grounding() -> str: |
| """Short lead-in before each spoken question (plan already in context).""" |
| return "Beantworte meine folgende gesprochene Frage kurz und nur aus dem heutigen Terminplan." |
|
|
|
|
| def classify_reply(assistant_reply: str) -> str: |
| """Coarse log tag for the session table.""" |
| normalized: str = assistant_reply.lower() |
| if "nichts im heutigen plan" in normalized or "praxissystem" in normalized: |
| return "nicht_im_plan" |
| return "aus_plan" |
|
|