Files
doctate/server/src/analyze/prompt.rs
T
Brummel 421fbb3e46 Update system prompt for medical assistant
The system prompt for the medical assistant LLM has been updated to
improve clarity and explicitly state the desired formatting for
corrections and uncertainties. This includes:

- Consolidating similar correction examples.
- Specifying that only "==text==" annotations are allowed for
  corrections and uncertainties.
- Explicitly disallowing other annotation formats like "(unsicher)" or
  "[TODO]".
2026-04-17 11:27:19 +02:00

98 lines
4.0 KiB
Rust

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"));
}
}