Skip to main content

khive_pack_brain/
event.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use khive_storage::event::Event;
5use khive_types::EventOutcome;
6
7/// Feedback signal values for the `brain.emit` verb.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum FeedbackSignal {
11    Useful,
12    NotUseful,
13    Wrong,
14}
15
16/// Interpreted brain signal extracted from a raw Event.
17#[derive(Debug)]
18pub enum BrainSignal {
19    /// A recall verb succeeded — positive signal for the recalled entity.
20    RecallHit { target_id: Uuid, latency_us: i64 },
21    /// A recall verb returned no results — miss signal for tuning.
22    RecallMiss,
23    /// A search verb completed.
24    SearchCompleted { latency_us: i64 },
25    /// Explicit feedback on a specific entity.
26    Feedback {
27        target_id: Uuid,
28        signal: FeedbackSignal,
29    },
30    /// Any other note-substrate access (get, list on notes).
31    NoteAccessed { target_id: Uuid },
32    /// Event is not relevant to the brain.
33    Irrelevant,
34}
35
36/// Extract a brain signal from a raw storage Event.
37///
38/// The brain interprets existing events by their verb + outcome + data fields.
39/// No parallel event enum needed — the Event substrate IS the source of truth.
40pub fn interpret(event: &Event) -> BrainSignal {
41    match event.verb.as_str() {
42        "recall" => match event.outcome {
43            EventOutcome::Success => match event.target_id {
44                Some(tid) => BrainSignal::RecallHit {
45                    target_id: tid,
46                    latency_us: event.duration_us,
47                },
48                None => BrainSignal::RecallMiss,
49            },
50            _ => BrainSignal::RecallMiss,
51        },
52        "search" => BrainSignal::SearchCompleted {
53            latency_us: event.duration_us,
54        },
55        "brain.emit" => {
56            let target = match event.target_id {
57                Some(t) => t,
58                None => return BrainSignal::Irrelevant,
59            };
60            let signal = event
61                .data
62                .as_ref()
63                .and_then(|d| d.get("signal"))
64                .and_then(|s| serde_json::from_value::<FeedbackSignal>(s.clone()).ok());
65            match signal {
66                Some(s) => BrainSignal::Feedback {
67                    target_id: target,
68                    signal: s,
69                },
70                None => BrainSignal::Irrelevant,
71            }
72        }
73        "get" | "remember" => match event.target_id {
74            Some(tid) => BrainSignal::NoteAccessed { target_id: tid },
75            None => BrainSignal::Irrelevant,
76        },
77        _ => BrainSignal::Irrelevant,
78    }
79}
80
81/// Extract (entity_id, positive_signal) for per-entity posterior updates.
82pub fn entity_signal(signal: &BrainSignal) -> Option<(Uuid, bool)> {
83    match signal {
84        BrainSignal::RecallHit { target_id, .. } => Some((*target_id, true)),
85        BrainSignal::NoteAccessed { target_id } => Some((*target_id, true)),
86        BrainSignal::Feedback {
87            target_id, signal, ..
88        } => Some((*target_id, matches!(signal, FeedbackSignal::Useful))),
89        BrainSignal::RecallMiss | BrainSignal::SearchCompleted { .. } | BrainSignal::Irrelevant => {
90            None
91        }
92    }
93}
94
95/// Is this signal positive for the global recall parameter?
96pub fn is_recall_positive(signal: &BrainSignal) -> Option<bool> {
97    match signal {
98        BrainSignal::RecallHit { .. } => Some(true),
99        BrainSignal::RecallMiss => Some(false),
100        _ => None,
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use khive_types::SubstrateKind;
108
109    fn make_event(verb: &str, outcome: EventOutcome, target: Option<Uuid>) -> Event {
110        let mut e = Event::new("test", verb, SubstrateKind::Note, "brain");
111        e.outcome = outcome;
112        e.target_id = target;
113        e
114    }
115
116    #[test]
117    fn recall_success_with_target_is_hit() {
118        let id = Uuid::new_v4();
119        let e = make_event("recall", EventOutcome::Success, Some(id));
120        match interpret(&e) {
121            BrainSignal::RecallHit { target_id, .. } => assert_eq!(target_id, id),
122            other => panic!("expected RecallHit, got {other:?}"),
123        }
124    }
125
126    #[test]
127    fn recall_success_without_target_is_miss() {
128        let e = make_event("recall", EventOutcome::Success, None);
129        assert!(matches!(interpret(&e), BrainSignal::RecallMiss));
130    }
131
132    #[test]
133    fn recall_error_is_miss() {
134        let e = make_event("recall", EventOutcome::Error, Some(Uuid::new_v4()));
135        assert!(matches!(interpret(&e), BrainSignal::RecallMiss));
136    }
137
138    #[test]
139    fn search_is_completed() {
140        let e = make_event("search", EventOutcome::Success, None);
141        assert!(matches!(interpret(&e), BrainSignal::SearchCompleted { .. }));
142    }
143
144    #[test]
145    fn brain_emit_with_feedback() {
146        let id = Uuid::new_v4();
147        let mut e = make_event("brain.emit", EventOutcome::Success, Some(id));
148        e.data = Some(serde_json::json!({"signal": "useful"}));
149        match interpret(&e) {
150            BrainSignal::Feedback { target_id, signal } => {
151                assert_eq!(target_id, id);
152                assert_eq!(signal, FeedbackSignal::Useful);
153            }
154            other => panic!("expected Feedback, got {other:?}"),
155        }
156    }
157
158    #[test]
159    fn brain_emit_without_target_is_irrelevant() {
160        let e = make_event("brain.emit", EventOutcome::Success, None);
161        assert!(matches!(interpret(&e), BrainSignal::Irrelevant));
162    }
163
164    #[test]
165    fn unknown_verb_is_irrelevant() {
166        let e = make_event("link", EventOutcome::Success, Some(Uuid::new_v4()));
167        assert!(matches!(interpret(&e), BrainSignal::Irrelevant));
168    }
169
170    #[test]
171    fn entity_signal_for_hit() {
172        let id = Uuid::new_v4();
173        let sig = BrainSignal::RecallHit {
174            target_id: id,
175            latency_us: 100,
176        };
177        assert_eq!(entity_signal(&sig), Some((id, true)));
178    }
179
180    #[test]
181    fn entity_signal_for_miss() {
182        assert_eq!(entity_signal(&BrainSignal::RecallMiss), None);
183    }
184
185    #[test]
186    fn recall_positive_classification() {
187        let hit = BrainSignal::RecallHit {
188            target_id: Uuid::new_v4(),
189            latency_us: 0,
190        };
191        assert_eq!(is_recall_positive(&hit), Some(true));
192        assert_eq!(is_recall_positive(&BrainSignal::RecallMiss), Some(false));
193        assert_eq!(
194            is_recall_positive(&BrainSignal::SearchCompleted { latency_us: 0 }),
195            None
196        );
197    }
198
199    #[test]
200    fn feedback_not_useful_is_negative_entity_signal() {
201        let id = Uuid::new_v4();
202        let sig = BrainSignal::Feedback {
203            target_id: id,
204            signal: FeedbackSignal::NotUseful,
205        };
206        assert_eq!(entity_signal(&sig), Some((id, false)));
207    }
208
209    #[test]
210    fn feedback_wrong_is_negative_entity_signal() {
211        let id = Uuid::new_v4();
212        let sig = BrainSignal::Feedback {
213            target_id: id,
214            signal: FeedbackSignal::Wrong,
215        };
216        assert_eq!(entity_signal(&sig), Some((id, false)));
217    }
218
219    #[test]
220    fn brain_emit_invalid_signal_data_is_irrelevant() {
221        let id = Uuid::new_v4();
222        let mut e = make_event("brain.emit", EventOutcome::Success, Some(id));
223        e.data = Some(serde_json::json!({"signal": "bad_value"}));
224        assert!(matches!(interpret(&e), BrainSignal::Irrelevant));
225    }
226
227    #[test]
228    fn note_accessed_via_get_verb_is_positive_entity_signal() {
229        let id = Uuid::new_v4();
230        let e = make_event("get", EventOutcome::Success, Some(id));
231        match interpret(&e) {
232            BrainSignal::NoteAccessed { target_id } => {
233                assert_eq!(target_id, id);
234                assert_eq!(
235                    entity_signal(&BrainSignal::NoteAccessed { target_id }),
236                    Some((id, true))
237                );
238            }
239            other => panic!("expected NoteAccessed, got {other:?}"),
240        }
241    }
242
243    #[test]
244    fn note_accessed_via_remember_verb_is_positive_entity_signal() {
245        let id = Uuid::new_v4();
246        let e = make_event("remember", EventOutcome::Success, Some(id));
247        match interpret(&e) {
248            BrainSignal::NoteAccessed { target_id } => {
249                assert_eq!(target_id, id);
250            }
251            other => panic!("expected NoteAccessed, got {other:?}"),
252        }
253    }
254}