collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use serde::Deserialize;

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

/// HTTP bridge for user-supplied RAG backends.
///
/// Protocol:
///   POST {url}/search
///   Body:  { "query": "...", "project": "..." }
///   Reply: { "results": [{ "source": "...", "content": "...", "score": 0.9 }] }
pub struct BridgeRag {
    url: String,
    token: Option<String>,
    max_results: usize,
    client: reqwest::Client,
}

impl BridgeRag {
    pub fn new(cfg: &BridgeRagConfig) -> Self {
        // Expand env vars in token
        let token = cfg.token.as_deref().map(|t| {
            if let Some(var) = t.strip_prefix("${").and_then(|s| s.strip_suffix('}')) {
                std::env::var(var).unwrap_or_default()
            } else {
                t.to_string()
            }
        });

        Self {
            url: cfg.url.trim_end_matches('/').to_string(),
            token,
            max_results: cfg.max_results.unwrap_or(5),
            client: reqwest::Client::new(),
        }
    }
}

#[derive(Deserialize)]
struct BridgeResponse {
    results: Vec<BridgeResult>,
}

#[derive(Deserialize)]
struct BridgeResult {
    source: String,
    content: String,
    #[serde(default)]
    score: f32,
}

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

    async fn retrieve(&self, query: &str, project: Option<&str>) -> Vec<RagChunk> {
        let mut body = serde_json::json!({
            "query": query,
            "max_results": self.max_results,
        });
        if let Some(proj) = project {
            body["project"] = serde_json::Value::String(proj.to_string());
        }

        let mut req = self.client.post(format!("{}/search", self.url)).json(&body);
        if let Some(token) = &self.token {
            req = req.bearer_auth(token);
        }

        match req.send().await {
            Ok(resp) if resp.status().is_success() => match resp.json::<BridgeResponse>().await {
                Ok(data) => data
                    .results
                    .into_iter()
                    .map(|r| RagChunk {
                        adapter: "bridge",
                        source: r.source,
                        content: r.content,
                        score: r.score,
                    })
                    .collect(),
                Err(e) => {
                    tracing::warn!("RAG bridge response parse error: {e}");
                    Vec::new()
                }
            },
            Ok(resp) => {
                tracing::warn!("RAG bridge HTTP {}", resp.status());
                Vec::new()
            }
            Err(e) => {
                tracing::warn!("RAG bridge request failed: {e}");
                Vec::new()
            }
        }
    }
}