feat: Add analyze module for LLM integration

This commit introduces the `analyze` module, which orchestrates
communication with Large Language Models (LLMs) for text summarization
and analysis.

Key components include:
- `llm.rs`: Contains the `LlmError` enum and the `chat_once` function
  for interacting with OpenAI-compatible LLM APIs.
- `prompt.rs`: Defines the system prompt and logic for rendering user
  content from analysis inputs.
- `recovery.rs`: Implements logic to scan for and re-enqueue pending
  analysis jobs upon server startup.
- `worker.rs`: The core worker that consumes analysis jobs, processes
  them with the LLM, and writes the output.
- `mod.rs`: Defines `AnalyzeJob` and associated channel types for
  inter-component communication.

This module enables the server to process dictated recordings, summarize
them using an LLM, and store the results.
This commit is contained in:
2026-04-15 19:14:28 +02:00
parent 5435b60b80
commit e2a05a108a
6 changed files with 498 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
pub mod llm;
pub mod prompt;
pub mod recovery;
pub mod worker;
/// A single request to analyze a case. Payload is tiny (path + u32), so the
/// channel is unbounded — persistence lives in `analysis_input_v{N}.json` on
/// disk, not in the queue. Recovery re-enqueues any inputs the server may
/// have held at crash time.
#[derive(Debug, Clone)]
pub struct AnalyzeJob {
pub case_dir: PathBuf,
pub version: u32,
}
pub type AnalyzeSender = mpsc::UnboundedSender<AnalyzeJob>;
pub type AnalyzeReceiver = mpsc::UnboundedReceiver<AnalyzeJob>;
pub fn channel() -> (AnalyzeSender, AnalyzeReceiver) {
mpsc::unbounded_channel()
}
/// Persistent input for a single analysis run. Written by the close-case
/// handler, consumed by the analyze worker, and left on disk indefinitely as
/// an audit trail of what was sent to the LLM.
///
/// Timestamps are stored as ISO-8601 strings (lexicographic order matches
/// chronological order, so string comparison is sufficient for the
/// "Nachtrag" detection the UI needs later).
#[derive(Debug, Serialize, Deserialize)]
pub struct AnalysisInput {
pub version: u32,
/// Latest filesystem mtime (server clock) of any `.m4a` in the case at
/// the moment the close button was clicked. Used later to detect late
/// arrivals that should trigger a re-analysis prompt.
pub last_recording_mtime: String,
pub recordings: Vec<RecordingInput>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RecordingInput {
/// Recording timestamp from the watch (filename-derived, ISO-8601).
pub recorded_at: String,
pub text: String,
}