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}