Skip to main content

pulsedb/search/
context.rs

1//! Context candidates retrieval types.
2//!
3//! [`ContextRequest`] and [`ContextCandidates`] enable a single call to
4//! [`PulseDB::get_context_candidates()`](crate::PulseDB::get_context_candidates)
5//! that orchestrates all retrieval primitives (similarity search, recent
6//! experiences, insights, relations, active agents) into one response.
7
8use crate::activity::Activity;
9use crate::experience::Experience;
10use crate::insight::DerivedInsight;
11use crate::relation::ExperienceRelation;
12use crate::search::{SearchFilter, SearchResult};
13use crate::types::CollectiveId;
14
15/// Request for unified context retrieval.
16///
17/// Configures which primitives to query and how many results to return.
18/// Pass this to [`PulseDB::get_context_candidates()`](crate::PulseDB::get_context_candidates).
19///
20/// # Required Fields
21///
22/// - `collective_id` - The collective to search within (must exist)
23/// - `query_embedding` - Embedding vector for similarity and insight search
24///   (must match the collective's embedding dimension)
25///
26/// # Example
27///
28/// ```rust
29/// # fn main() -> pulsedb::Result<()> {
30/// # let dir = tempfile::tempdir().unwrap();
31/// # let db = pulsedb::PulseDB::open(dir.path().join("test.db"), pulsedb::Config::default())?;
32/// # let collective_id = db.create_collective("example")?;
33/// # let query_vec = vec![0.1f32; 384];
34/// use pulsedb::{ContextRequest, SearchFilter};
35///
36/// let candidates = db.get_context_candidates(ContextRequest {
37///     collective_id,
38///     query_embedding: query_vec,
39///     max_similar: 10,
40///     max_recent: 5,
41///     include_insights: true,
42///     filter: SearchFilter {
43///         domains: Some(vec!["rust".to_string()]),
44///         ..SearchFilter::default()
45///     },
46///     ..ContextRequest::default()
47/// })?;
48/// # Ok(())
49/// # }
50/// ```
51#[derive(Clone, Debug)]
52pub struct ContextRequest {
53    /// The collective to search within (must exist).
54    pub collective_id: CollectiveId,
55
56    /// Query embedding vector for similarity search and insight retrieval.
57    ///
58    /// Must match the collective's configured embedding dimension.
59    pub query_embedding: Vec<f32>,
60
61    /// Maximum number of similar experiences to return (1-1000, default: 20).
62    pub max_similar: usize,
63
64    /// Maximum number of recent experiences to return (1-1000, default: 10).
65    pub max_recent: usize,
66
67    /// Whether to include derived insights in the response (default: true).
68    pub include_insights: bool,
69
70    /// Whether to include relations for returned experiences (default: true).
71    pub include_relations: bool,
72
73    /// Whether to include active agent activities (default: true).
74    pub include_active_agents: bool,
75
76    /// Filter criteria applied to similar and recent experience queries.
77    pub filter: SearchFilter,
78}
79
80impl Default for ContextRequest {
81    fn default() -> Self {
82        Self {
83            collective_id: CollectiveId::nil(),
84            query_embedding: vec![],
85            max_similar: 20,
86            max_recent: 10,
87            include_insights: true,
88            include_relations: true,
89            include_active_agents: true,
90            filter: SearchFilter::default(),
91        }
92    }
93}
94
95/// Aggregated context candidates from all retrieval primitives.
96///
97/// Returned by [`PulseDB::get_context_candidates()`](crate::PulseDB::get_context_candidates).
98/// Each field may be empty if no results were found or the corresponding
99/// feature was disabled in the [`ContextRequest`].
100///
101/// # Field Semantics
102///
103/// - `similar_experiences` - Sorted by similarity descending (most similar first)
104/// - `recent_experiences` - Sorted by timestamp descending (newest first)
105/// - `insights` - Similar insights found via HNSW vector search
106/// - `relations` - Relations involving any returned experience (deduplicated)
107/// - `active_agents` - Non-stale agents in the collective
108#[derive(Clone, Debug)]
109pub struct ContextCandidates {
110    /// Semantically similar experiences, sorted by similarity descending.
111    pub similar_experiences: Vec<SearchResult>,
112
113    /// Most recent experiences, sorted by timestamp descending.
114    pub recent_experiences: Vec<Experience>,
115
116    /// Derived insights similar to the query embedding.
117    ///
118    /// Empty if `include_insights` was `false` in the request.
119    pub insights: Vec<DerivedInsight>,
120
121    /// Relations involving the returned similar and recent experiences.
122    ///
123    /// Deduplicated by `RelationId`. Empty if `include_relations` was `false`.
124    pub relations: Vec<ExperienceRelation>,
125
126    /// Currently active (non-stale) agents in the collective.
127    ///
128    /// Empty if `include_active_agents` was `false` in the request.
129    pub active_agents: Vec<Activity>,
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_context_request_default_values() {
138        let req = ContextRequest::default();
139        assert_eq!(req.collective_id, CollectiveId::nil());
140        assert!(req.query_embedding.is_empty());
141        assert_eq!(req.max_similar, 20);
142        assert_eq!(req.max_recent, 10);
143        assert!(req.include_insights);
144        assert!(req.include_relations);
145        assert!(req.include_active_agents);
146        assert!(req.filter.exclude_archived);
147    }
148
149    #[test]
150    fn test_context_request_clone_and_debug() {
151        let req = ContextRequest {
152            collective_id: CollectiveId::new(),
153            query_embedding: vec![0.1; 384],
154            max_similar: 5,
155            ..ContextRequest::default()
156        };
157        let cloned = req.clone();
158        assert_eq!(cloned.max_similar, 5);
159        assert_eq!(cloned.query_embedding.len(), 384);
160
161        let debug = format!("{:?}", req);
162        assert!(debug.contains("ContextRequest"));
163    }
164
165    #[test]
166    fn test_context_candidates_clone_and_debug() {
167        let candidates = ContextCandidates {
168            similar_experiences: vec![],
169            recent_experiences: vec![],
170            insights: vec![],
171            relations: vec![],
172            active_agents: vec![],
173        };
174        let cloned = candidates.clone();
175        assert!(cloned.similar_experiences.is_empty());
176
177        let debug = format!("{:?}", candidates);
178        assert!(debug.contains("ContextCandidates"));
179    }
180}