zeph_memory/semantic/
cross_session.rs1use zeph_llm::provider::LlmProvider as _;
5
6use crate::error::MemoryError;
7use crate::types::ConversationId;
8use crate::vector_store::{FieldCondition, FieldValue, VectorFilter};
9
10use super::{SESSION_SUMMARIES_COLLECTION, SemanticMemory};
11
12#[derive(Debug, Clone)]
13pub struct SessionSummaryResult {
14 pub summary_text: String,
15 pub score: f32,
16 pub conversation_id: ConversationId,
17}
18
19impl SemanticMemory {
20 pub async fn store_session_summary(
26 &self,
27 conversation_id: ConversationId,
28 summary_text: &str,
29 ) -> Result<(), MemoryError> {
30 let Some(qdrant) = &self.qdrant else {
31 return Ok(());
32 };
33 if !self.provider.supports_embeddings() {
34 return Ok(());
35 }
36
37 let vector = self.provider.embed(summary_text).await?;
38 let vector_size = u64::try_from(vector.len()).unwrap_or(896);
39 qdrant
40 .ensure_named_collection(SESSION_SUMMARIES_COLLECTION, vector_size)
41 .await?;
42
43 let payload = serde_json::json!({
44 "conversation_id": conversation_id.0,
45 "summary_text": summary_text,
46 });
47
48 qdrant
49 .store_to_collection(SESSION_SUMMARIES_COLLECTION, payload, vector)
50 .await?;
51
52 tracing::debug!(
53 conversation_id = conversation_id.0,
54 "stored session summary"
55 );
56 Ok(())
57 }
58
59 pub async fn search_session_summaries(
65 &self,
66 query: &str,
67 limit: usize,
68 exclude_conversation_id: Option<ConversationId>,
69 ) -> Result<Vec<SessionSummaryResult>, MemoryError> {
70 let Some(qdrant) = &self.qdrant else {
71 tracing::debug!("session-summaries: skipped, no vector store");
72 return Ok(Vec::new());
73 };
74 if !self.provider.supports_embeddings() {
75 tracing::debug!("session-summaries: skipped, no embedding support");
76 return Ok(Vec::new());
77 }
78
79 let vector = self.provider.embed(query).await?;
80 let vector_size = u64::try_from(vector.len()).unwrap_or(896);
81 qdrant
82 .ensure_named_collection(SESSION_SUMMARIES_COLLECTION, vector_size)
83 .await?;
84
85 let filter = exclude_conversation_id.map(|cid| VectorFilter {
86 must: vec![],
87 must_not: vec![FieldCondition {
88 field: "conversation_id".into(),
89 value: FieldValue::Integer(cid.0),
90 }],
91 });
92
93 let points = qdrant
94 .search_collection(SESSION_SUMMARIES_COLLECTION, &vector, limit, filter)
95 .await?;
96
97 tracing::debug!(
98 results = points.len(),
99 limit,
100 exclude_conversation_id = exclude_conversation_id.map(|c| c.0),
101 "session-summaries: search complete"
102 );
103
104 let results = points
105 .into_iter()
106 .filter_map(|point| {
107 let summary_text = point.payload.get("summary_text")?.as_str()?.to_owned();
108 let conversation_id =
109 ConversationId(point.payload.get("conversation_id")?.as_i64()?);
110 Some(SessionSummaryResult {
111 summary_text,
112 score: point.score,
113 conversation_id,
114 })
115 })
116 .collect();
117
118 Ok(results)
119 }
120}