llm_memory_graph/observatory/
events.rs

1//! Event types for Observatory integration
2//!
3//! This module defines all events that can be emitted by the memory graph
4//! for real-time monitoring and analysis.
5
6use crate::types::{
7    AgentId, EdgeId, EdgeType, NodeId, NodeType, SessionId, TemplateId, TokenUsage,
8};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Events emitted by the memory graph
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum MemoryGraphEvent {
17    /// Node created event
18    NodeCreated {
19        /// ID of the created node
20        node_id: NodeId,
21        /// Type of node created
22        node_type: NodeType,
23        /// Session ID (if applicable)
24        #[serde(skip_serializing_if = "Option::is_none")]
25        session_id: Option<SessionId>,
26        /// Event timestamp
27        timestamp: DateTime<Utc>,
28        /// Additional metadata
29        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
30        metadata: HashMap<String, String>,
31    },
32
33    /// Edge created event
34    EdgeCreated {
35        /// ID of the created edge
36        edge_id: EdgeId,
37        /// Type of edge created
38        edge_type: EdgeType,
39        /// Source node ID
40        from: NodeId,
41        /// Target node ID
42        to: NodeId,
43        /// Event timestamp
44        timestamp: DateTime<Utc>,
45    },
46
47    /// Prompt submitted event
48    PromptSubmitted {
49        /// ID of the prompt
50        prompt_id: NodeId,
51        /// Session ID
52        session_id: SessionId,
53        /// Length of prompt content
54        content_length: usize,
55        /// Model used for the prompt
56        model: String,
57        /// Event timestamp
58        timestamp: DateTime<Utc>,
59    },
60
61    /// Response generated event
62    ResponseGenerated {
63        /// ID of the response
64        response_id: NodeId,
65        /// ID of the prompt this responds to
66        prompt_id: NodeId,
67        /// Length of response content
68        content_length: usize,
69        /// Token usage statistics
70        tokens_used: TokenUsage,
71        /// Response generation latency in milliseconds
72        latency_ms: u64,
73        /// Event timestamp
74        timestamp: DateTime<Utc>,
75    },
76
77    /// Tool invoked event
78    ToolInvoked {
79        /// ID of the tool invocation
80        tool_id: NodeId,
81        /// Name of the tool
82        tool_name: String,
83        /// Whether the tool invocation succeeded
84        success: bool,
85        /// Tool execution duration in milliseconds
86        duration_ms: u64,
87        /// Event timestamp
88        timestamp: DateTime<Utc>,
89    },
90
91    /// Agent handoff event
92    AgentHandoff {
93        /// Agent transferring from
94        from_agent: AgentId,
95        /// Agent transferring to
96        to_agent: AgentId,
97        /// Session ID
98        session_id: SessionId,
99        /// Reason for handoff
100        reason: String,
101        /// Event timestamp
102        timestamp: DateTime<Utc>,
103    },
104
105    /// Template instantiated event
106    TemplateInstantiated {
107        /// Template ID
108        template_id: TemplateId,
109        /// Prompt ID created from template
110        prompt_id: NodeId,
111        /// Template version used
112        version: String,
113        /// Variable bindings used
114        variables: HashMap<String, String>,
115        /// Event timestamp
116        timestamp: DateTime<Utc>,
117    },
118
119    /// Query executed event
120    QueryExecuted {
121        /// Type of query executed
122        query_type: String,
123        /// Number of results returned
124        results_count: usize,
125        /// Query execution duration in milliseconds
126        duration_ms: u64,
127        /// Event timestamp
128        timestamp: DateTime<Utc>,
129    },
130}
131
132impl MemoryGraphEvent {
133    /// Get a unique key for this event (for Kafka partitioning)
134    pub fn key(&self) -> String {
135        match self {
136            Self::NodeCreated { node_id, .. } => format!("node:{}", node_id),
137            Self::EdgeCreated { edge_id, .. } => format!("edge:{}", edge_id),
138            Self::PromptSubmitted { session_id, .. } | Self::AgentHandoff { session_id, .. } => {
139                format!("session:{}", session_id)
140            }
141            Self::ResponseGenerated { prompt_id, .. } => {
142                format!("prompt:{}", prompt_id)
143            }
144            Self::ToolInvoked { tool_id, .. } => format!("tool:{}", tool_id),
145            Self::TemplateInstantiated { template_id, .. } => format!("template:{}", template_id),
146            Self::QueryExecuted { query_type, .. } => format!("query:{}", query_type),
147        }
148    }
149
150    /// Get the event type name
151    pub fn event_type(&self) -> &'static str {
152        match self {
153            Self::NodeCreated { .. } => "node_created",
154            Self::EdgeCreated { .. } => "edge_created",
155            Self::PromptSubmitted { .. } => "prompt_submitted",
156            Self::ResponseGenerated { .. } => "response_generated",
157            Self::ToolInvoked { .. } => "tool_invoked",
158            Self::AgentHandoff { .. } => "agent_handoff",
159            Self::TemplateInstantiated { .. } => "template_instantiated",
160            Self::QueryExecuted { .. } => "query_executed",
161        }
162    }
163
164    /// Get the timestamp of this event
165    pub fn timestamp(&self) -> DateTime<Utc> {
166        match self {
167            Self::NodeCreated { timestamp, .. }
168            | Self::EdgeCreated { timestamp, .. }
169            | Self::PromptSubmitted { timestamp, .. }
170            | Self::ResponseGenerated { timestamp, .. }
171            | Self::ToolInvoked { timestamp, .. }
172            | Self::AgentHandoff { timestamp, .. }
173            | Self::TemplateInstantiated { timestamp, .. }
174            | Self::QueryExecuted { timestamp, .. } => *timestamp,
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::types::NodeId;
183
184    #[test]
185    fn test_event_serialization() {
186        let event = MemoryGraphEvent::NodeCreated {
187            node_id: NodeId::new(),
188            node_type: NodeType::Prompt,
189            session_id: Some(SessionId::new()),
190            timestamp: Utc::now(),
191            metadata: HashMap::new(),
192        };
193
194        let json = serde_json::to_string(&event).unwrap();
195        let deserialized: MemoryGraphEvent = serde_json::from_str(&json).unwrap();
196
197        assert_eq!(event.event_type(), deserialized.event_type());
198    }
199
200    #[test]
201    fn test_event_key_generation() {
202        let node_id = NodeId::new();
203        let event = MemoryGraphEvent::NodeCreated {
204            node_id,
205            node_type: NodeType::Prompt,
206            session_id: None,
207            timestamp: Utc::now(),
208            metadata: HashMap::new(),
209        };
210
211        let key = event.key();
212        assert!(key.starts_with("node:"));
213        assert!(key.contains(&node_id.to_string()));
214    }
215
216    #[test]
217    fn test_all_event_types() {
218        let events = vec![
219            MemoryGraphEvent::NodeCreated {
220                node_id: NodeId::new(),
221                node_type: NodeType::Prompt,
222                session_id: None,
223                timestamp: Utc::now(),
224                metadata: HashMap::new(),
225            },
226            MemoryGraphEvent::EdgeCreated {
227                edge_id: EdgeId::new(),
228                edge_type: EdgeType::Follows,
229                from: NodeId::new(),
230                to: NodeId::new(),
231                timestamp: Utc::now(),
232            },
233            MemoryGraphEvent::PromptSubmitted {
234                prompt_id: NodeId::new(),
235                session_id: SessionId::new(),
236                content_length: 100,
237                model: "gpt-4".to_string(),
238                timestamp: Utc::now(),
239            },
240            MemoryGraphEvent::ResponseGenerated {
241                response_id: NodeId::new(),
242                prompt_id: NodeId::new(),
243                content_length: 200,
244                tokens_used: TokenUsage::new(10, 20),
245                latency_ms: 150,
246                timestamp: Utc::now(),
247            },
248            MemoryGraphEvent::ToolInvoked {
249                tool_id: NodeId::new(),
250                tool_name: "calculator".to_string(),
251                success: true,
252                duration_ms: 50,
253                timestamp: Utc::now(),
254            },
255            MemoryGraphEvent::AgentHandoff {
256                from_agent: AgentId::new(),
257                to_agent: AgentId::new(),
258                session_id: SessionId::new(),
259                reason: "specialized task".to_string(),
260                timestamp: Utc::now(),
261            },
262            MemoryGraphEvent::TemplateInstantiated {
263                template_id: TemplateId::new(),
264                prompt_id: NodeId::new(),
265                version: "1.0.0".to_string(),
266                variables: HashMap::new(),
267                timestamp: Utc::now(),
268            },
269            MemoryGraphEvent::QueryExecuted {
270                query_type: "session_nodes".to_string(),
271                results_count: 42,
272                duration_ms: 25,
273                timestamp: Utc::now(),
274            },
275        ];
276
277        for event in events {
278            assert!(!event.key().is_empty());
279            assert!(!event.event_type().is_empty());
280        }
281    }
282
283    #[test]
284    fn test_tool_invoked_event() {
285        let event = MemoryGraphEvent::ToolInvoked {
286            tool_id: NodeId::new(),
287            tool_name: "weather_api".to_string(),
288            success: true,
289            duration_ms: 250,
290            timestamp: Utc::now(),
291        };
292
293        assert_eq!(event.event_type(), "tool_invoked");
294        assert!(event.key().starts_with("tool:"));
295
296        let json = serde_json::to_string(&event).unwrap();
297        let deserialized: MemoryGraphEvent = serde_json::from_str(&json).unwrap();
298        assert_eq!(event.event_type(), deserialized.event_type());
299    }
300
301    #[test]
302    fn test_agent_handoff_event() {
303        let from_agent = AgentId::new();
304        let to_agent = AgentId::new();
305        let session_id = SessionId::new();
306
307        let event = MemoryGraphEvent::AgentHandoff {
308            from_agent,
309            to_agent,
310            session_id,
311            reason: "expertise required".to_string(),
312            timestamp: Utc::now(),
313        };
314
315        assert_eq!(event.event_type(), "agent_handoff");
316        assert!(event.key().contains(&session_id.to_string()));
317
318        let json = serde_json::to_string(&event).unwrap();
319        assert!(json.contains("expertise required"));
320    }
321
322    #[test]
323    fn test_template_instantiated_event() {
324        let template_id = TemplateId::new();
325        let prompt_id = NodeId::new();
326        let mut variables = HashMap::new();
327        variables.insert("name".to_string(), "Alice".to_string());
328        variables.insert("topic".to_string(), "AI".to_string());
329
330        let event = MemoryGraphEvent::TemplateInstantiated {
331            template_id,
332            prompt_id,
333            version: "2.1.0".to_string(),
334            variables: variables.clone(),
335            timestamp: Utc::now(),
336        };
337
338        assert_eq!(event.event_type(), "template_instantiated");
339        assert!(event.key().contains(&template_id.to_string()));
340
341        let json = serde_json::to_string(&event).unwrap();
342        assert!(json.contains("Alice"));
343        assert!(json.contains("2.1.0"));
344    }
345
346    #[test]
347    fn test_query_executed_event() {
348        let event = MemoryGraphEvent::QueryExecuted {
349            query_type: "filtered_search".to_string(),
350            results_count: 128,
351            duration_ms: 45,
352            timestamp: Utc::now(),
353        };
354
355        assert_eq!(event.event_type(), "query_executed");
356        assert!(event.key().contains("filtered_search"));
357
358        let json = serde_json::to_string(&event).unwrap();
359        let deserialized: MemoryGraphEvent = serde_json::from_str(&json).unwrap();
360
361        if let MemoryGraphEvent::QueryExecuted { results_count, .. } = deserialized {
362            assert_eq!(results_count, 128);
363        } else {
364            panic!("Wrong event type");
365        }
366    }
367
368    #[test]
369    fn test_event_timestamp() {
370        let timestamp = Utc::now();
371        let event = MemoryGraphEvent::NodeCreated {
372            node_id: NodeId::new(),
373            node_type: NodeType::Response,
374            session_id: Some(SessionId::new()),
375            timestamp,
376            metadata: HashMap::new(),
377        };
378
379        assert_eq!(event.timestamp(), timestamp);
380    }
381
382    #[test]
383    fn test_metadata_serialization() {
384        let mut metadata = HashMap::new();
385        metadata.insert("model".to_string(), "gpt-4".to_string());
386        metadata.insert("temperature".to_string(), "0.7".to_string());
387
388        let event = MemoryGraphEvent::NodeCreated {
389            node_id: NodeId::new(),
390            node_type: NodeType::Prompt,
391            session_id: Some(SessionId::new()),
392            timestamp: Utc::now(),
393            metadata: metadata.clone(),
394        };
395
396        let json = serde_json::to_string(&event).unwrap();
397        assert!(json.contains("gpt-4"));
398        assert!(json.contains("0.7"));
399
400        let deserialized: MemoryGraphEvent = serde_json::from_str(&json).unwrap();
401        if let MemoryGraphEvent::NodeCreated { metadata: meta, .. } = deserialized {
402            assert_eq!(meta.get("model").unwrap(), "gpt-4");
403            assert_eq!(meta.get("temperature").unwrap(), "0.7");
404        } else {
405            panic!("Wrong event type");
406        }
407    }
408
409    #[test]
410    fn test_response_generated_with_token_usage() {
411        let tokens = TokenUsage::new(150, 300);
412        let event = MemoryGraphEvent::ResponseGenerated {
413            response_id: NodeId::new(),
414            prompt_id: NodeId::new(),
415            content_length: 500,
416            tokens_used: tokens,
417            latency_ms: 1250,
418            timestamp: Utc::now(),
419        };
420
421        let json = serde_json::to_string(&event).unwrap();
422        let deserialized: MemoryGraphEvent = serde_json::from_str(&json).unwrap();
423
424        if let MemoryGraphEvent::ResponseGenerated {
425            tokens_used,
426            latency_ms,
427            ..
428        } = deserialized
429        {
430            assert_eq!(tokens_used.prompt_tokens, 150);
431            assert_eq!(tokens_used.completion_tokens, 300);
432            assert_eq!(latency_ms, 1250);
433        } else {
434            panic!("Wrong event type");
435        }
436    }
437}