feat: nightly agentic pre-generation of memory reels
Implement end-to-end nightly pre-generation of memory reels with agentic
scripting that grounds narration in calendar, location, messages, and RAG.
Sections A-E from the plan:
A. Extract produce_reel pipeline core from run_reel_job with
ScripterMode::Fast/Agentic and progress callbacks.
B. Agentic scripter: factor run_readonly_tool_loop from the insight
generator, build read-only tool gate, prompt builder with GPS, and
generate_script_agentic with fallback to fast path.
C. Precomputed reels ledger (SQLite table + DAO), GET /reels/precomputed
handler with validity gate, GET /reels/by-key/{key}/video streaming,
and normalize_library_key helper.
D. Nightly scheduler: spawn_pregen_scheduler with configurable hour,
run_pregen_batch (day/week/month spans), pregen_one with dedup and
disk-check, secs_until_next_run_hour time math.
E. user_ai_prefs passive mirror table + DAO for param capture in
create_reel_handler and replay in the scheduler.
Also fixes resolve_library_param signature to take &[Library] and adds
resolve_library_param_state wrapper for AppState callers.
New files: migrations/2026-06-13-000000_add_precomputed_reels/,
migrations/2026-06-13-000010_add_user_ai_prefs/,
src/database/precomputed_reel_dao.rs,
src/database/user_ai_prefs_dao.rs
This commit is contained in:
+42
-37
@@ -291,11 +291,11 @@ pub fn seed_or_patch_from_env(conn: &mut SqliteConnection, base_path: &str) {
|
||||
}
|
||||
|
||||
/// Resolve a library request parameter (accepts numeric id as string or name)
|
||||
/// against the configured libraries. Returns `Ok(None)` when the param is
|
||||
/// against a list of libraries. Returns `Ok(None)` when the param is
|
||||
/// absent, meaning "span all libraries". Returns `Err` when a value is
|
||||
/// provided but does not match any library.
|
||||
pub fn resolve_library_param<'a>(
|
||||
state: &'a AppState,
|
||||
libs: &'a [Library],
|
||||
param: Option<&str>,
|
||||
) -> Result<Option<&'a Library>, String> {
|
||||
let Some(raw) = param.map(str::trim).filter(|s| !s.is_empty()) else {
|
||||
@@ -303,18 +303,29 @@ pub fn resolve_library_param<'a>(
|
||||
};
|
||||
|
||||
if let Ok(id) = raw.parse::<i32>() {
|
||||
return state
|
||||
.library_by_id(id)
|
||||
return libs
|
||||
.iter()
|
||||
.find(|l| l.id == id)
|
||||
.map(Some)
|
||||
.ok_or_else(|| format!("unknown library id: {}", id));
|
||||
}
|
||||
|
||||
state
|
||||
.library_by_name(raw)
|
||||
libs.iter()
|
||||
.find(|l| l.name == raw)
|
||||
.map(Some)
|
||||
.ok_or_else(|| format!("unknown library name: {}", raw))
|
||||
}
|
||||
|
||||
/// Resolve a library request parameter against the AppState's libraries.
|
||||
/// Returns `Ok(None)` when the param is absent, meaning "span all libraries".
|
||||
/// Returns `Err` when a value is provided but does not match any library.
|
||||
pub fn resolve_library_param_state<'a>(
|
||||
state: &'a AppState,
|
||||
param: Option<&str>,
|
||||
) -> Result<Option<&'a Library>, String> {
|
||||
resolve_library_param(&state.libraries, param)
|
||||
}
|
||||
|
||||
/// Health of a library at a point in time. Probed at the top of each
|
||||
/// file-watcher tick. The `Stale` state is the "be conservative" signal:
|
||||
/// destructive paths (ingest writes, future move-handoff and orphan GC in
|
||||
@@ -662,12 +673,6 @@ mod tests {
|
||||
assert_eq!(abs, PathBuf::from("/tmp/media/2024/photo.jpg"));
|
||||
}
|
||||
|
||||
fn state_with_libraries(libs: Vec<Library>) -> AppState {
|
||||
let mut state = AppState::test_state();
|
||||
state.libraries = libs;
|
||||
state
|
||||
}
|
||||
|
||||
fn sample_libraries() -> Vec<Library> {
|
||||
vec![
|
||||
Library {
|
||||
@@ -687,52 +692,52 @@ mod tests {
|
||||
]
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn resolve_library_param_absent_is_union() {
|
||||
let state = state_with_libraries(sample_libraries());
|
||||
assert!(matches!(resolve_library_param(&state, None), Ok(None)));
|
||||
#[test]
|
||||
fn resolve_library_param_absent_is_union() {
|
||||
let libs = sample_libraries();
|
||||
assert!(matches!(resolve_library_param(&libs, None), Ok(None)));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn resolve_library_param_empty_or_whitespace_is_union() {
|
||||
let state = state_with_libraries(sample_libraries());
|
||||
assert!(matches!(resolve_library_param(&state, Some("")), Ok(None)));
|
||||
#[test]
|
||||
fn resolve_library_param_empty_or_whitespace_is_union() {
|
||||
let libs = sample_libraries();
|
||||
assert!(matches!(resolve_library_param(&libs, Some("")), Ok(None)));
|
||||
assert!(matches!(
|
||||
resolve_library_param(&state, Some(" ")),
|
||||
resolve_library_param(&libs, Some(" ")),
|
||||
Ok(None)
|
||||
));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn resolve_library_param_numeric_id_matches() {
|
||||
let state = state_with_libraries(sample_libraries());
|
||||
let lib = resolve_library_param(&state, Some("7"))
|
||||
#[test]
|
||||
fn resolve_library_param_numeric_id_matches() {
|
||||
let libs = sample_libraries();
|
||||
let lib = resolve_library_param(&libs, Some("7"))
|
||||
.expect("valid id")
|
||||
.expect("some library");
|
||||
assert_eq!(lib.id, 7);
|
||||
assert_eq!(lib.name, "archive");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn resolve_library_param_name_matches() {
|
||||
let state = state_with_libraries(sample_libraries());
|
||||
let lib = resolve_library_param(&state, Some("main"))
|
||||
#[test]
|
||||
fn resolve_library_param_name_matches() {
|
||||
let libs = sample_libraries();
|
||||
let lib = resolve_library_param(&libs, Some("main"))
|
||||
.expect("valid name")
|
||||
.expect("some library");
|
||||
assert_eq!(lib.id, 1);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn resolve_library_param_unknown_id_errs() {
|
||||
let state = state_with_libraries(sample_libraries());
|
||||
let err = resolve_library_param(&state, Some("999")).unwrap_err();
|
||||
#[test]
|
||||
fn resolve_library_param_unknown_id_errs() {
|
||||
let libs = sample_libraries();
|
||||
let err = resolve_library_param(&libs, Some("999")).unwrap_err();
|
||||
assert!(err.contains("unknown library id"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn resolve_library_param_unknown_name_errs() {
|
||||
let state = state_with_libraries(sample_libraries());
|
||||
let err = resolve_library_param(&state, Some("missing")).unwrap_err();
|
||||
#[test]
|
||||
fn resolve_library_param_unknown_name_errs() {
|
||||
let libs = sample_libraries();
|
||||
let err = resolve_library_param(&libs, Some("missing")).unwrap_err();
|
||||
assert!(err.contains("unknown library name"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user