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}