Skip to main content

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}