collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use std::path::PathBuf;

use super::{RagAdapter, RagChunk};
use crate::config::RagConfig;

pub struct AlcoveRag {
    docs_root: PathBuf,
    max_results: usize,
}

impl AlcoveRag {
    /// Build from config. Returns None if docs_root doesn't exist (alcove not set up).
    pub fn from_config(cfg: &RagConfig) -> Option<Self> {
        let docs_root = cfg
            .alcove
            .as_ref()
            .and_then(|a| a.docs_root.as_deref())
            .map(|s| {
                // Expand ~ prefix
                if let Some(rest) = s.strip_prefix("~/") {
                    dirs::home_dir().unwrap_or_default().join(rest)
                } else {
                    PathBuf::from(s)
                }
            })
            .or_else(|| {
                // Auto-detect: use alcove's default_docs_root
                #[cfg(feature = "rag-alcove")]
                {
                    Some(alcove::default_docs_root())
                }
                #[cfg(not(feature = "rag-alcove"))]
                {
                    None
                }
            })?;

        if !docs_root.exists() {
            tracing::debug!("alcove docs_root {:?} not found, skipping RAG", docs_root);
            return None;
        }

        let max_results = cfg.alcove.as_ref().and_then(|a| a.max_results).unwrap_or(5);

        Some(Self {
            docs_root,
            max_results,
        })
    }
}

#[async_trait::async_trait]
impl RagAdapter for AlcoveRag {
    fn name(&self) -> &'static str {
        "alcove"
    }

    async fn retrieve(&self, query: &str, project: Option<&str>) -> Vec<RagChunk> {
        #[cfg(feature = "rag-alcove")]
        {
            // Ensure index is fresh before searching
            let docs_root = self.docs_root.clone();
            let query = query.to_string();
            let project = project.map(|s| s.to_string());
            let max_results = self.max_results;

            // Run blocking tantivy search on a threadpool thread
            let result = tokio::task::spawn_blocking(move || {
                alcove::ensure_index_fresh(&docs_root);
                alcove::search_indexed(&docs_root, &query, max_results, project.as_deref())
            })
            .await;

            match result {
                Ok(Ok(json)) => parse_alcove_results(&json),
                Ok(Err(e)) => {
                    tracing::warn!("alcove search failed: {e}");
                    Vec::new()
                }
                Err(e) => {
                    tracing::warn!("alcove search task panicked: {e}");
                    Vec::new()
                }
            }
        }

        #[cfg(not(feature = "rag-alcove"))]
        {
            let _ = (query, project);
            Vec::new()
        }
    }
}

fn parse_alcove_results(json: &serde_json::Value) -> Vec<RagChunk> {
    let matches = match json.get("matches").and_then(|m| m.as_array()) {
        Some(m) => m,
        None => return Vec::new(),
    };

    matches
        .iter()
        .filter_map(|m| {
            let source = format!(
                "{}/{}",
                m.get("project").and_then(|v| v.as_str()).unwrap_or(""),
                m.get("file").and_then(|v| v.as_str()).unwrap_or("")
            );
            let content = m.get("snippet").and_then(|v| v.as_str())?.to_string();
            let score = m.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
            Some(RagChunk {
                adapter: "alcove",
                source,
                content,
                score,
            })
        })
        .collect()
}