use super::AnalysisInput; /// System prompt for the consolidation LLM. From docs/projektplan.md:365-369. /// Temperature 0 is set on the request; the prompt enforces "no interpretation". pub const SYSTEM_PROMPT: &str = "Du bist ein medizinischer Assistent.\n Fasse die folgenden diktierten Abschnitte zu einem zusammenhängenden Text zusammen. Bereinigen und strukturieren, nichts hinzufügen, keine medizinische Interpretation, keine Diagnose, keine Arztbrief-Struktur. Entferne Redundanzen. Spätere Aufnahmen haben Vorrang — Korrekturen, Nachträge und Widersprüche zugunsten der chronologisch letzten Aussage auflösen. Antworte auf Deutsch. Nur der zusammengefasste Text, keine Einleitung, kein Abschlusssatz.\n\n WICHTIG: Das Diktat wurde automatisch transkribiert und enthält typische ASR-Fehler wie phonetisch ähnliche Verwechslungen und zerschnittene Komposita (z.B. \"Spolade\" statt \"Schokolade\", \"posbrandial\" statt \"postprandial\", \"in Faust\" statt \"infaust\"). Korrigiere diese Fehler AKTIV. Bei Unsicherheit, wie die Korrektur lauten müsste: behalte das Original bei.\n\n MARKIERUNGEN IM FORMAT \"==text==\" signalisieren dem Arzt \"bitte prüfen\".\n Markiere jede korrigierte oder zweifelhafte Stelle.\n Markiere zusätzlich: Zahlen ohne klaren Kontext oder Einheit.\n Markiere sparsam — geläufige, klar gesprochene Fachbegriffe bleiben unmarkiert.\n WICHTIG: Es sind keine anderen Arten von Annotationen erlaubt (z.B. \"(unsicher)\", \"[TODO]\")."; /// Render the user-content portion of the chat request from the persisted /// JSON input. Recordings with blank text are skipped — they carry no signal /// for the LLM and only add noise. Chronological order is taken as given; /// callers must sort `recordings` before passing in (the handler does). pub fn render_prompt(input: &AnalysisInput) -> String { let mut out = String::new(); let mut first = true; for r in &input.recordings { if r.text.trim().is_empty() { continue; } if !first { out.push_str("\n\n---\n\n"); } first = false; out.push_str("## "); out.push_str(&r.recorded_at); out.push_str("\n\n"); out.push_str(r.text.trim()); } out } #[cfg(test)] mod tests { use super::*; use crate::analyze::RecordingInput; fn mk(input: Vec<(&str, &str)>) -> AnalysisInput { AnalysisInput { last_recording_mtime: "2026-04-15T10:47:00Z".into(), recordings: input .into_iter() .map(|(ts, text)| RecordingInput { recorded_at: ts.into(), text: text.into(), }) .collect(), } } #[test] fn renders_single_recording() { let out = render_prompt(&mk(vec![( "2026-04-15T10:32:00Z", "Patient klagt über Knie.", )])); assert_eq!(out, "## 2026-04-15T10:32:00Z\n\nPatient klagt über Knie."); } #[test] fn renders_multiple_with_separator() { let out = render_prompt(&mk(vec![ ("2026-04-15T10:32:00Z", "Erste Aufnahme."), ("2026-04-15T10:38:15Z", "Zweite Aufnahme."), ])); assert!(out.contains("\n\n---\n\n")); assert!(out.starts_with("## 2026-04-15T10:32:00Z")); assert!(out.contains("## 2026-04-15T10:38:15Z")); } #[test] fn skips_blank_recordings() { let out = render_prompt(&mk(vec![ ("2026-04-15T10:32:00Z", "Nur Inhalt."), ("2026-04-15T10:33:00Z", " "), ("2026-04-15T10:34:00Z", ""), ])); assert_eq!(out.matches("---").count(), 0); assert!(!out.contains("10:33:00")); assert!(!out.contains("10:34:00")); } #[test] fn handles_empty_input() { assert_eq!(render_prompt(&mk(vec![])), ""); } #[test] fn trims_per_recording_text() { let out = render_prompt(&mk(vec![("2026-04-15T10:32:00Z", " hallo\n\n")])); assert!(out.ends_with("hallo")); } }