Refactor ollama transcription prompts

The system prompt for the transcription Ollama LLM has been updated to
improve robustness and clarity, especially for handling empty or
non-medical transcripts.

The prompt has been refactored to:
- Use English for instructions to improve robustness with smaller LLMs.
- Clearly define the behavior for empty or non-medical transcripts,
  requiring an empty string response.
- Reduce the maximum character limit from 120 to 60 to better fit
  smartwatch displays.
- Ensure the output is German, adhering to the original language
  requirement for the output.
- Add an explicit instruction to correct transcription errors in the
  input dictation for the consolidation LLM.
This commit is contained in:
2026-04-16 15:30:45 +02:00
parent b1a8186fcd
commit 44ed5e8333
2 changed files with 11 additions and 9 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ 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. Fasse die folgenden diktierten Abschnitte zu einem zusammenhängenden Text zusammen. Bereinigen und strukturieren, nichts hinzufügen, nichts interpretieren, 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.";
pub const SYSTEM_PROMPT: &str = "Du bist ein medizinischer Assistent. Fasse die folgenden diktierten Abschnitte zu einem zusammenhängenden Text zusammen. Bereinigen und strukturieren, nichts hinzufügen, nichts interpretieren, 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. WICHTIG: das Diktat wurde automatisch transkribiert, es kann entsprechende Fehler enthalten, die du korrigieren musst.";
/// Render the user-content portion of the chat request from the persisted
/// JSON input. Recordings with blank text are skipped — they carry no signal
+10 -8
View File
@@ -3,9 +3,11 @@ use std::time::Duration;
use serde::{Deserialize, Serialize};
use tracing::debug;
/// Smartwatch-aware system prompt: leading word must be the diagnosis/region
/// so the doctor can distinguish cases from the watch face.
const SYSTEM_PROMPT: &str = "Du bist ein Assistent für Ärzte. Extrahiere aus dem folgenden deutschen medizinischen Diktat prägnante Schlüsselwörter in einer einzigen Zeile (so kurz wie möglich, idealerweise 36 Wörter, maximal 60 Zeichen).\n\nDer Oneliner wird auf einem winzigen Smartwatch-Display angezeigt. Nur die ersten Zeichen sind sicher lesbar, deshalb zählt das ERSTE WORT am meisten.\n\nRegeln:\n- Beginne mit dem wichtigsten Begriff: Körperregion, Organ oder markanter Befund (z.B. \"Kniegelenk\", \"Herzkatheter\", \"HOCM\", \"Thorax-CT\").\n- KEINE Füllwörter am Anfang: kein \"Patient\", \"Fall\", \"Verdacht auf\", \"Untersuchung von\", \"Diktat zu\", \"Zusammenfassung\", Artikel.\n- KEINE wertenden Adjektive wie \"massiv\", \"ausgeprägt\", \"deutlich\", \"hochgradig\" — nur nackte Begriffe.\n- Nenne Schlüsselwörter, an denen der Arzt den Fall wiedererkennt. Keine medizinische Interpretation, keine Therapievorschläge, keine Nebenbefunde.\n- Keine Anführungszeichen, kein Punkt am Ende, keine Einleitung.\n- Wenn der Arzt im Diktat eine eigene Bezeichnung nennt (z.B. \"Bezeichnung: …\" oder \"Fall-ID: …\"), übernimm GENAU diese als Anfang.\n- Nur die eine Zeile, nichts davor oder danach.\n\nBeispiele:\nDiktat: \"Also, Patient kommt mit Schmerzen im rechten Knie, V.a. Meniskusriss...\"\n→ Knie re., V.a. Meniskusriss\n\nDiktat: \"Bezeichnung: Schulter links. Impingement-Syndrom...\"\n→ Schulter links, Impingement\n\nDiktat: \"...HOCM mit ausgeprägter Septumhypertrophie, SAM-Phänomen, Valsalva-Manöver zeigt LVOT-Gradient...\"\n→ HOCM, Septumhypertrophie, SAM-Phänomen. Wenn du kein Schlüsselwort findest, gib einen LEEREN String zurück.";
/// Smartwatch-aware keyword-extraction prompt: leading noun must come from
/// the transcript itself (no hallucination) so the doctor can recognize the
/// case from the watch face. English instructions for small-model
/// robustness, German output.
const SYSTEM_PROMPT: &str = "You are a keyword-extraction assistant for a German medical dictation app. Given a transcript, output a short German one-line label for the case.\n\nCRITICAL — empty-string rule:\nIf the transcript is empty, silent, or contains no identifiable medical keyword, respond with an EMPTY STRING. Do not guess, do not produce filler, do not write \"Unbekannt\" or \"Kein Befund\". An empty answer is correct.\n\nRules:\n- 2-4 words, at most 50 characters, single line.\n- Start with the most important noun from the transcript: body region, organ, imaging modality, or key finding.\n- Use ONLY terms that appear in the transcript. Do not invent, translate, or paraphrase. Do not add diagnoses the doctor did not mention.\n- No filler prefixes: \"Patient\", \"Fall\", \"Verdacht auf\", \"Untersuchung\", \"Diktat\", articles.\n- No intensifiers (\"massiv\", \"ausgeprägt\", \"deutlich\", \"hochgradig\").\n- No quotes, no trailing period, no introductory phrase.\n- If the doctor explicitly names the case (\"Bezeichnung: X\", \"Fall-ID: X\"), start with that exact string.\n- Respond in German. One line only.\n\nExamples:\n\nTranscript: \"Bezeichnung: Schulter links. Patient klagt über...\"\nOutput: Schulter links\n\nTranscript: \"...V.a. Bandscheibenvorfall in der LWS, Schmerzen strahlen...\"\nOutput: LWS, V.a. Bandscheibenvorfall\n\nREMINDER: For silent, empty, or non-medical transcripts, respond with nothing at all — an empty string. Never invent content.";
/// Deterministic leading-filler prefixes stripped after the LLM response.
/// The trailing space is part of the match — we only strip whole tokens.
@@ -23,7 +25,7 @@ const LEADING_FILLERS: &[&str] = &[
"Zusammenfassung ",
];
const MAX_CHARS: usize = 120;
const MAX_CHARS: usize = 60;
#[derive(Debug)]
pub enum OllamaError {
@@ -230,11 +232,11 @@ mod tests {
}
#[test]
fn normalize_caps_at_120_chars_unicode_safe() {
// 150 × 'ä' → 300 bytes but 150 chars; cap must not split a codepoint.
let input: String = "ä".repeat(150);
fn normalize_caps_at_60_chars_unicode_safe() {
// 80 × 'ä' → 160 bytes but 80 chars; cap must not split a codepoint.
let input: String = "ä".repeat(80);
let out = normalize(&input);
assert_eq!(out.chars().count(), 120);
assert_eq!(out.chars().count(), 60);
// Round-trip: must still be valid UTF-8 (would have panicked otherwise).
assert!(out.starts_with('ä'));
}