1use zeph_llm::any::AnyProvider;
2use zeph_llm::provider::{LlmProvider, Message, Role};
3
4use crate::embedding_store::{EmbeddingStore, MessageKind, SearchFilter};
5use crate::error::MemoryError;
6use crate::sqlite::SqliteStore;
7use crate::types::{ConversationId, MessageId};
8use crate::vector_store::{FieldCondition, FieldValue, VectorFilter};
9
10const SESSION_SUMMARIES_COLLECTION: &str = "zeph_session_summaries";
11const KEY_FACTS_COLLECTION: &str = "zeph_key_facts";
12
13#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
14pub struct StructuredSummary {
15 pub summary: String,
16 pub key_facts: Vec<String>,
17 pub entities: Vec<String>,
18}
19
20#[derive(Debug)]
21pub struct RecalledMessage {
22 pub message: Message,
23 pub score: f32,
24}
25
26#[derive(Debug, Clone)]
27pub struct Summary {
28 pub id: i64,
29 pub conversation_id: ConversationId,
30 pub content: String,
31 pub first_message_id: MessageId,
32 pub last_message_id: MessageId,
33 pub token_estimate: i64,
34}
35
36#[derive(Debug, Clone)]
37pub struct SessionSummaryResult {
38 pub summary_text: String,
39 pub score: f32,
40 pub conversation_id: ConversationId,
41}
42
43#[must_use]
45pub fn estimate_tokens(text: &str) -> usize {
46 text.len() / 3
47}
48
49fn build_summarization_prompt(messages: &[(MessageId, String, String)]) -> String {
50 let mut prompt = String::from(
51 "Summarize the following conversation. Extract key facts, decisions, entities, \
52 and context needed to continue the conversation.\n\n\
53 Respond in JSON with fields: summary (string), key_facts (list of strings), \
54 entities (list of strings).\n\nConversation:\n",
55 );
56
57 for (_, role, content) in messages {
58 prompt.push_str(role);
59 prompt.push_str(": ");
60 prompt.push_str(content);
61 prompt.push('\n');
62 }
63
64 prompt
65}
66
67pub struct SemanticMemory {
68 sqlite: SqliteStore,
69 qdrant: Option<EmbeddingStore>,
70 provider: AnyProvider,
71 embedding_model: String,
72 vector_weight: f64,
73 keyword_weight: f64,
74}
75
76impl SemanticMemory {
77 pub async fn new(
85 sqlite_path: &str,
86 qdrant_url: &str,
87 provider: AnyProvider,
88 embedding_model: &str,
89 ) -> Result<Self, MemoryError> {
90 Self::with_weights(sqlite_path, qdrant_url, provider, embedding_model, 0.7, 0.3).await
91 }
92
93 pub async fn with_weights(
99 sqlite_path: &str,
100 qdrant_url: &str,
101 provider: AnyProvider,
102 embedding_model: &str,
103 vector_weight: f64,
104 keyword_weight: f64,
105 ) -> Result<Self, MemoryError> {
106 let sqlite = SqliteStore::new(sqlite_path).await?;
107 let pool = sqlite.pool().clone();
108
109 let qdrant = match EmbeddingStore::new(qdrant_url, pool) {
110 Ok(store) => Some(store),
111 Err(e) => {
112 tracing::warn!("Qdrant unavailable, semantic search disabled: {e:#}");
113 None
114 }
115 };
116
117 Ok(Self {
118 sqlite,
119 qdrant,
120 provider,
121 embedding_model: embedding_model.into(),
122 vector_weight,
123 keyword_weight,
124 })
125 }
126
127 pub async fn remember(
136 &self,
137 conversation_id: ConversationId,
138 role: &str,
139 content: &str,
140 ) -> Result<MessageId, MemoryError> {
141 let message_id = self
142 .sqlite
143 .save_message(conversation_id, role, content)
144 .await?;
145
146 if let Some(qdrant) = &self.qdrant
147 && self.provider.supports_embeddings()
148 {
149 match self.provider.embed(content).await {
150 Ok(vector) => {
151 let vector_size = u64::try_from(vector.len()).unwrap_or(896);
153 if let Err(e) = qdrant.ensure_collection(vector_size).await {
154 tracing::warn!("Failed to ensure Qdrant collection: {e:#}");
155 } else if let Err(e) = qdrant
156 .store(
157 message_id,
158 conversation_id,
159 role,
160 vector,
161 MessageKind::Regular,
162 &self.embedding_model,
163 )
164 .await
165 {
166 tracing::warn!("Failed to store embedding: {e:#}");
167 }
168 }
169 Err(e) => {
170 tracing::warn!("Failed to generate embedding: {e:#}");
171 }
172 }
173 }
174
175 Ok(message_id)
176 }
177
178 pub async fn remember_with_parts(
187 &self,
188 conversation_id: ConversationId,
189 role: &str,
190 content: &str,
191 parts_json: &str,
192 ) -> Result<(MessageId, bool), MemoryError> {
193 let message_id = self
194 .sqlite
195 .save_message_with_parts(conversation_id, role, content, parts_json)
196 .await?;
197
198 let mut embedding_stored = false;
199
200 if let Some(qdrant) = &self.qdrant
201 && self.provider.supports_embeddings()
202 {
203 match self.provider.embed(content).await {
204 Ok(vector) => {
205 let vector_size = u64::try_from(vector.len()).unwrap_or(896);
206 if let Err(e) = qdrant.ensure_collection(vector_size).await {
207 tracing::warn!("Failed to ensure Qdrant collection: {e:#}");
208 } else if let Err(e) = qdrant
209 .store(
210 message_id,
211 conversation_id,
212 role,
213 vector,
214 MessageKind::Regular,
215 &self.embedding_model,
216 )
217 .await
218 {
219 tracing::warn!("Failed to store embedding: {e:#}");
220 } else {
221 embedding_stored = true;
222 }
223 }
224 Err(e) => {
225 tracing::warn!("Failed to generate embedding: {e:#}");
226 }
227 }
228 }
229
230 Ok((message_id, embedding_stored))
231 }
232
233 pub async fn recall(
243 &self,
244 query: &str,
245 limit: usize,
246 filter: Option<SearchFilter>,
247 ) -> Result<Vec<RecalledMessage>, MemoryError> {
248 let conversation_id = filter.as_ref().and_then(|f| f.conversation_id);
249
250 let keyword_results = match self
252 .sqlite
253 .keyword_search(query, limit * 2, conversation_id)
254 .await
255 {
256 Ok(results) => results,
257 Err(e) => {
258 tracing::warn!("FTS5 keyword search failed: {e:#}");
259 Vec::new()
260 }
261 };
262
263 let vector_results = if let Some(qdrant) = &self.qdrant
265 && self.provider.supports_embeddings()
266 {
267 let query_vector = self.provider.embed(query).await?;
268 let vector_size = u64::try_from(query_vector.len()).unwrap_or(896);
269 qdrant.ensure_collection(vector_size).await?;
270 qdrant.search(&query_vector, limit * 2, filter).await?
271 } else {
272 Vec::new()
273 };
274
275 let mut scores: std::collections::HashMap<MessageId, f64> =
277 std::collections::HashMap::new();
278
279 if !vector_results.is_empty() {
280 let max_vs = vector_results
281 .iter()
282 .map(|r| r.score)
283 .fold(f32::NEG_INFINITY, f32::max);
284 let norm = if max_vs > 0.0 { max_vs } else { 1.0 };
285 for r in &vector_results {
286 let normalized = f64::from(r.score / norm);
287 *scores.entry(r.message_id).or_default() += normalized * self.vector_weight;
288 }
289 }
290
291 if !keyword_results.is_empty() {
292 let max_ks = keyword_results
293 .iter()
294 .map(|r| r.1)
295 .fold(f64::NEG_INFINITY, f64::max);
296 let norm = if max_ks > 0.0 { max_ks } else { 1.0 };
297 for &(msg_id, score) in &keyword_results {
298 let normalized = score / norm;
299 *scores.entry(msg_id).or_default() += normalized * self.keyword_weight;
300 }
301 }
302
303 if scores.is_empty() {
304 return Ok(Vec::new());
305 }
306
307 let mut ranked: Vec<(MessageId, f64)> = scores.into_iter().collect();
309 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
310 ranked.truncate(limit);
311
312 let ids: Vec<MessageId> = ranked.iter().map(|r| r.0).collect();
313 let messages = self.sqlite.messages_by_ids(&ids).await?;
314 let msg_map: std::collections::HashMap<MessageId, _> = messages.into_iter().collect();
315
316 let recalled = ranked
317 .iter()
318 .filter_map(|(msg_id, score)| {
319 msg_map.get(msg_id).map(|msg| RecalledMessage {
320 message: msg.clone(),
321 #[expect(clippy::cast_possible_truncation)]
322 score: *score as f32,
323 })
324 })
325 .collect();
326
327 Ok(recalled)
328 }
329
330 pub async fn has_embedding(&self, message_id: MessageId) -> Result<bool, MemoryError> {
336 match &self.qdrant {
337 Some(qdrant) => qdrant.has_embedding(message_id).await,
338 None => Ok(false),
339 }
340 }
341
342 pub async fn embed_missing(&self) -> Result<usize, MemoryError> {
351 let Some(qdrant) = &self.qdrant else {
352 return Ok(0);
353 };
354 if !self.provider.supports_embeddings() {
355 return Ok(0);
356 }
357
358 let unembedded = self.sqlite.unembedded_message_ids(Some(1000)).await?;
359
360 if unembedded.is_empty() {
361 return Ok(0);
362 }
363
364 let probe = self.provider.embed("probe").await?;
365 let vector_size = u64::try_from(probe.len())?;
366 qdrant.ensure_collection(vector_size).await?;
367
368 let mut count = 0;
369 for (msg_id, conversation_id, role, content) in &unembedded {
370 match self.provider.embed(content).await {
371 Ok(vector) => {
372 if let Err(e) = qdrant
373 .store(
374 *msg_id,
375 *conversation_id,
376 role,
377 vector,
378 MessageKind::Regular,
379 &self.embedding_model,
380 )
381 .await
382 {
383 tracing::warn!("Failed to store embedding for msg {msg_id}: {e:#}");
384 continue;
385 }
386 count += 1;
387 }
388 Err(e) => {
389 tracing::warn!("Failed to embed msg {msg_id}: {e:#}");
390 }
391 }
392 }
393
394 tracing::info!("Embedded {count}/{} missing messages", unembedded.len());
395 Ok(count)
396 }
397
398 pub async fn store_session_summary(
404 &self,
405 conversation_id: ConversationId,
406 summary_text: &str,
407 ) -> Result<(), MemoryError> {
408 let Some(qdrant) = &self.qdrant else {
409 return Ok(());
410 };
411 if !self.provider.supports_embeddings() {
412 return Ok(());
413 }
414
415 let vector = self.provider.embed(summary_text).await?;
416 let vector_size = u64::try_from(vector.len()).unwrap_or(896);
417 qdrant
418 .ensure_named_collection(SESSION_SUMMARIES_COLLECTION, vector_size)
419 .await?;
420
421 let payload = serde_json::json!({
422 "conversation_id": conversation_id.0,
423 "summary_text": summary_text,
424 });
425
426 qdrant
427 .store_to_collection(SESSION_SUMMARIES_COLLECTION, payload, vector)
428 .await?;
429
430 tracing::debug!(
431 conversation_id = conversation_id.0,
432 "stored session summary"
433 );
434 Ok(())
435 }
436
437 pub async fn search_session_summaries(
443 &self,
444 query: &str,
445 limit: usize,
446 exclude_conversation_id: Option<ConversationId>,
447 ) -> Result<Vec<SessionSummaryResult>, MemoryError> {
448 let Some(qdrant) = &self.qdrant else {
449 return Ok(Vec::new());
450 };
451 if !self.provider.supports_embeddings() {
452 return Ok(Vec::new());
453 }
454
455 let vector = self.provider.embed(query).await?;
456 let vector_size = u64::try_from(vector.len()).unwrap_or(896);
457 qdrant
458 .ensure_named_collection(SESSION_SUMMARIES_COLLECTION, vector_size)
459 .await?;
460
461 let filter = exclude_conversation_id.map(|cid| VectorFilter {
462 must: vec![],
463 must_not: vec![FieldCondition {
464 field: "conversation_id".into(),
465 value: FieldValue::Integer(cid.0),
466 }],
467 });
468
469 let points = qdrant
470 .search_collection(SESSION_SUMMARIES_COLLECTION, &vector, limit, filter)
471 .await?;
472
473 let results = points
474 .into_iter()
475 .filter_map(|point| {
476 let summary_text = point.payload.get("summary_text")?.as_str()?.to_owned();
477 let conversation_id =
478 ConversationId(point.payload.get("conversation_id")?.as_i64()?);
479 Some(SessionSummaryResult {
480 summary_text,
481 score: point.score,
482 conversation_id,
483 })
484 })
485 .collect();
486
487 Ok(results)
488 }
489
490 #[must_use]
492 pub fn sqlite(&self) -> &SqliteStore {
493 &self.sqlite
494 }
495
496 #[must_use]
498 pub fn has_qdrant(&self) -> bool {
499 self.qdrant.is_some()
500 }
501
502 pub async fn message_count(&self, conversation_id: ConversationId) -> Result<i64, MemoryError> {
508 self.sqlite.count_messages(conversation_id).await
509 }
510
511 pub async fn unsummarized_message_count(
517 &self,
518 conversation_id: ConversationId,
519 ) -> Result<i64, MemoryError> {
520 let after_id = self
521 .sqlite
522 .latest_summary_last_message_id(conversation_id)
523 .await?
524 .unwrap_or(MessageId(0));
525 self.sqlite
526 .count_messages_after(conversation_id, after_id)
527 .await
528 }
529
530 pub async fn load_summaries(
536 &self,
537 conversation_id: ConversationId,
538 ) -> Result<Vec<Summary>, MemoryError> {
539 let rows = self.sqlite.load_summaries(conversation_id).await?;
540 let summaries = rows
541 .into_iter()
542 .map(
543 |(
544 id,
545 conversation_id,
546 content,
547 first_message_id,
548 last_message_id,
549 token_estimate,
550 )| {
551 Summary {
552 id,
553 conversation_id,
554 content,
555 first_message_id,
556 last_message_id,
557 token_estimate,
558 }
559 },
560 )
561 .collect();
562 Ok(summaries)
563 }
564
565 pub async fn summarize(
573 &self,
574 conversation_id: ConversationId,
575 message_count: usize,
576 ) -> Result<Option<i64>, MemoryError> {
577 let total = self.sqlite.count_messages(conversation_id).await?;
578
579 if total <= i64::try_from(message_count)? {
580 return Ok(None);
581 }
582
583 let after_id = self
584 .sqlite
585 .latest_summary_last_message_id(conversation_id)
586 .await?
587 .unwrap_or(MessageId(0));
588
589 let messages = self
590 .sqlite
591 .load_messages_range(conversation_id, after_id, message_count)
592 .await?;
593
594 if messages.is_empty() {
595 return Ok(None);
596 }
597
598 let prompt = build_summarization_prompt(&messages);
599 let chat_messages = vec![Message {
600 role: Role::User,
601 content: prompt,
602 parts: vec![],
603 }];
604
605 let structured = match self
606 .provider
607 .chat_typed_erased::<StructuredSummary>(&chat_messages)
608 .await
609 {
610 Ok(s) => s,
611 Err(e) => {
612 tracing::warn!(
613 "structured summarization failed, falling back to plain text: {e:#}"
614 );
615 let plain = self.provider.chat(&chat_messages).await?;
616 StructuredSummary {
617 summary: plain,
618 key_facts: vec![],
619 entities: vec![],
620 }
621 }
622 };
623 let summary_text = &structured.summary;
624
625 let token_estimate = i64::try_from(estimate_tokens(summary_text))?;
626 let first_message_id = messages[0].0;
627 let last_message_id = messages[messages.len() - 1].0;
628
629 let summary_id = self
630 .sqlite
631 .save_summary(
632 conversation_id,
633 summary_text,
634 first_message_id,
635 last_message_id,
636 token_estimate,
637 )
638 .await?;
639
640 if let Some(qdrant) = &self.qdrant
641 && self.provider.supports_embeddings()
642 {
643 match self.provider.embed(summary_text).await {
644 Ok(vector) => {
645 let vector_size = u64::try_from(vector.len()).unwrap_or(896);
647 if let Err(e) = qdrant.ensure_collection(vector_size).await {
648 tracing::warn!("Failed to ensure Qdrant collection: {e:#}");
649 } else if let Err(e) = qdrant
650 .store(
651 MessageId(summary_id),
652 conversation_id,
653 "system",
654 vector,
655 MessageKind::Summary,
656 &self.embedding_model,
657 )
658 .await
659 {
660 tracing::warn!("Failed to embed summary: {e:#}");
661 }
662 }
663 Err(e) => {
664 tracing::warn!("Failed to generate summary embedding: {e:#}");
665 }
666 }
667 }
668
669 if !structured.key_facts.is_empty() {
671 self.store_key_facts(conversation_id, summary_id, &structured.key_facts)
672 .await;
673 }
674
675 Ok(Some(summary_id))
676 }
677
678 async fn store_key_facts(
679 &self,
680 conversation_id: ConversationId,
681 source_summary_id: i64,
682 key_facts: &[String],
683 ) {
684 let Some(qdrant) = &self.qdrant else {
685 return;
686 };
687 if !self.provider.supports_embeddings() {
688 return;
689 }
690
691 let Some(first_fact) = key_facts.first() else {
692 return;
693 };
694 let first_vector = match self.provider.embed(first_fact).await {
695 Ok(v) => v,
696 Err(e) => {
697 tracing::warn!("Failed to embed key fact: {e:#}");
698 return;
699 }
700 };
701 let vector_size = u64::try_from(first_vector.len()).unwrap_or(896);
702 if let Err(e) = qdrant
703 .ensure_named_collection(KEY_FACTS_COLLECTION, vector_size)
704 .await
705 {
706 tracing::warn!("Failed to ensure key_facts collection: {e:#}");
707 return;
708 }
709
710 let first_payload = serde_json::json!({
711 "conversation_id": conversation_id.0,
712 "fact_text": first_fact,
713 "source_summary_id": source_summary_id,
714 });
715 if let Err(e) = qdrant
716 .store_to_collection(KEY_FACTS_COLLECTION, first_payload, first_vector)
717 .await
718 {
719 tracing::warn!("Failed to store key fact: {e:#}");
720 }
721
722 for fact in &key_facts[1..] {
723 match self.provider.embed(fact).await {
724 Ok(vector) => {
725 let payload = serde_json::json!({
726 "conversation_id": conversation_id.0,
727 "fact_text": fact,
728 "source_summary_id": source_summary_id,
729 });
730 if let Err(e) = qdrant
731 .store_to_collection(KEY_FACTS_COLLECTION, payload, vector)
732 .await
733 {
734 tracing::warn!("Failed to store key fact: {e:#}");
735 }
736 }
737 Err(e) => {
738 tracing::warn!("Failed to embed key fact: {e:#}");
739 }
740 }
741 }
742 }
743
744 pub async fn search_key_facts(
750 &self,
751 query: &str,
752 limit: usize,
753 ) -> Result<Vec<String>, MemoryError> {
754 let Some(qdrant) = &self.qdrant else {
755 return Ok(Vec::new());
756 };
757 if !self.provider.supports_embeddings() {
758 return Ok(Vec::new());
759 }
760
761 let vector = self.provider.embed(query).await?;
762 let vector_size = u64::try_from(vector.len()).unwrap_or(896);
763 qdrant
764 .ensure_named_collection(KEY_FACTS_COLLECTION, vector_size)
765 .await?;
766
767 let points = qdrant
768 .search_collection(KEY_FACTS_COLLECTION, &vector, limit, None)
769 .await?;
770
771 let facts = points
772 .into_iter()
773 .filter_map(|p| p.payload.get("fact_text")?.as_str().map(String::from))
774 .collect();
775
776 Ok(facts)
777 }
778}
779
780#[cfg(test)]
781mod tests {
782 use zeph_llm::mock::MockProvider;
783 use zeph_llm::provider::Role;
784
785 use super::*;
786
787 fn test_provider() -> AnyProvider {
788 AnyProvider::Mock(MockProvider::default())
789 }
790
791 async fn test_semantic_memory(_supports_embeddings: bool) -> SemanticMemory {
792 let provider = test_provider();
793 let sqlite = SqliteStore::new(":memory:").await.unwrap();
794
795 SemanticMemory {
796 sqlite,
797 qdrant: None,
798 provider,
799 embedding_model: "test-model".into(),
800 vector_weight: 0.7,
801 keyword_weight: 0.3,
802 }
803 }
804
805 #[tokio::test]
806 async fn remember_saves_to_sqlite() {
807 let memory = test_semantic_memory(false).await;
808
809 let cid = memory.sqlite.create_conversation().await.unwrap();
810 let msg_id = memory.remember(cid, "user", "hello").await.unwrap();
811
812 assert_eq!(msg_id, MessageId(1));
813
814 let history = memory.sqlite.load_history(cid, 50).await.unwrap();
815 assert_eq!(history.len(), 1);
816 assert_eq!(history[0].role, Role::User);
817 assert_eq!(history[0].content, "hello");
818 }
819
820 #[tokio::test]
821 async fn remember_with_parts_saves_parts_json() {
822 let memory = test_semantic_memory(false).await;
823 let cid = memory.sqlite.create_conversation().await.unwrap();
824
825 let parts_json =
826 r#"[{"kind":"ToolOutput","tool_name":"shell","body":"hello","compacted_at":null}]"#;
827 let (msg_id, _embedding_stored) = memory
828 .remember_with_parts(cid, "assistant", "tool output", parts_json)
829 .await
830 .unwrap();
831 assert!(msg_id > MessageId(0));
832
833 let history = memory.sqlite.load_history(cid, 50).await.unwrap();
834 assert_eq!(history.len(), 1);
835 assert_eq!(history[0].content, "tool output");
836 }
837
838 #[tokio::test]
839 async fn recall_returns_empty_without_qdrant() {
840 let memory = test_semantic_memory(true).await;
841
842 let recalled = memory.recall("test", 5, None).await.unwrap();
843 assert!(recalled.is_empty());
844 }
845
846 #[tokio::test]
847 async fn has_embedding_without_qdrant() {
848 let memory = test_semantic_memory(true).await;
849
850 let has_embedding = memory.has_embedding(MessageId(1)).await.unwrap();
851 assert!(!has_embedding);
852 }
853
854 #[tokio::test]
855 async fn embed_missing_without_qdrant() {
856 let memory = test_semantic_memory(true).await;
857
858 let count = memory.embed_missing().await.unwrap();
859 assert_eq!(count, 0);
860 }
861
862 #[tokio::test]
863 async fn sqlite_accessor() {
864 let memory = test_semantic_memory(false).await;
865
866 let cid = memory.sqlite().create_conversation().await.unwrap();
867 assert_eq!(cid, ConversationId(1));
868
869 memory
870 .sqlite()
871 .save_message(cid, "user", "test")
872 .await
873 .unwrap();
874
875 let history = memory.sqlite().load_history(cid, 50).await.unwrap();
876 assert_eq!(history.len(), 1);
877 }
878
879 #[tokio::test]
880 async fn has_qdrant_returns_false_when_unavailable() {
881 let memory = test_semantic_memory(false).await;
882 assert!(!memory.has_qdrant());
883 }
884
885 #[tokio::test]
886 async fn recall_returns_empty_when_embeddings_not_supported() {
887 let memory = test_semantic_memory(false).await;
888
889 let recalled = memory.recall("test", 5, None).await.unwrap();
890 assert!(recalled.is_empty());
891 }
892
893 #[tokio::test]
894 async fn embed_missing_returns_zero_when_embeddings_not_supported() {
895 let memory = test_semantic_memory(false).await;
896
897 let cid = memory.sqlite().create_conversation().await.unwrap();
898 memory
899 .sqlite()
900 .save_message(cid, "user", "test")
901 .await
902 .unwrap();
903
904 let count = memory.embed_missing().await.unwrap();
905 assert_eq!(count, 0);
906 }
907
908 #[test]
909 fn estimate_tokens_ascii() {
910 let text = "Hello, world!";
911 assert_eq!(estimate_tokens(text), 4);
912 }
913
914 #[test]
915 fn estimate_tokens_unicode() {
916 let text = "Привет мир";
917 assert_eq!(estimate_tokens(text), 6);
918 }
919
920 #[test]
921 fn estimate_tokens_empty() {
922 assert_eq!(estimate_tokens(""), 0);
923 }
924
925 #[tokio::test]
926 async fn message_count_empty_conversation() {
927 let memory = test_semantic_memory(false).await;
928 let cid = memory.sqlite().create_conversation().await.unwrap();
929
930 let count = memory.message_count(cid).await.unwrap();
931 assert_eq!(count, 0);
932 }
933
934 #[tokio::test]
935 async fn message_count_after_saves() {
936 let memory = test_semantic_memory(false).await;
937 let cid = memory.sqlite().create_conversation().await.unwrap();
938
939 memory.remember(cid, "user", "msg1").await.unwrap();
940 memory.remember(cid, "assistant", "msg2").await.unwrap();
941
942 let count = memory.message_count(cid).await.unwrap();
943 assert_eq!(count, 2);
944 }
945
946 #[tokio::test]
947 async fn unsummarized_count_decreases_after_summary() {
948 let memory = test_semantic_memory(false).await;
949 let cid = memory.sqlite().create_conversation().await.unwrap();
950
951 for i in 0..10 {
952 memory
953 .remember(cid, "user", &format!("msg{i}"))
954 .await
955 .unwrap();
956 }
957 assert_eq!(memory.unsummarized_message_count(cid).await.unwrap(), 10);
958
959 memory.summarize(cid, 5).await.unwrap();
960
961 assert!(memory.unsummarized_message_count(cid).await.unwrap() < 10);
962 assert_eq!(memory.message_count(cid).await.unwrap(), 10);
963 }
964
965 #[tokio::test]
966 async fn load_summaries_empty() {
967 let memory = test_semantic_memory(false).await;
968 let cid = memory.sqlite().create_conversation().await.unwrap();
969
970 let summaries = memory.load_summaries(cid).await.unwrap();
971 assert!(summaries.is_empty());
972 }
973
974 #[tokio::test]
975 async fn load_summaries_ordered() {
976 let memory = test_semantic_memory(false).await;
977 let cid = memory.sqlite().create_conversation().await.unwrap();
978
979 let msg_id1 = memory.remember(cid, "user", "m1").await.unwrap();
980 let msg_id2 = memory.remember(cid, "assistant", "m2").await.unwrap();
981 let msg_id3 = memory.remember(cid, "user", "m3").await.unwrap();
982
983 let s1 = memory
984 .sqlite()
985 .save_summary(cid, "summary1", msg_id1, msg_id2, 3)
986 .await
987 .unwrap();
988 let s2 = memory
989 .sqlite()
990 .save_summary(cid, "summary2", msg_id2, msg_id3, 3)
991 .await
992 .unwrap();
993
994 let summaries = memory.load_summaries(cid).await.unwrap();
995 assert_eq!(summaries.len(), 2);
996 assert_eq!(summaries[0].id, s1);
997 assert_eq!(summaries[0].content, "summary1");
998 assert_eq!(summaries[1].id, s2);
999 assert_eq!(summaries[1].content, "summary2");
1000 }
1001
1002 #[tokio::test]
1003 async fn summarize_below_threshold() {
1004 let memory = test_semantic_memory(false).await;
1005 let cid = memory.sqlite().create_conversation().await.unwrap();
1006
1007 memory.remember(cid, "user", "hello").await.unwrap();
1008
1009 let result = memory.summarize(cid, 10).await.unwrap();
1010 assert!(result.is_none());
1011 }
1012
1013 #[tokio::test]
1014 async fn summarize_stores_summary() {
1015 let memory = test_semantic_memory(false).await;
1016 let cid = memory.sqlite().create_conversation().await.unwrap();
1017
1018 for i in 0..5 {
1019 memory
1020 .remember(cid, "user", &format!("message {i}"))
1021 .await
1022 .unwrap();
1023 }
1024
1025 let summary_id = memory.summarize(cid, 3).await.unwrap();
1026 assert!(summary_id.is_some());
1027
1028 let summaries = memory.load_summaries(cid).await.unwrap();
1029 assert_eq!(summaries.len(), 1);
1030 assert_eq!(summaries[0].id, summary_id.unwrap());
1031 assert!(!summaries[0].content.is_empty());
1032 }
1033
1034 #[tokio::test]
1035 async fn summarize_respects_previous_summaries() {
1036 let memory = test_semantic_memory(false).await;
1037 let cid = memory.sqlite().create_conversation().await.unwrap();
1038
1039 for i in 0..10 {
1040 memory
1041 .remember(cid, "user", &format!("message {i}"))
1042 .await
1043 .unwrap();
1044 }
1045
1046 let s1 = memory.summarize(cid, 3).await.unwrap();
1047 assert!(s1.is_some());
1048
1049 let s2 = memory.summarize(cid, 3).await.unwrap();
1050 assert!(s2.is_some());
1051
1052 let summaries = memory.load_summaries(cid).await.unwrap();
1053 assert_eq!(summaries.len(), 2);
1054 assert!(summaries[0].last_message_id < summaries[1].first_message_id);
1055 }
1056
1057 #[tokio::test]
1058 async fn remember_multiple_messages_increments_ids() {
1059 let memory = test_semantic_memory(false).await;
1060 let cid = memory.sqlite.create_conversation().await.unwrap();
1061
1062 let id1 = memory.remember(cid, "user", "first").await.unwrap();
1063 let id2 = memory.remember(cid, "assistant", "second").await.unwrap();
1064 let id3 = memory.remember(cid, "user", "third").await.unwrap();
1065
1066 assert!(id1 < id2);
1067 assert!(id2 < id3);
1068 }
1069
1070 #[tokio::test]
1071 async fn message_count_across_conversations() {
1072 let memory = test_semantic_memory(false).await;
1073 let cid1 = memory.sqlite().create_conversation().await.unwrap();
1074 let cid2 = memory.sqlite().create_conversation().await.unwrap();
1075
1076 memory.remember(cid1, "user", "msg1").await.unwrap();
1077 memory.remember(cid1, "user", "msg2").await.unwrap();
1078 memory.remember(cid2, "user", "msg3").await.unwrap();
1079
1080 assert_eq!(memory.message_count(cid1).await.unwrap(), 2);
1081 assert_eq!(memory.message_count(cid2).await.unwrap(), 1);
1082 }
1083
1084 #[tokio::test]
1085 async fn summarize_exact_threshold_returns_none() {
1086 let memory = test_semantic_memory(false).await;
1087 let cid = memory.sqlite().create_conversation().await.unwrap();
1088
1089 for i in 0..3 {
1090 memory
1091 .remember(cid, "user", &format!("msg {i}"))
1092 .await
1093 .unwrap();
1094 }
1095
1096 let result = memory.summarize(cid, 3).await.unwrap();
1097 assert!(result.is_none());
1098 }
1099
1100 #[tokio::test]
1101 async fn summarize_one_above_threshold_produces_summary() {
1102 let memory = test_semantic_memory(false).await;
1103 let cid = memory.sqlite().create_conversation().await.unwrap();
1104
1105 for i in 0..4 {
1106 memory
1107 .remember(cid, "user", &format!("msg {i}"))
1108 .await
1109 .unwrap();
1110 }
1111
1112 let result = memory.summarize(cid, 3).await.unwrap();
1113 assert!(result.is_some());
1114 }
1115
1116 #[tokio::test]
1117 async fn summary_fields_populated() {
1118 let memory = test_semantic_memory(false).await;
1119 let cid = memory.sqlite().create_conversation().await.unwrap();
1120
1121 for i in 0..5 {
1122 memory
1123 .remember(cid, "user", &format!("msg {i}"))
1124 .await
1125 .unwrap();
1126 }
1127
1128 memory.summarize(cid, 3).await.unwrap();
1129 let summaries = memory.load_summaries(cid).await.unwrap();
1130 let s = &summaries[0];
1131
1132 assert_eq!(s.conversation_id, cid);
1133 assert!(s.first_message_id > MessageId(0));
1134 assert!(s.last_message_id >= s.first_message_id);
1135 assert!(s.token_estimate >= 0);
1136 assert!(!s.content.is_empty());
1137 }
1138
1139 #[test]
1140 fn build_summarization_prompt_format() {
1141 let messages = vec![
1142 (MessageId(1), "user".into(), "Hello".into()),
1143 (MessageId(2), "assistant".into(), "Hi there".into()),
1144 ];
1145 let prompt = build_summarization_prompt(&messages);
1146 assert!(prompt.contains("user: Hello"));
1147 assert!(prompt.contains("assistant: Hi there"));
1148 assert!(prompt.contains("key_facts"));
1149 }
1150
1151 #[test]
1152 fn build_summarization_prompt_empty() {
1153 let messages: Vec<(MessageId, String, String)> = vec![];
1154 let prompt = build_summarization_prompt(&messages);
1155 assert!(prompt.contains("key_facts"));
1156 }
1157
1158 #[test]
1159 fn structured_summary_deserialize() {
1160 let json = r#"{"summary":"s","key_facts":["f1","f2"],"entities":["e1"]}"#;
1161 let ss: StructuredSummary = serde_json::from_str(json).unwrap();
1162 assert_eq!(ss.summary, "s");
1163 assert_eq!(ss.key_facts.len(), 2);
1164 assert_eq!(ss.entities.len(), 1);
1165 }
1166
1167 #[test]
1168 fn structured_summary_empty_facts() {
1169 let json = r#"{"summary":"s","key_facts":[],"entities":[]}"#;
1170 let ss: StructuredSummary = serde_json::from_str(json).unwrap();
1171 assert!(ss.key_facts.is_empty());
1172 assert!(ss.entities.is_empty());
1173 }
1174
1175 #[tokio::test]
1176 async fn search_key_facts_no_qdrant_empty() {
1177 let memory = test_semantic_memory(false).await;
1178 let facts = memory.search_key_facts("query", 5).await.unwrap();
1179 assert!(facts.is_empty());
1180 }
1181
1182 #[test]
1183 fn recalled_message_debug() {
1184 let recalled = RecalledMessage {
1185 message: Message {
1186 role: Role::User,
1187 content: "test".into(),
1188 parts: vec![],
1189 },
1190 score: 0.95,
1191 };
1192 let dbg = format!("{recalled:?}");
1193 assert!(dbg.contains("RecalledMessage"));
1194 assert!(dbg.contains("0.95"));
1195 }
1196
1197 #[test]
1198 fn summary_clone() {
1199 let summary = Summary {
1200 id: 1,
1201 conversation_id: ConversationId(2),
1202 content: "test summary".into(),
1203 first_message_id: MessageId(1),
1204 last_message_id: MessageId(5),
1205 token_estimate: 10,
1206 };
1207 let cloned = summary.clone();
1208 assert_eq!(summary.id, cloned.id);
1209 assert_eq!(summary.content, cloned.content);
1210 }
1211
1212 #[test]
1213 fn estimate_tokens_short_text() {
1214 assert_eq!(estimate_tokens("ab"), 0);
1215 }
1216
1217 #[test]
1218 fn estimate_tokens_longer_text() {
1219 let text = "a".repeat(100);
1220 assert_eq!(estimate_tokens(&text), 33);
1221 }
1222
1223 #[tokio::test]
1224 async fn remember_preserves_role_mapping() {
1225 let memory = test_semantic_memory(false).await;
1226 let cid = memory.sqlite.create_conversation().await.unwrap();
1227
1228 memory.remember(cid, "user", "u").await.unwrap();
1229 memory.remember(cid, "assistant", "a").await.unwrap();
1230 memory.remember(cid, "system", "s").await.unwrap();
1231
1232 let history = memory.sqlite.load_history(cid, 50).await.unwrap();
1233 assert_eq!(history.len(), 3);
1234 assert_eq!(history[0].role, Role::User);
1235 assert_eq!(history[1].role, Role::Assistant);
1236 assert_eq!(history[2].role, Role::System);
1237 }
1238
1239 #[tokio::test]
1240 async fn new_with_invalid_qdrant_url_graceful() {
1241 let mut mock = MockProvider::default();
1242 mock.supports_embeddings = true;
1243 let provider = AnyProvider::Mock(mock);
1244 let result =
1245 SemanticMemory::new(":memory:", "http://127.0.0.1:1", provider, "test-model").await;
1246 assert!(result.is_ok());
1247 }
1248
1249 #[tokio::test]
1250 async fn remember_with_embeddings_supported_but_no_qdrant() {
1251 let memory = test_semantic_memory(true).await;
1252 let cid = memory.sqlite.create_conversation().await.unwrap();
1253
1254 let msg_id = memory.remember(cid, "user", "hello embed").await.unwrap();
1255 assert!(msg_id > MessageId(0));
1256
1257 let history = memory.sqlite.load_history(cid, 50).await.unwrap();
1258 assert_eq!(history.len(), 1);
1259 assert_eq!(history[0].content, "hello embed");
1260 }
1261
1262 #[tokio::test]
1263 async fn remember_verifies_content_via_load_history() {
1264 let memory = test_semantic_memory(false).await;
1265 let cid = memory.sqlite.create_conversation().await.unwrap();
1266
1267 memory.remember(cid, "user", "alpha").await.unwrap();
1268 memory.remember(cid, "assistant", "beta").await.unwrap();
1269 memory.remember(cid, "user", "gamma").await.unwrap();
1270
1271 let history = memory.sqlite().load_history(cid, 50).await.unwrap();
1272 assert_eq!(history.len(), 3);
1273 assert_eq!(history[0].content, "alpha");
1274 assert_eq!(history[1].content, "beta");
1275 assert_eq!(history[2].content, "gamma");
1276 }
1277
1278 #[tokio::test]
1279 async fn message_count_multiple_conversations_isolated() {
1280 let memory = test_semantic_memory(false).await;
1281 let cid1 = memory.sqlite().create_conversation().await.unwrap();
1282 let cid2 = memory.sqlite().create_conversation().await.unwrap();
1283 let cid3 = memory.sqlite().create_conversation().await.unwrap();
1284
1285 for _ in 0..5 {
1286 memory.remember(cid1, "user", "msg").await.unwrap();
1287 }
1288 for _ in 0..3 {
1289 memory.remember(cid2, "user", "msg").await.unwrap();
1290 }
1291
1292 assert_eq!(memory.message_count(cid1).await.unwrap(), 5);
1293 assert_eq!(memory.message_count(cid2).await.unwrap(), 3);
1294 assert_eq!(memory.message_count(cid3).await.unwrap(), 0);
1295 }
1296
1297 #[tokio::test]
1298 async fn summarize_empty_messages_range_returns_none() {
1299 let memory = test_semantic_memory(false).await;
1300 let cid = memory.sqlite().create_conversation().await.unwrap();
1301
1302 for i in 0..6 {
1303 memory
1304 .remember(cid, "user", &format!("msg {i}"))
1305 .await
1306 .unwrap();
1307 }
1308
1309 memory.summarize(cid, 3).await.unwrap();
1310 memory.summarize(cid, 3).await.unwrap();
1311
1312 let summaries = memory.load_summaries(cid).await.unwrap();
1313 assert_eq!(summaries.len(), 2);
1314 }
1315
1316 #[tokio::test]
1317 async fn summarize_token_estimate_populated() {
1318 let memory = test_semantic_memory(false).await;
1319 let cid = memory.sqlite().create_conversation().await.unwrap();
1320
1321 for i in 0..5 {
1322 memory
1323 .remember(cid, "user", &format!("message {i}"))
1324 .await
1325 .unwrap();
1326 }
1327
1328 memory.summarize(cid, 3).await.unwrap();
1329 let summaries = memory.load_summaries(cid).await.unwrap();
1330 let token_est = summaries[0].token_estimate;
1331 let expected = i64::try_from(estimate_tokens(&summaries[0].content)).unwrap();
1332 assert_eq!(token_est, expected);
1333 }
1334
1335 #[tokio::test]
1336 async fn summarize_fails_when_provider_chat_fails() {
1337 let sqlite = SqliteStore::new(":memory:").await.unwrap();
1338 let provider = AnyProvider::Ollama(zeph_llm::ollama::OllamaProvider::new(
1339 "http://127.0.0.1:1",
1340 "test".into(),
1341 "embed".into(),
1342 ));
1343 let memory = SemanticMemory {
1344 sqlite,
1345 qdrant: None,
1346 provider,
1347 embedding_model: "test".into(),
1348 vector_weight: 0.7,
1349 keyword_weight: 0.3,
1350 };
1351 let cid = memory.sqlite().create_conversation().await.unwrap();
1352
1353 for i in 0..5 {
1354 memory
1355 .remember(cid, "user", &format!("msg {i}"))
1356 .await
1357 .unwrap();
1358 }
1359
1360 let result = memory.summarize(cid, 3).await;
1361 assert!(result.is_err());
1362 }
1363
1364 #[tokio::test]
1365 async fn embed_missing_without_embedding_support_returns_zero() {
1366 let memory = test_semantic_memory(false).await;
1367 let cid = memory.sqlite().create_conversation().await.unwrap();
1368 memory
1369 .sqlite()
1370 .save_message(cid, "user", "test message")
1371 .await
1372 .unwrap();
1373
1374 let count = memory.embed_missing().await.unwrap();
1375 assert_eq!(count, 0);
1376 }
1377
1378 #[tokio::test]
1379 async fn has_embedding_returns_false_when_no_qdrant() {
1380 let memory = test_semantic_memory(false).await;
1381 let cid = memory.sqlite.create_conversation().await.unwrap();
1382 let msg_id = memory.remember(cid, "user", "test").await.unwrap();
1383 assert!(!memory.has_embedding(msg_id).await.unwrap());
1384 }
1385
1386 #[tokio::test]
1387 async fn recall_empty_without_qdrant_regardless_of_filter() {
1388 let memory = test_semantic_memory(true).await;
1389 let filter = SearchFilter {
1390 conversation_id: Some(ConversationId(1)),
1391 role: None,
1392 };
1393 let recalled = memory.recall("query", 10, Some(filter)).await.unwrap();
1394 assert!(recalled.is_empty());
1395 }
1396
1397 #[tokio::test]
1398 async fn summarize_message_range_bounds() {
1399 let memory = test_semantic_memory(false).await;
1400 let cid = memory.sqlite().create_conversation().await.unwrap();
1401
1402 for i in 0..8 {
1403 memory
1404 .remember(cid, "user", &format!("msg {i}"))
1405 .await
1406 .unwrap();
1407 }
1408
1409 let summary_id = memory.summarize(cid, 4).await.unwrap().unwrap();
1410 let summaries = memory.load_summaries(cid).await.unwrap();
1411 assert_eq!(summaries.len(), 1);
1412 assert_eq!(summaries[0].id, summary_id);
1413 assert!(summaries[0].first_message_id >= MessageId(1));
1414 assert!(summaries[0].last_message_id >= summaries[0].first_message_id);
1415 }
1416
1417 #[test]
1418 fn build_summarization_prompt_preserves_order() {
1419 let messages = vec![
1420 (MessageId(1), "user".into(), "first".into()),
1421 (MessageId(2), "assistant".into(), "second".into()),
1422 (MessageId(3), "user".into(), "third".into()),
1423 ];
1424 let prompt = build_summarization_prompt(&messages);
1425 let first_pos = prompt.find("user: first").unwrap();
1426 let second_pos = prompt.find("assistant: second").unwrap();
1427 let third_pos = prompt.find("user: third").unwrap();
1428 assert!(first_pos < second_pos);
1429 assert!(second_pos < third_pos);
1430 }
1431
1432 #[test]
1433 fn summary_debug() {
1434 let summary = Summary {
1435 id: 1,
1436 conversation_id: ConversationId(2),
1437 content: "test".into(),
1438 first_message_id: MessageId(1),
1439 last_message_id: MessageId(5),
1440 token_estimate: 10,
1441 };
1442 let dbg = format!("{summary:?}");
1443 assert!(dbg.contains("Summary"));
1444 }
1445
1446 #[tokio::test]
1447 async fn message_count_nonexistent_conversation() {
1448 let memory = test_semantic_memory(false).await;
1449 let count = memory.message_count(ConversationId(999)).await.unwrap();
1450 assert_eq!(count, 0);
1451 }
1452
1453 #[tokio::test]
1454 async fn load_summaries_nonexistent_conversation() {
1455 let memory = test_semantic_memory(false).await;
1456 let summaries = memory.load_summaries(ConversationId(999)).await.unwrap();
1457 assert!(summaries.is_empty());
1458 }
1459
1460 #[tokio::test]
1461 async fn store_session_summary_no_qdrant_noop() {
1462 let memory = test_semantic_memory(true).await;
1463 let result = memory
1464 .store_session_summary(ConversationId(1), "test summary")
1465 .await;
1466 assert!(result.is_ok());
1467 }
1468
1469 #[tokio::test]
1470 async fn store_session_summary_no_embeddings_noop() {
1471 let memory = test_semantic_memory(false).await;
1472 let result = memory
1473 .store_session_summary(ConversationId(1), "test summary")
1474 .await;
1475 assert!(result.is_ok());
1476 }
1477
1478 #[tokio::test]
1479 async fn search_session_summaries_no_qdrant_empty() {
1480 let memory = test_semantic_memory(true).await;
1481 let results = memory
1482 .search_session_summaries("query", 5, None)
1483 .await
1484 .unwrap();
1485 assert!(results.is_empty());
1486 }
1487
1488 #[tokio::test]
1489 async fn search_session_summaries_no_embeddings_empty() {
1490 let memory = test_semantic_memory(false).await;
1491 let results = memory
1492 .search_session_summaries("query", 5, Some(ConversationId(1)))
1493 .await
1494 .unwrap();
1495 assert!(results.is_empty());
1496 }
1497
1498 #[test]
1499 fn session_summary_result_debug() {
1500 let result = SessionSummaryResult {
1501 summary_text: "test".into(),
1502 score: 0.9,
1503 conversation_id: ConversationId(1),
1504 };
1505 let dbg = format!("{result:?}");
1506 assert!(dbg.contains("SessionSummaryResult"));
1507 }
1508
1509 #[test]
1510 fn session_summary_result_clone() {
1511 let result = SessionSummaryResult {
1512 summary_text: "test".into(),
1513 score: 0.9,
1514 conversation_id: ConversationId(1),
1515 };
1516 let cloned = result.clone();
1517 assert_eq!(result.summary_text, cloned.summary_text);
1518 assert_eq!(result.conversation_id, cloned.conversation_id);
1519 }
1520
1521 #[tokio::test]
1522 async fn recall_fts5_fallback_without_qdrant() {
1523 let memory = test_semantic_memory(false).await;
1524 let cid = memory.sqlite.create_conversation().await.unwrap();
1525
1526 memory
1527 .remember(cid, "user", "rust programming guide")
1528 .await
1529 .unwrap();
1530 memory
1531 .remember(cid, "assistant", "python tutorial")
1532 .await
1533 .unwrap();
1534 memory
1535 .remember(cid, "user", "advanced rust patterns")
1536 .await
1537 .unwrap();
1538
1539 let recalled = memory.recall("rust", 5, None).await.unwrap();
1540 assert_eq!(recalled.len(), 2);
1541 assert!(recalled[0].score >= recalled[1].score);
1542 }
1543
1544 #[tokio::test]
1545 async fn recall_fts5_fallback_with_filter() {
1546 let memory = test_semantic_memory(false).await;
1547 let cid1 = memory.sqlite.create_conversation().await.unwrap();
1548 let cid2 = memory.sqlite.create_conversation().await.unwrap();
1549
1550 memory.remember(cid1, "user", "hello world").await.unwrap();
1551 memory
1552 .remember(cid2, "user", "hello universe")
1553 .await
1554 .unwrap();
1555
1556 let filter = SearchFilter {
1557 conversation_id: Some(cid1),
1558 role: None,
1559 };
1560 let recalled = memory.recall("hello", 5, Some(filter)).await.unwrap();
1561 assert_eq!(recalled.len(), 1);
1562 }
1563
1564 #[tokio::test]
1565 async fn recall_fts5_no_matches_returns_empty() {
1566 let memory = test_semantic_memory(false).await;
1567 let cid = memory.sqlite.create_conversation().await.unwrap();
1568
1569 memory.remember(cid, "user", "hello world").await.unwrap();
1570
1571 let recalled = memory.recall("nonexistent", 5, None).await.unwrap();
1572 assert!(recalled.is_empty());
1573 }
1574
1575 #[tokio::test]
1576 async fn recall_fts5_respects_limit() {
1577 let memory = test_semantic_memory(false).await;
1578 let cid = memory.sqlite.create_conversation().await.unwrap();
1579
1580 for i in 0..10 {
1581 memory
1582 .remember(cid, "user", &format!("test message number {i}"))
1583 .await
1584 .unwrap();
1585 }
1586
1587 let recalled = memory.recall("test", 3, None).await.unwrap();
1588 assert_eq!(recalled.len(), 3);
1589 }
1590
1591 #[tokio::test]
1594 async fn summarize_fallback_to_plain_text_when_structured_fails() {
1595 let sqlite = SqliteStore::new(":memory:").await.unwrap();
1603 let mut mock = MockProvider::default();
1604 mock.default_response = "plain text summary".into();
1606 let provider = AnyProvider::Mock(mock);
1607
1608 let memory = SemanticMemory {
1609 sqlite,
1610 qdrant: None,
1611 provider,
1612 embedding_model: "test".into(),
1613 vector_weight: 0.7,
1614 keyword_weight: 0.3,
1615 };
1616
1617 let cid = memory.sqlite().create_conversation().await.unwrap();
1618 for i in 0..5 {
1619 memory
1620 .remember(cid, "user", &format!("msg {i}"))
1621 .await
1622 .unwrap();
1623 }
1624
1625 let result = memory.summarize(cid, 3).await;
1626 assert!(result.is_ok());
1632 let summaries = memory.load_summaries(cid).await.unwrap();
1633 assert_eq!(summaries.len(), 1);
1634 assert!(!summaries[0].content.is_empty());
1635 }
1636
1637 use proptest::prelude::*;
1640
1641 proptest! {
1642 #[test]
1643 fn estimate_tokens_never_panics(s in ".*") {
1644 let _ = estimate_tokens(&s);
1645 }
1646 }
1647}