feat: Handle empty Ollama responses gracefully

Introduce `OllamaError::EmptyResponse` to represent cases where the
Ollama
model returns an empty result due to silence rules or lack of keywords.

This change prevents treating an empty response as a parse error and
logs
it at a lower severity level. A new integration test verifies this
behavior.
This commit is contained in:
2026-04-20 11:24:44 +02:00
parent 3ad8c4c2e9
commit 224ee60363
4 changed files with 47 additions and 3 deletions
+3 -1
View File
@@ -322,7 +322,9 @@ pub async fn handle_case_page(
let is_admin = user.is_admin();
let most_recent = recordings.last().map(|r| r.filename.as_str());
let recorded_at_iso = most_recent.map(recorded_at_iso_of).filter(|s| !s.is_empty());
let recorded_at_iso = most_recent
.map(recorded_at_iso_of)
.filter(|s| !s.is_empty());
let time_hms_utc = most_recent.map(extract_hhmm_utc).unwrap_or_default();
CasePageTemplate {
+13 -2
View File
@@ -30,8 +30,15 @@ const MAX_CHARS: usize = 60;
#[derive(Debug)]
pub enum OllamaError {
Http(reqwest::Error),
Status { status: u16, body: String },
Status {
status: u16,
body: String,
},
Parse(String),
/// Model obeyed the silence rule: transcript had no extractable keyword,
/// so the response was empty. Not a failure — callers should treat this
/// as "no oneliner this round" and log below error level.
EmptyResponse,
}
impl std::fmt::Display for OllamaError {
@@ -40,6 +47,10 @@ impl std::fmt::Display for OllamaError {
Self::Http(e) => write!(f, "ollama http error: {e}"),
Self::Status { status, body } => write!(f, "ollama returned {status}: {body}"),
Self::Parse(msg) => write!(f, "ollama response parse error: {msg}"),
Self::EmptyResponse => write!(
f,
"ollama response was empty (no medical keyword in transcript)"
),
}
}
}
@@ -132,7 +143,7 @@ pub async fn generate_oneliner(
let normalized = normalize(&content);
if normalized.is_empty() {
return Err(OllamaError::Parse("empty after normalization".into()));
return Err(OllamaError::EmptyResponse);
}
Ok(normalized)
}
+6
View File
@@ -229,6 +229,12 @@ pub(crate) async fn update_oneliner(
);
}
}
Err(ollama::OllamaError::EmptyResponse) => {
warn!(
case = %case_dir.display(),
"oneliner skipped — model returned empty (no medical keyword in transcript)"
);
}
Err(e) => {
error!(case = %case_dir.display(), error = %e, "oneliner generation failed");
}
+25
View File
@@ -500,6 +500,31 @@ async fn ollama_oneliner_sends_keep_alive_and_model() {
// Mock::expect(1) + server drop verifies the matcher hit exactly once.
}
#[tokio::test]
async fn ollama_oneliner_empty_response_when_model_obeys_silence_rule() {
// Model followed the silence rule for a keyword-less transcript and
// returned empty content. That's a valid outcome, not a parse error.
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_chat_body(" \n ")))
.mount(&server)
.await;
let client = reqwest::Client::new();
let err = generate_oneliner(
&client,
&server.uri(),
MODEL,
0,
"äh, Moment",
Duration::from_secs(5),
)
.await
.expect_err("expected EmptyResponse");
assert!(matches!(err, OllamaError::EmptyResponse), "got {err:?}");
}
#[tokio::test]
async fn ollama_oneliner_parse_error_on_missing_content() {
let server = MockServer::start().await;