reasonkit/
memory_interface.rs

1//! Memory Interface Trait
2//!
3//! This module defines the trait that reasonkit-core uses to interface with reasonkit-mem.
4//! It provides a clean abstraction for document storage, retrieval, and context assembly.
5//!
6//! ## Design Principles
7//!
8//! - **Async-first**: All operations are async (tokio runtime required)
9//! - **Result-oriented**: All operations return `Result<T>` with structured error handling
10//! - **Trait-based**: Allows multiple implementations (in-memory, Qdrant, file-based, etc.)
11//! - **Batch-friendly**: Supports operations on multiple documents/queries
12//!
13//! ## Usage Example
14//!
15//! ```rust,ignore
16//! use reasonkit::memory_interface::MemoryService;
17//! use reasonkit_mem::Document;
18//!
19//! #[tokio::main]
20//! async fn main() -> anyhow::Result<()> {
21//!     // Get memory service implementation (from reasonkit-mem)
22//!     let memory = create_memory_service().await?;
23//!
24//!     // Store a document
25//!     memory.store_document(doc).await?;
26//!
27//!     // Search for related content
28//!     let results = memory.search("query text", 10).await?;
29//!
30//!     // Get context for reasoning
31//!     let context = memory.get_context("query", 5).await?;
32//!
33//!     Ok(())
34//! }
35//! ```
36
37use async_trait::async_trait;
38use serde::{Deserialize, Serialize};
39use uuid::Uuid;
40
41/// Re-export reasonkit-mem types for convenience (when memory feature is enabled)
42#[cfg(feature = "memory")]
43pub use reasonkit_mem::{
44    Chunk, Document, DocumentContent, DocumentType, MatchSource, Metadata, ProcessingState,
45    ProcessingStatus, RetrievalConfig, SearchResult, Source, SourceType,
46};
47
48// Type stubs for when memory feature is disabled
49// These allow code to reference types without compilation errors
50#[cfg(not(feature = "memory"))]
51pub type Chunk = ();
52#[cfg(not(feature = "memory"))]
53pub type Document = ();
54#[cfg(not(feature = "memory"))]
55pub type DocumentContent = ();
56#[cfg(not(feature = "memory"))]
57pub type DocumentType = ();
58#[cfg(not(feature = "memory"))]
59pub type Metadata = ();
60#[cfg(not(feature = "memory"))]
61pub type ProcessingState = ();
62#[cfg(not(feature = "memory"))]
63pub type ProcessingStatus = ();
64#[cfg(not(feature = "memory"))]
65pub type RetrievalConfig = ();
66#[cfg(not(feature = "memory"))]
67pub type SearchResult = ();
68#[cfg(not(feature = "memory"))]
69pub type Source = ();
70#[cfg(not(feature = "memory"))]
71pub type SourceType = ();
72
73// MatchSource stub
74#[cfg(not(feature = "memory"))]
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76pub enum MatchSource {
77    Dense,
78    Sparse,
79    Hybrid,
80    Raptor,
81}
82
83/// Error type for memory interface operations
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct MemoryError {
86    /// Error category
87    pub category: ErrorCategory,
88    /// Human-readable message
89    pub message: String,
90    /// Optional error context
91    pub context: Option<String>,
92}
93
94/// Error categories for memory operations
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96pub enum ErrorCategory {
97    /// Storage operation failed
98    Storage,
99    /// Embedding/vector operation failed
100    Embedding,
101    /// Retrieval/search failed
102    Retrieval,
103    /// Indexing failed
104    Indexing,
105    /// Document not found
106    NotFound,
107    /// Invalid input data
108    InvalidInput,
109    /// Configuration error
110    Config,
111    /// Unknown or internal error
112    Internal,
113}
114
115impl std::fmt::Display for MemoryError {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(
118            f,
119            "{:?}: {}{}",
120            self.category,
121            self.message,
122            self.context
123                .as_ref()
124                .map(|c| format!(" ({})", c))
125                .unwrap_or_default()
126        )
127    }
128}
129
130impl std::error::Error for MemoryError {}
131
132/// Result type for memory interface operations
133pub type MemoryResult<T> = std::result::Result<T, MemoryError>;
134
135/// Configuration for context retrieval
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct ContextConfig {
138    /// Number of chunks to retrieve
139    pub top_k: usize,
140    /// Minimum relevance score (0.0-1.0)
141    pub min_score: f32,
142    /// Alpha weight for hybrid search (0=sparse only, 1=dense only)
143    pub alpha: f32,
144    /// Whether to use RAPTOR hierarchical tree
145    pub use_raptor: bool,
146    /// Whether to rerank results with cross-encoder
147    pub rerank: bool,
148    /// Include metadata in results
149    pub include_metadata: bool,
150}
151
152impl Default for ContextConfig {
153    fn default() -> Self {
154        Self {
155            top_k: 10,
156            min_score: 0.0,
157            alpha: 0.7, // Favor semantic search
158            use_raptor: false,
159            rerank: false,
160            include_metadata: true,
161        }
162    }
163}
164
165/// A context window retrieved from memory
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ContextWindow {
168    /// Ordered list of relevant chunks
169    pub chunks: Vec<Chunk>,
170    /// Associated documents
171    pub documents: Vec<Document>,
172    /// Relevance scores for each chunk
173    pub scores: Vec<f32>,
174    /// Source information (dense, sparse, hybrid, raptor)
175    pub sources: Vec<MatchSource>,
176    /// Total token count (approximate)
177    pub token_count: usize,
178    /// Quality metrics
179    pub quality: ContextQuality,
180}
181
182/// Quality metrics for a context window
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct ContextQuality {
185    /// Average relevance score
186    pub avg_score: f32,
187    /// Highest relevance score
188    pub max_score: f32,
189    /// Lowest relevance score
190    pub min_score: f32,
191    /// Diversity score (0-1, higher = more diverse)
192    pub diversity: f32,
193    /// Coverage score (0-1, how complete is the context)
194    pub coverage: f32,
195}
196
197/// Statistics about memory service state
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct MemoryStats {
200    /// Number of stored documents
201    pub document_count: usize,
202    /// Number of chunks across all documents
203    pub chunk_count: usize,
204    /// Number of embeddings stored
205    pub embedding_count: usize,
206    /// Total storage size in bytes
207    pub storage_size_bytes: u64,
208    /// Number of indexed documents
209    pub indexed_count: usize,
210    /// Memory service health status
211    pub is_healthy: bool,
212}
213
214/// Main trait for memory service operations
215///
216/// This trait defines the interface that reasonkit-core uses to interact with reasonkit-mem.
217/// Implementations handle:
218/// - Document storage and retrieval
219/// - Vector embeddings and similarity search
220/// - Hybrid search (dense + sparse)
221/// - Context assembly for reasoning
222///
223/// # Thread Safety
224///
225/// All implementations MUST be:
226/// - `Send + Sync` for safe cross-thread sharing
227/// - Internally synchronized (e.g., using `Arc<RwLock<T>>`)
228/// - Panic-safe (errors should propagate, not panic)
229#[async_trait]
230pub trait MemoryService: Send + Sync {
231    // ==================== DOCUMENT STORAGE ====================
232
233    /// Store a document in memory
234    ///
235    /// This operation:
236    /// 1. Validates the document structure
237    /// 2. Stores metadata in the document store
238    /// 3. Chunks the content (if not already chunked)
239    /// 4. Prepares chunks for embedding
240    ///
241    /// # Arguments
242    /// * `document` - The document to store
243    ///
244    /// # Returns
245    /// * `Ok(Uuid)` - The document ID if successful
246    /// * `Err(MemoryError)` - If storage fails
247    ///
248    /// # Example
249    /// ```rust,ignore
250    /// let doc_id = memory.store_document(document).await?;
251    /// println!("Stored document: {}", doc_id);
252    /// ```
253    async fn store_document(&self, document: &Document) -> MemoryResult<Uuid>;
254
255    /// Store multiple documents (batch operation)
256    ///
257    /// Stores documents in parallel where possible for efficiency.
258    ///
259    /// # Arguments
260    /// * `documents` - Slice of documents to store
261    ///
262    /// # Returns
263    /// * `Ok(Vec<Uuid>)` - IDs of stored documents
264    /// * `Err(MemoryError)` - If any document fails to store
265    async fn store_documents(&self, documents: &[Document]) -> MemoryResult<Vec<Uuid>>;
266
267    /// Retrieve a document by ID
268    ///
269    /// # Arguments
270    /// * `doc_id` - The document UUID
271    ///
272    /// # Returns
273    /// * `Ok(Some(Document))` - The document if found
274    /// * `Ok(None)` - If document doesn't exist
275    /// * `Err(MemoryError)` - If retrieval fails
276    async fn get_document(&self, doc_id: &Uuid) -> MemoryResult<Option<Document>>;
277
278    /// Delete a document by ID
279    ///
280    /// This removes:
281    /// - Document metadata
282    /// - All associated chunks
283    /// - Embeddings for those chunks
284    /// - Index entries
285    ///
286    /// # Arguments
287    /// * `doc_id` - The document UUID
288    ///
289    /// # Returns
290    /// * `Ok(())` - If successful (no error if document doesn't exist)
291    /// * `Err(MemoryError)` - If deletion fails
292    async fn delete_document(&self, doc_id: &Uuid) -> MemoryResult<()>;
293
294    /// List all document IDs in memory
295    ///
296    /// # Returns
297    /// * `Ok(Vec<Uuid>)` - All document IDs
298    /// * `Err(MemoryError)` - If listing fails
299    async fn list_documents(&self) -> MemoryResult<Vec<Uuid>>;
300
301    // ==================== SEARCH & RETRIEVAL ====================
302
303    /// Search documents using hybrid search
304    ///
305    /// Performs a combined search across:
306    /// - Dense vector search (semantic similarity)
307    /// - Sparse BM25 search (keyword matching)
308    /// - Reciprocal Rank Fusion for combining results
309    /// - Optional cross-encoder reranking
310    ///
311    /// # Arguments
312    /// * `query` - The search query text
313    /// * `top_k` - Number of results to return
314    ///
315    /// # Returns
316    /// * `Ok(Vec<SearchResult>)` - Ranked search results
317    /// * `Err(MemoryError)` - If search fails
318    ///
319    /// # Example
320    /// ```rust,ignore
321    /// let results = memory.search("machine learning optimization", 10).await?;
322    /// for result in results {
323    ///     println!("Score: {}, Document: {}", result.score, result.document_id);
324    /// }
325    /// ```
326    async fn search(&self, query: &str, top_k: usize) -> MemoryResult<Vec<SearchResult>>;
327
328    /// Search with advanced configuration
329    ///
330    /// # Arguments
331    /// * `query` - The search query
332    /// * `config` - Retrieval configuration
333    ///
334    /// # Returns
335    /// * `Ok(Vec<SearchResult>)` - Ranked results
336    /// * `Err(MemoryError)` - If search fails
337    async fn search_with_config(
338        &self,
339        query: &str,
340        config: &ContextConfig,
341    ) -> MemoryResult<Vec<SearchResult>>;
342
343    /// Vector similarity search
344    ///
345    /// Searches using only dense embeddings (fast, semantic).
346    /// Use when you already have embeddings or want pure semantic search.
347    ///
348    /// # Arguments
349    /// * `embedding` - Query vector
350    /// * `top_k` - Number of results
351    ///
352    /// # Returns
353    /// * `Ok(Vec<SearchResult>)` - Top K similar chunks
354    /// * `Err(MemoryError)` - If search fails
355    async fn search_by_vector(
356        &self,
357        embedding: &[f32],
358        top_k: usize,
359    ) -> MemoryResult<Vec<SearchResult>>;
360
361    /// Keyword search (BM25)
362    ///
363    /// Searches using only sparse BM25 indexing (fast, keyword-based).
364    /// Use when you want keyword matching or have specific terms.
365    ///
366    /// # Arguments
367    /// * `query` - The search query
368    /// * `top_k` - Number of results
369    ///
370    /// # Returns
371    /// * `Ok(Vec<SearchResult>)` - Ranked results by BM25 score
372    /// * `Err(MemoryError)` - If search fails
373    async fn search_by_keywords(
374        &self,
375        query: &str,
376        top_k: usize,
377    ) -> MemoryResult<Vec<SearchResult>>;
378
379    // ==================== CONTEXT ASSEMBLY ====================
380
381    /// Get context window for reasoning
382    ///
383    /// This is the primary method for assembling context for LLM reasoning.
384    /// It returns a structured context window with:
385    /// - Ranked, relevant chunks
386    /// - Associated documents
387    /// - Quality metrics
388    /// - Token count estimate
389    ///
390    /// # Arguments
391    /// * `query` - The reasoning query/prompt
392    /// * `top_k` - Number of chunks to include
393    ///
394    /// # Returns
395    /// * `Ok(ContextWindow)` - Assembled context
396    /// * `Err(MemoryError)` - If context assembly fails
397    ///
398    /// # Example
399    /// ```rust,ignore
400    /// let context = memory.get_context("How does RAG improve reasoning?", 5).await?;
401    /// println!("Context: {} chunks, {} tokens",
402    ///     context.chunks.len(),
403    ///     context.token_count);
404    ///
405    /// // Use context in prompt
406    /// let prompt = format!("Context:\n{}\n\nQuestion: ...",
407    ///     context.chunks.iter()
408    ///         .map(|c| &c.text)
409    ///         .collect::<Vec<_>>()
410    ///         .join("\n---\n"));
411    /// ```
412    async fn get_context(&self, query: &str, top_k: usize) -> MemoryResult<ContextWindow>;
413
414    /// Get context with advanced configuration
415    ///
416    /// # Arguments
417    /// * `query` - The reasoning query
418    /// * `config` - Context retrieval configuration
419    ///
420    /// # Returns
421    /// * `Ok(ContextWindow)` - Assembled context
422    /// * `Err(MemoryError)` - If context assembly fails
423    async fn get_context_with_config(
424        &self,
425        query: &str,
426        config: &ContextConfig,
427    ) -> MemoryResult<ContextWindow>;
428
429    /// Get chunks by document ID
430    ///
431    /// # Arguments
432    /// * `doc_id` - The document UUID
433    ///
434    /// # Returns
435    /// * `Ok(Vec<Chunk>)` - All chunks in the document
436    /// * `Err(MemoryError)` - If operation fails
437    async fn get_document_chunks(&self, doc_id: &Uuid) -> MemoryResult<Vec<Chunk>>;
438
439    // ==================== EMBEDDINGS ====================
440
441    /// Embed text and get vector representation
442    ///
443    /// Uses the configured embedding model to convert text to vectors.
444    /// Results are cached where possible.
445    ///
446    /// # Arguments
447    /// * `text` - Text to embed
448    ///
449    /// # Returns
450    /// * `Ok(Vec<f32>)` - The embedding vector
451    /// * `Err(MemoryError)` - If embedding fails
452    async fn embed(&self, text: &str) -> MemoryResult<Vec<f32>>;
453
454    /// Embed multiple texts (batch operation)
455    ///
456    /// # Arguments
457    /// * `texts` - Slice of texts to embed
458    ///
459    /// # Returns
460    /// * `Ok(Vec<Vec<f32>>)` - Embeddings (same order as input)
461    /// * `Err(MemoryError)` - If any embedding fails
462    async fn embed_batch(&self, texts: &[&str]) -> MemoryResult<Vec<Vec<f32>>>;
463
464    // ==================== INDEXING ====================
465
466    /// Build or update indexes
467    ///
468    /// Triggers indexing for documents that haven't been indexed yet.
469    /// Safe to call multiple times (idempotent for already-indexed docs).
470    ///
471    /// # Returns
472    /// * `Ok(())` - If indexing succeeds
473    /// * `Err(MemoryError)` - If indexing fails
474    async fn build_indexes(&self) -> MemoryResult<()>;
475
476    /// Rebuild all indexes from scratch
477    ///
478    /// Use when you suspect corruption or want to optimize.
479    /// This is slower but ensures consistency.
480    ///
481    /// # Returns
482    /// * `Ok(())` - If rebuild succeeds
483    /// * `Err(MemoryError)` - If rebuild fails
484    async fn rebuild_indexes(&self) -> MemoryResult<()>;
485
486    /// Check index health and statistics
487    ///
488    /// # Returns
489    /// * `Ok(IndexStats)` - Index statistics
490    /// * `Err(MemoryError)` - If health check fails
491    async fn check_index_health(&self) -> MemoryResult<IndexStats>;
492
493    // ==================== STATS & HEALTH ====================
494
495    /// Get current memory service statistics
496    ///
497    /// # Returns
498    /// * `Ok(MemoryStats)` - Current statistics
499    /// * `Err(MemoryError)` - If stats retrieval fails
500    async fn stats(&self) -> MemoryResult<MemoryStats>;
501
502    /// Check if memory service is healthy
503    ///
504    /// # Returns
505    /// * `Ok(true)` - Service is operational
506    /// * `Ok(false)` - Service has issues
507    /// * `Err(MemoryError)` - If health check fails
508    async fn is_healthy(&self) -> MemoryResult<bool>;
509
510    // ==================== ADVANCED FEATURES ====================
511
512    /// Clear all data (for testing)
513    ///
514    /// WARNING: This is destructive and irreversible in most implementations.
515    /// Only use for testing.
516    ///
517    /// # Returns
518    /// * `Ok(())` - If clear succeeds
519    /// * `Err(MemoryError)` - If clear fails
520    async fn clear_all(&self) -> MemoryResult<()>;
521}
522
523/// Index statistics from indexing operations
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct IndexStats {
526    /// Number of indexed documents
527    pub indexed_docs: usize,
528    /// Number of indexed chunks
529    pub indexed_chunks: usize,
530    /// Index size in bytes
531    pub index_size_bytes: u64,
532    /// Last indexing timestamp (Unix seconds)
533    pub last_indexed_at: i64,
534    /// Index is valid and consistent
535    pub is_valid: bool,
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn test_context_config_default() {
544        let config = ContextConfig::default();
545        assert_eq!(config.top_k, 10);
546        assert_eq!(config.alpha, 0.7);
547        assert!(!config.use_raptor);
548    }
549
550    #[test]
551    fn test_memory_error_display() {
552        let err = MemoryError {
553            category: ErrorCategory::NotFound,
554            message: "Document not found".to_string(),
555            context: Some("doc_id=123".to_string()),
556        };
557        let display = format!("{}", err);
558        assert!(display.contains("NotFound"));
559        assert!(display.contains("Document not found"));
560    }
561
562    #[test]
563    fn test_context_quality_fields() {
564        let quality = ContextQuality {
565            avg_score: 0.8,
566            max_score: 0.95,
567            min_score: 0.65,
568            diversity: 0.7,
569            coverage: 0.85,
570        };
571
572        assert!(quality.avg_score < quality.max_score);
573        assert!(quality.min_score < quality.avg_score);
574        assert!(quality.diversity >= 0.0 && quality.diversity <= 1.0);
575        assert!(quality.coverage >= 0.0 && quality.coverage <= 1.0);
576    }
577}