sara-tasks 2.7.6

Sara — folder-aware task manager
use anyhow::Result;
use rusqlite::Connection;
use std::collections::HashSet;

use crate::commands::recall::enrich::hit::item_hit;
use crate::commands::recall::types::Hit;
use crate::infrastructure::config::Config;
use crate::infrastructure::db;

pub(in crate::commands::recall) struct SemanticOpts {
    pub(in crate::commands::recall) enabled: bool,
    pub(in crate::commands::recall) threshold: f32,
    pub(in crate::commands::recall) top_k: usize,
}

impl SemanticOpts {
    pub(in crate::commands::recall) fn from_cfg(cfg: &Config) -> Self {
        SemanticOpts {
            enabled: true,
            threshold: cfg.recall.semantic_threshold,
            top_k: cfg.recall.semantic_top_k,
        }
    }

    pub(in crate::commands::recall) fn off() -> Self {
        SemanticOpts {
            enabled: false,
            threshold: 1.0,
            top_k: 0,
        }
    }
}

pub(in crate::commands::recall) fn merge_semantic_hits(
    conn: &Connection,
    query: &str,
    opts: &SemanticOpts,
    allowlist: Option<&HashSet<uuid::Uuid>>,
    hits: &mut Vec<Hit>,
) -> Result<()> {
    use crate::infrastructure::memory::embedding::{self, Embedder};

    let qv = embedding::bundled().embed(query);
    if qv.iter().all(|&x| x == 0.0) {
        return Ok(());
    }

    let already: HashSet<String> = hits
        .iter()
        .filter_map(|h| h.item_uuid.map(|u| u.to_string()))
        .collect();

    let mut scored: Vec<(String, f32)> = db::active_embeddings(conn)?
        .into_iter()
        .filter(|(uuid, _)| {
            if already.contains(uuid) {
                return false;
            }
            match allowlist {
                Some(allow) => matches!(uuid::Uuid::parse_str(uuid), Ok(u) if allow.contains(&u)),
                None => true,
            }
        })
        .map(|(uuid, v)| (uuid, embedding::cosine(&qv, &v)))
        .filter(|(_, c)| *c >= opts.threshold)
        .collect();
    scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
    scored.truncate(opts.top_k);

    for (uuid, cos) in scored {
        if let Ok(item) = db::get_item_by_uuid(conn, &uuid) {
            let mut hit = item_hit(conn, item, false);
            hit.semantic = true;
            hit.cosine = Some(cos);
            hits.push(hit);
        }
    }
    Ok(())
}