1mod algorithms;
5mod corrections;
6mod cross_session;
7mod graph;
8pub(crate) mod importance;
9mod recall;
10mod summarization;
11
12#[cfg(test)]
13mod tests;
14
15use std::sync::Arc;
16use std::sync::atomic::AtomicU64;
17
18use zeph_llm::any::AnyProvider;
19
20use crate::admission::AdmissionControl;
21use crate::embedding_store::EmbeddingStore;
22use crate::error::MemoryError;
23use crate::store::SqliteStore;
24use crate::token_counter::TokenCounter;
25
26pub(crate) const SESSION_SUMMARIES_COLLECTION: &str = "zeph_session_summaries";
27pub(crate) const KEY_FACTS_COLLECTION: &str = "zeph_key_facts";
28pub(crate) const CORRECTIONS_COLLECTION: &str = "zeph_corrections";
29
30pub use algorithms::{apply_mmr, apply_temporal_decay};
31pub use cross_session::SessionSummaryResult;
32pub use graph::{
33 ExtractionResult, ExtractionStats, GraphExtractionConfig, LinkingStats, NoteLinkingConfig,
34 PostExtractValidator, extract_and_store, link_memory_notes,
35};
36pub use recall::RecalledMessage;
37pub use summarization::{StructuredSummary, Summary, build_summarization_prompt};
38
39pub struct SemanticMemory {
40 pub(crate) sqlite: SqliteStore,
41 pub(crate) qdrant: Option<Arc<EmbeddingStore>>,
42 pub(crate) provider: AnyProvider,
43 pub(crate) embedding_model: String,
44 pub(crate) vector_weight: f64,
45 pub(crate) keyword_weight: f64,
46 pub(crate) temporal_decay_enabled: bool,
47 pub(crate) temporal_decay_half_life_days: u32,
48 pub(crate) mmr_enabled: bool,
49 pub(crate) mmr_lambda: f32,
50 pub(crate) importance_enabled: bool,
51 pub(crate) importance_weight: f64,
52 pub(crate) tier_boost_semantic: f64,
55 pub token_counter: Arc<TokenCounter>,
56 pub graph_store: Option<Arc<crate::graph::GraphStore>>,
57 pub(crate) community_detection_failures: Arc<AtomicU64>,
58 pub(crate) graph_extraction_count: Arc<AtomicU64>,
59 pub(crate) graph_extraction_failures: Arc<AtomicU64>,
60 pub(crate) admission_control: Option<Arc<AdmissionControl>>,
62}
63
64impl SemanticMemory {
65 pub async fn new(
76 sqlite_path: &str,
77 qdrant_url: &str,
78 provider: AnyProvider,
79 embedding_model: &str,
80 ) -> Result<Self, MemoryError> {
81 Self::with_weights(sqlite_path, qdrant_url, provider, embedding_model, 0.7, 0.3).await
82 }
83
84 pub async fn with_weights(
93 sqlite_path: &str,
94 qdrant_url: &str,
95 provider: AnyProvider,
96 embedding_model: &str,
97 vector_weight: f64,
98 keyword_weight: f64,
99 ) -> Result<Self, MemoryError> {
100 Self::with_weights_and_pool_size(
101 sqlite_path,
102 qdrant_url,
103 provider,
104 embedding_model,
105 vector_weight,
106 keyword_weight,
107 5,
108 )
109 .await
110 }
111
112 pub async fn with_weights_and_pool_size(
121 sqlite_path: &str,
122 qdrant_url: &str,
123 provider: AnyProvider,
124 embedding_model: &str,
125 vector_weight: f64,
126 keyword_weight: f64,
127 pool_size: u32,
128 ) -> Result<Self, MemoryError> {
129 let sqlite = SqliteStore::with_pool_size(sqlite_path, pool_size).await?;
130 let pool = sqlite.pool().clone();
131
132 let qdrant = match EmbeddingStore::new(qdrant_url, pool) {
133 Ok(store) => Some(Arc::new(store)),
134 Err(e) => {
135 tracing::warn!("Qdrant unavailable, semantic search disabled: {e:#}");
136 None
137 }
138 };
139
140 Ok(Self {
141 sqlite,
142 qdrant,
143 provider,
144 embedding_model: embedding_model.into(),
145 vector_weight,
146 keyword_weight,
147 temporal_decay_enabled: false,
148 temporal_decay_half_life_days: 30,
149 mmr_enabled: false,
150 mmr_lambda: 0.7,
151 importance_enabled: false,
152 importance_weight: 0.15,
153 tier_boost_semantic: 1.3,
154 token_counter: Arc::new(TokenCounter::new()),
155 graph_store: None,
156 community_detection_failures: Arc::new(AtomicU64::new(0)),
157 graph_extraction_count: Arc::new(AtomicU64::new(0)),
158 graph_extraction_failures: Arc::new(AtomicU64::new(0)),
159 admission_control: None,
160 })
161 }
162
163 pub async fn with_qdrant_ops(
172 sqlite_path: &str,
173 ops: crate::QdrantOps,
174 provider: AnyProvider,
175 embedding_model: &str,
176 vector_weight: f64,
177 keyword_weight: f64,
178 pool_size: u32,
179 ) -> Result<Self, MemoryError> {
180 let sqlite = SqliteStore::with_pool_size(sqlite_path, pool_size).await?;
181 let pool = sqlite.pool().clone();
182 let store = EmbeddingStore::with_store(Box::new(ops), pool);
183
184 Ok(Self {
185 sqlite,
186 qdrant: Some(Arc::new(store)),
187 provider,
188 embedding_model: embedding_model.into(),
189 vector_weight,
190 keyword_weight,
191 temporal_decay_enabled: false,
192 temporal_decay_half_life_days: 30,
193 mmr_enabled: false,
194 mmr_lambda: 0.7,
195 importance_enabled: false,
196 importance_weight: 0.15,
197 tier_boost_semantic: 1.3,
198 token_counter: Arc::new(TokenCounter::new()),
199 graph_store: None,
200 community_detection_failures: Arc::new(AtomicU64::new(0)),
201 graph_extraction_count: Arc::new(AtomicU64::new(0)),
202 graph_extraction_failures: Arc::new(AtomicU64::new(0)),
203 admission_control: None,
204 })
205 }
206
207 #[must_use]
212 pub fn with_graph_store(mut self, store: Arc<crate::graph::GraphStore>) -> Self {
213 self.graph_store = Some(store);
214 self
215 }
216
217 #[must_use]
219 pub fn community_detection_failures(&self) -> u64 {
220 use std::sync::atomic::Ordering;
221 self.community_detection_failures.load(Ordering::Relaxed)
222 }
223
224 #[must_use]
226 pub fn graph_extraction_count(&self) -> u64 {
227 use std::sync::atomic::Ordering;
228 self.graph_extraction_count.load(Ordering::Relaxed)
229 }
230
231 #[must_use]
233 pub fn graph_extraction_failures(&self) -> u64 {
234 use std::sync::atomic::Ordering;
235 self.graph_extraction_failures.load(Ordering::Relaxed)
236 }
237
238 #[must_use]
240 pub fn with_ranking_options(
241 mut self,
242 temporal_decay_enabled: bool,
243 temporal_decay_half_life_days: u32,
244 mmr_enabled: bool,
245 mmr_lambda: f32,
246 ) -> Self {
247 self.temporal_decay_enabled = temporal_decay_enabled;
248 self.temporal_decay_half_life_days = temporal_decay_half_life_days;
249 self.mmr_enabled = mmr_enabled;
250 self.mmr_lambda = mmr_lambda;
251 self
252 }
253
254 #[must_use]
256 pub fn with_importance_options(mut self, enabled: bool, weight: f64) -> Self {
257 self.importance_enabled = enabled;
258 self.importance_weight = weight;
259 self
260 }
261
262 #[must_use]
266 pub fn with_tier_boost(mut self, boost: f64) -> Self {
267 self.tier_boost_semantic = boost;
268 self
269 }
270
271 #[must_use]
276 pub fn with_admission_control(mut self, control: AdmissionControl) -> Self {
277 self.admission_control = Some(Arc::new(control));
278 self
279 }
280
281 #[must_use]
285 pub fn from_parts(
286 sqlite: SqliteStore,
287 qdrant: Option<Arc<EmbeddingStore>>,
288 provider: AnyProvider,
289 embedding_model: impl Into<String>,
290 vector_weight: f64,
291 keyword_weight: f64,
292 token_counter: Arc<TokenCounter>,
293 ) -> Self {
294 Self {
295 sqlite,
296 qdrant,
297 provider,
298 embedding_model: embedding_model.into(),
299 vector_weight,
300 keyword_weight,
301 temporal_decay_enabled: false,
302 temporal_decay_half_life_days: 30,
303 mmr_enabled: false,
304 mmr_lambda: 0.7,
305 importance_enabled: false,
306 importance_weight: 0.15,
307 tier_boost_semantic: 1.3,
308 token_counter,
309 graph_store: None,
310 community_detection_failures: Arc::new(AtomicU64::new(0)),
311 graph_extraction_count: Arc::new(AtomicU64::new(0)),
312 graph_extraction_failures: Arc::new(AtomicU64::new(0)),
313 admission_control: None,
314 }
315 }
316
317 pub async fn with_sqlite_backend(
323 sqlite_path: &str,
324 provider: AnyProvider,
325 embedding_model: &str,
326 vector_weight: f64,
327 keyword_weight: f64,
328 ) -> Result<Self, MemoryError> {
329 Self::with_sqlite_backend_and_pool_size(
330 sqlite_path,
331 provider,
332 embedding_model,
333 vector_weight,
334 keyword_weight,
335 5,
336 )
337 .await
338 }
339
340 pub async fn with_sqlite_backend_and_pool_size(
346 sqlite_path: &str,
347 provider: AnyProvider,
348 embedding_model: &str,
349 vector_weight: f64,
350 keyword_weight: f64,
351 pool_size: u32,
352 ) -> Result<Self, MemoryError> {
353 let sqlite = SqliteStore::with_pool_size(sqlite_path, pool_size).await?;
354 let pool = sqlite.pool().clone();
355 let store = EmbeddingStore::new_sqlite(pool);
356
357 Ok(Self {
358 sqlite,
359 qdrant: Some(Arc::new(store)),
360 provider,
361 embedding_model: embedding_model.into(),
362 vector_weight,
363 keyword_weight,
364 temporal_decay_enabled: false,
365 temporal_decay_half_life_days: 30,
366 mmr_enabled: false,
367 mmr_lambda: 0.7,
368 importance_enabled: false,
369 importance_weight: 0.15,
370 tier_boost_semantic: 1.3,
371 token_counter: Arc::new(TokenCounter::new()),
372 graph_store: None,
373 community_detection_failures: Arc::new(AtomicU64::new(0)),
374 graph_extraction_count: Arc::new(AtomicU64::new(0)),
375 graph_extraction_failures: Arc::new(AtomicU64::new(0)),
376 admission_control: None,
377 })
378 }
379
380 #[must_use]
382 pub fn sqlite(&self) -> &SqliteStore {
383 &self.sqlite
384 }
385
386 pub async fn is_vector_store_connected(&self) -> bool {
391 match self.qdrant.as_ref() {
392 Some(store) => store.health_check().await,
393 None => false,
394 }
395 }
396
397 #[must_use]
399 pub fn has_vector_store(&self) -> bool {
400 self.qdrant.is_some()
401 }
402
403 #[must_use]
405 pub fn embedding_store(&self) -> Option<&Arc<EmbeddingStore>> {
406 self.qdrant.as_ref()
407 }
408
409 pub fn provider(&self) -> &AnyProvider {
411 &self.provider
412 }
413
414 pub async fn message_count(
420 &self,
421 conversation_id: crate::types::ConversationId,
422 ) -> Result<i64, MemoryError> {
423 self.sqlite.count_messages(conversation_id).await
424 }
425
426 pub async fn unsummarized_message_count(
432 &self,
433 conversation_id: crate::types::ConversationId,
434 ) -> Result<i64, MemoryError> {
435 let after_id = self
436 .sqlite
437 .latest_summary_last_message_id(conversation_id)
438 .await?
439 .unwrap_or(crate::types::MessageId(0));
440 self.sqlite
441 .count_messages_after(conversation_id, after_id)
442 .await
443 }
444}