pulsedb/insight/types.rs
1//! Data types for derived insights.
2//!
3//! Insights are synthesized knowledge computed from multiple experiences
4//! within the same collective. They represent higher-level understanding
5//! that agents can use for decision-making.
6
7use serde::{Deserialize, Serialize};
8
9use crate::types::{CollectiveId, ExperienceId, InsightId, Timestamp};
10
11/// Type of derived insight.
12///
13/// Categorizes the kind of synthesis that produced this insight,
14/// enabling agents to filter and prioritize different knowledge types.
15///
16/// # Example
17///
18/// ```rust
19/// use pulsedb::InsightType;
20///
21/// let kind = InsightType::Pattern;
22/// // "A recurring pattern detected across experiences"
23/// ```
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum InsightType {
26 /// A recurring pattern detected across multiple experiences.
27 Pattern,
28 /// A synthesis combining knowledge from multiple experiences.
29 Synthesis,
30 /// An abstraction generalizing multiple specific experiences.
31 Abstraction,
32 /// A correlation detected between experiences.
33 Correlation,
34}
35
36/// A stored derived insight — synthesized knowledge from multiple experiences.
37///
38/// Unlike experiences (which are raw agent observations), insights are
39/// computed/derived knowledge that represents higher-level understanding.
40///
41/// # Embedding Storage
42///
43/// Insight embeddings are stored **inline** (not in a separate table) because:
44/// 1. Insights are expected to be far fewer than experiences
45/// 2. The insight record is always loaded with its embedding for HNSW rebuild
46/// 3. Simpler storage model (no table join needed)
47#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct DerivedInsight {
49 /// Unique identifier (UUID v7, time-ordered).
50 pub id: InsightId,
51
52 /// The collective this insight belongs to.
53 pub collective_id: CollectiveId,
54
55 /// The insight content (text).
56 pub content: String,
57
58 /// Semantic embedding vector for similarity search.
59 pub embedding: Vec<f32>,
60
61 /// IDs of the source experiences this insight was derived from.
62 pub source_experience_ids: Vec<ExperienceId>,
63
64 /// The type of derivation.
65 pub insight_type: InsightType,
66
67 /// Confidence in this insight (0.0 = uncertain, 1.0 = certain).
68 pub confidence: f32,
69
70 /// Domain tags for categorical filtering.
71 pub domain: Vec<String>,
72
73 /// When this insight was created.
74 pub created_at: Timestamp,
75
76 /// When this insight was last updated.
77 pub updated_at: Timestamp,
78}
79
80/// Input for creating a new derived insight.
81///
82/// The `embedding` field is required when using the External embedding
83/// provider, and optional when using the Builtin provider (which
84/// generates embeddings from content automatically).
85///
86/// # Example
87///
88/// ```rust
89/// # fn main() -> pulsedb::Result<()> {
90/// # let dir = tempfile::tempdir().unwrap();
91/// # let db = pulsedb::PulseDB::open(dir.path().join("test.db"), pulsedb::Config::default())?;
92/// # let collective_id = db.create_collective("example")?;
93/// # let emb = vec![0.1f32; 384];
94/// # let exp_a = db.record_experience(pulsedb::NewExperience {
95/// # collective_id, content: "a".into(), embedding: Some(emb.clone()), ..Default::default()
96/// # })?;
97/// # let exp_b = db.record_experience(pulsedb::NewExperience {
98/// # collective_id, content: "b".into(), embedding: Some(emb.clone()), ..Default::default()
99/// # })?;
100/// # let exp_c = db.record_experience(pulsedb::NewExperience {
101/// # collective_id, content: "c".into(), embedding: Some(emb.clone()), ..Default::default()
102/// # })?;
103/// # let embedding_vec = vec![0.2f32; 384];
104/// use pulsedb::{NewDerivedInsight, InsightType};
105///
106/// let insight = NewDerivedInsight {
107/// collective_id,
108/// content: "Error handling patterns converge on early return".to_string(),
109/// embedding: Some(embedding_vec),
110/// source_experience_ids: vec![exp_a, exp_b, exp_c],
111/// insight_type: InsightType::Pattern,
112/// confidence: 0.85,
113/// domain: vec!["rust".to_string(), "error-handling".to_string()],
114/// };
115/// let id = db.store_insight(insight)?;
116/// # Ok(())
117/// # }
118/// ```
119pub struct NewDerivedInsight {
120 /// The collective to store this insight in.
121 pub collective_id: CollectiveId,
122
123 /// The insight content (text, max 50KB).
124 pub content: String,
125
126 /// Pre-computed embedding vector (required for External provider).
127 pub embedding: Option<Vec<f32>>,
128
129 /// IDs of the source experiences this insight was derived from (1-100).
130 pub source_experience_ids: Vec<ExperienceId>,
131
132 /// The type of derivation.
133 pub insight_type: InsightType,
134
135 /// Confidence in this insight (0.0-1.0).
136 pub confidence: f32,
137
138 /// Domain tags for categorical filtering.
139 pub domain: Vec<String>,
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_insight_type_bincode_roundtrip() {
148 let types = [
149 InsightType::Pattern,
150 InsightType::Synthesis,
151 InsightType::Abstraction,
152 InsightType::Correlation,
153 ];
154 for it in &types {
155 let bytes = bincode::serialize(it).unwrap();
156 let restored: InsightType = bincode::deserialize(&bytes).unwrap();
157 assert_eq!(*it, restored);
158 }
159 }
160
161 #[test]
162 fn test_derived_insight_bincode_roundtrip() {
163 let insight = DerivedInsight {
164 id: InsightId::new(),
165 collective_id: CollectiveId::new(),
166 content: "Test insight content".to_string(),
167 embedding: vec![0.1, 0.2, 0.3],
168 source_experience_ids: vec![ExperienceId::new(), ExperienceId::new()],
169 insight_type: InsightType::Pattern,
170 confidence: 0.85,
171 domain: vec!["rust".to_string()],
172 created_at: Timestamp::now(),
173 updated_at: Timestamp::now(),
174 };
175
176 let bytes = bincode::serialize(&insight).unwrap();
177 let restored: DerivedInsight = bincode::deserialize(&bytes).unwrap();
178
179 assert_eq!(insight.id, restored.id);
180 assert_eq!(insight.collective_id, restored.collective_id);
181 assert_eq!(insight.content, restored.content);
182 assert_eq!(insight.embedding, restored.embedding);
183 assert_eq!(
184 insight.source_experience_ids,
185 restored.source_experience_ids
186 );
187 assert_eq!(insight.insight_type, restored.insight_type);
188 assert_eq!(insight.confidence, restored.confidence);
189 assert_eq!(insight.domain, restored.domain);
190 }
191
192 #[test]
193 fn test_insight_type_copy_and_eq() {
194 let a = InsightType::Synthesis;
195 let b = a; // Copy
196 assert_eq!(a, b);
197 }
198
199 #[test]
200 fn test_insight_type_all_variants_distinct() {
201 assert_ne!(InsightType::Pattern, InsightType::Synthesis);
202 assert_ne!(InsightType::Synthesis, InsightType::Abstraction);
203 assert_ne!(InsightType::Abstraction, InsightType::Correlation);
204 assert_ne!(InsightType::Pattern, InsightType::Correlation);
205 }
206}