feat: async insight generation with SQLite job tracking

- Add insight_generation_jobs table migration and DAO
- Implement job lifecycle: create_or_get_active, complete, fail, cancel
- Refactor POST /insights/generate and /agentic to async spawn with timeout
- Add GET /insights/generation/status endpoint with job_id and file_path lookup
- Use String for enum fields in Diesel models to avoid private Bound type
- Add from_str() helpers on InsightJobStatus and InsightGenerationType
- Fix update_training_messages to return Result<usize, DbError>
- 7/7 DAO unit tests passing
This commit is contained in:
Cameron Cordes
2026-05-27 10:01:17 -04:00
parent 5a75d1a28c
commit b87eb4e690
13 changed files with 1046 additions and 174 deletions
+15 -4
View File
@@ -6,10 +6,10 @@ use crate::ai::llamacpp::LlamaCppClient;
use crate::ai::openrouter::OpenRouterClient;
use crate::ai::{InsightGenerator, OllamaClient, SmsApiClient};
use crate::database::{
CalendarEventDao, DailySummaryDao, ExifDao, InsightDao, KnowledgeDao, LocationHistoryDao,
SearchHistoryDao, SqliteCalendarEventDao, SqliteDailySummaryDao, SqliteExifDao,
SqliteInsightDao, SqliteKnowledgeDao, SqliteLocationHistoryDao, SqliteSearchHistoryDao,
connect,
CalendarEventDao, DailySummaryDao, ExifDao, InsightDao, InsightGenerationJobDao, KnowledgeDao,
LocationHistoryDao, SearchHistoryDao, SqliteCalendarEventDao, SqliteDailySummaryDao,
SqliteExifDao, SqliteInsightDao, SqliteInsightGenerationJobDao, SqliteKnowledgeDao,
SqliteLocationHistoryDao, SqliteSearchHistoryDao, connect,
};
use crate::database::{PreviewDao, SqlitePreviewDao};
use crate::faces;
@@ -86,6 +86,8 @@ pub struct AppState {
/// Same disabled semantics as `face_client`: unset env → no-op
/// backlog drain, /photos/search returns an empty result.
pub clip_client: ClipClient,
/// Tracks async insight generation jobs (spawned by generate endpoints).
pub insight_job_dao: Arc<Mutex<Box<dyn InsightGenerationJobDao>>>,
}
impl AppState {
@@ -124,6 +126,7 @@ impl AppState {
preview_dao: Arc<Mutex<Box<dyn PreviewDao>>>,
face_client: FaceClient,
clip_client: ClipClient,
insight_job_dao: Arc<Mutex<Box<dyn InsightGenerationJobDao>>>,
) -> Self {
assert!(
!libraries_vec.is_empty(),
@@ -165,6 +168,7 @@ impl AppState {
insight_chat,
face_client,
clip_client,
insight_job_dao,
}
}
@@ -253,6 +257,10 @@ impl Default for AppState {
let face_dao: Arc<Mutex<Box<dyn faces::FaceDao>>> =
Arc::new(Mutex::new(Box::new(faces::SqliteFaceDao::new())));
// Initialize insight generation job DAO (async generation tracking)
let insight_job_dao: Arc<Mutex<Box<dyn InsightGenerationJobDao>>> =
Arc::new(Mutex::new(Box::new(SqliteInsightGenerationJobDao::new())));
// Load base path and ensure the primary library row reflects it.
let base_path = env::var("BASE_PATH").expect("BASE_PATH was not set in the env");
let mut seed_conn = connect();
@@ -319,6 +327,7 @@ impl Default for AppState {
preview_dao,
face_client,
clip_client,
insight_job_dao,
)
}
}
@@ -389,6 +398,7 @@ fn parse_llamacpp_allowed_models() -> Vec<String> {
impl AppState {
/// Creates an AppState instance for testing with temporary directories
pub fn test_state() -> Self {
use crate::database::insight_generation_job_dao::SqliteInsightGenerationJobDao;
use actix::Actor;
// Create a base temporary directory
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
@@ -502,6 +512,7 @@ impl AppState {
preview_dao,
FaceClient::new(None), // disabled in test
ClipClient::new(None), // disabled in test
Arc::new(Mutex::new(Box::new(SqliteInsightGenerationJobDao::new()))), // placeholder for test
)
}
}