feat: Add auto-trigger for LLM analysis

Introduces a new module `auto_trigger` responsible for opportunistically
initiating LLM analysis jobs. This mechanism scans case directories for
new or updated recordings and transcripts, automatically creating and
queuing analysis jobs when appropriate.

Key components:
- `evaluate_case`: Pure function to determine if a case is ready for
  analysis based on recording status, transcriptions, document
  modification times, and existing job/failure markers.
- `try_enqueue`: Orchestrates the analysis job creation and queuing
  process if `evaluate_case` returns `AutoDecision::Enqueue`.
- `try_enqueue_all_for_user`: Iterates through all user cases to trigger
  `try_enqueue` for eligible ones.
- `write_failure_marker`/`remove_failure_marker`: Handles persistent
  recording of analysis failures and their cleanup.

This feature aims to reduce manual intervention by automatically
analyzing new or changed case data.
This commit is contained in:
2026-04-20 09:18:30 +02:00
parent 1d702e2d85
commit 70425939b2
9 changed files with 585 additions and 3 deletions
+23 -1
View File
@@ -5,7 +5,9 @@ use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tracing::{error, info, warn};
use super::{ANALYSIS_INPUT_FILE, AnalysisInput, AnalyzeReceiver, DOCUMENT_FILE, llm, prompt};
use super::{
ANALYSIS_INPUT_FILE, AnalysisInput, AnalyzeReceiver, DOCUMENT_FILE, auto_trigger, llm, prompt,
};
use crate::config::Config;
use crate::events::{self, CaseEventKind, EventSender};
use crate::gazetteer::Gazetteer;
@@ -69,11 +71,18 @@ async fn process(
let stub = b"_Keine verwertbaren Aufnahmen (alle Aufnahmen still)._\n";
if let Err(e) = write_atomic(&document_path, stub).await {
error!(path = %document_path.display(), error = %e, "writing stub document failed");
let _ = auto_trigger::write_failure_marker(
case_dir,
&input.last_recording_mtime,
&format!("stub write: {e}"),
)
.await;
return;
}
if let Err(e) = tokio::fs::remove_file(&input_path).await {
warn!(path = %input_path.display(), error = %e, "removing analysis input failed");
}
auto_trigger::remove_failure_marker(case_dir).await;
info!(case = %case_dir.display(), "analysis done (stub, no recordings)");
events::emit(
events_tx,
@@ -117,6 +126,12 @@ async fn process(
// Do not log the response body — LlmError::Display is already
// redacted, but reinforce the rule here for future readers.
error!(case = %case_dir.display(), error = %e, "llm call failed");
let _ = auto_trigger::write_failure_marker(
case_dir,
&input.last_recording_mtime,
&format!("llm: {e}"),
)
.await;
return;
}
};
@@ -128,6 +143,12 @@ async fn process(
if let Err(e) = write_atomic(&document_path, document.as_bytes()).await {
error!(path = %document_path.display(), error = %e, "writing document failed");
let _ = auto_trigger::write_failure_marker(
case_dir,
&input.last_recording_mtime,
&format!("write document: {e}"),
)
.await;
return;
}
@@ -137,6 +158,7 @@ async fn process(
if let Err(e) = tokio::fs::remove_file(&input_path).await {
warn!(path = %input_path.display(), error = %e, "removing analysis input failed");
}
auto_trigger::remove_failure_marker(case_dir).await;
info!(
case = %case_dir.display(),