klieo-memory-qdrant 2.1.0

Qdrant-backed implementation of klieo-core's LongTermMemory.
Documentation
//! `MemoryQdrant` — convenience factory wrapping the `LongTermMemory`
//! handle in an `Arc<dyn …>` ready for `AgentContext`.

use crate::client::{QdrantConfig, QdrantHandle};
use crate::embedder::{DummyEmbedder, Embedder};
use crate::long_term::QdrantLongTerm;
use klieo_core::error::MemoryError;
use klieo_core::memory::LongTermMemory;
use std::sync::Arc;

/// Factory bundle returned by [`MemoryQdrant::new`].
#[non_exhaustive]
pub struct MemoryQdrant {
    /// `LongTermMemory` implementation backed by Qdrant.
    pub long_term: Arc<dyn LongTermMemory>,
    /// Concrete [`QdrantLongTerm`] handle. The M2 GraphRAG composer
    /// needs the concrete type to call `recall_filtered_checked` —
    /// `FilterableLongTermMemory` lives in `klieo-memory-graph` (0.x)
    /// so it cannot be smuggled into the `LongTermMemory` trait
    /// object. Both fields point to the same `QdrantLongTerm`.
    pub qdrant_long_term: Arc<QdrantLongTerm>,
}

impl MemoryQdrant {
    /// Capability-shaped default — connect to a Qdrant endpoint URL
    /// with [`DummyEmbedder`] (zero-vector, FIFO recall) and default
    /// [`QdrantConfig`] (collection prefix `"klieo_facts"`, plaintext
    /// remote refused, embedder id `"default"`).
    ///
    /// Use [`Self::new`] when you need a real embedder or non-default
    /// `QdrantConfig` (API key, custom prefix, opt-in plaintext for
    /// sealed mesh deployments, custom embedder id).
    pub async fn connect(url: impl Into<String>) -> Result<Self, MemoryError> {
        Self::new(QdrantConfig::new(url), Arc::new(DummyEmbedder)).await
    }

    /// Connect to Qdrant and return a ready-to-use handle.
    ///
    /// The collection is *not* created upfront; it is bootstrapped
    /// lazily on the first `remember()` call. This avoids races when
    /// multiple processes concurrently start before any fact is
    /// written.
    pub async fn new(cfg: QdrantConfig, embedder: Arc<dyn Embedder>) -> Result<Self, MemoryError> {
        let handle = QdrantHandle::connect(&cfg)?;
        let embedder_id = cfg.embedder_id().to_string();
        let qdrant_long_term = Arc::new(QdrantLongTerm::new(handle, embedder, embedder_id));
        let long_term: Arc<dyn LongTermMemory> = qdrant_long_term.clone();
        Ok(Self {
            long_term,
            qdrant_long_term,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use klieo_core::error::MemoryError;

    #[tokio::test]
    async fn connect_refuses_plaintext_remote_by_default() {
        let result = MemoryQdrant::connect("http://qdrant.example.com:6334").await;
        match result {
            Err(MemoryError::Store(_)) => {}
            Err(other) => {
                panic!("expected MemoryError::Store from plaintext-remote refusal, got {other:?}")
            }
            Ok(_) => panic!("non-loopback http:// must be rejected by default"),
        }
    }
}