Skip to main content

zeph_memory/semantic/
cross_session.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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    /// Store a session summary into the dedicated `zeph_session_summaries` Qdrant collection.
21    ///
22    /// # Errors
23    ///
24    /// Returns an error if embedding or Qdrant storage fails.
25    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    /// Search session summaries from other conversations.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if embedding or Qdrant search fails.
64    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}