mockforge_data/
persona_graph.rs

1//! Persona Graph & Relationship Management
2//!
3//! This module provides graph-based relationship management for personas,
4//! enabling coherent persona switching across related entities (user → orders → payments → support tickets).
5
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet, VecDeque};
8use std::sync::{Arc, RwLock};
9
10/// Represents a node in the persona graph
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PersonaNode {
13    /// Persona ID
14    pub persona_id: String,
15    /// Entity type (e.g., "user", "order", "payment", "support_ticket")
16    pub entity_type: String,
17    /// Relationships from this persona to others
18    /// Key: relationship type (e.g., "has_orders", "has_payments")
19    /// Value: List of related persona IDs
20    pub relationships: HashMap<String, Vec<String>>,
21    /// Additional metadata for the node
22    #[serde(default)]
23    pub metadata: HashMap<String, serde_json::Value>,
24}
25
26impl PersonaNode {
27    /// Create a new persona node
28    pub fn new(persona_id: String, entity_type: String) -> Self {
29        Self {
30            persona_id,
31            entity_type,
32            relationships: HashMap::new(),
33            metadata: HashMap::new(),
34        }
35    }
36
37    /// Add a relationship to another persona
38    pub fn add_relationship(&mut self, relationship_type: String, related_persona_id: String) {
39        self.relationships
40            .entry(relationship_type)
41            .or_default()
42            .push(related_persona_id);
43    }
44
45    /// Get all related personas for a relationship type
46    pub fn get_related(&self, relationship_type: &str) -> Vec<String> {
47        self.relationships.get(relationship_type).cloned().unwrap_or_default()
48    }
49
50    /// Get all relationship types for this node
51    pub fn get_relationship_types(&self) -> Vec<String> {
52        self.relationships.keys().cloned().collect()
53    }
54}
55
56/// Edge in the persona graph
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Edge {
59    /// Source persona ID
60    pub from: String,
61    /// Target persona ID
62    pub to: String,
63    /// Relationship type
64    pub relationship_type: String,
65    /// Edge weight (for weighted traversals, default 1.0)
66    #[serde(default = "default_edge_weight")]
67    pub weight: f64,
68}
69
70fn default_edge_weight() -> f64 {
71    1.0
72}
73
74/// Persona graph for managing entity relationships
75///
76/// Maintains a graph structure of personas and their relationships,
77/// enabling coherent persona switching across related entities.
78#[derive(Debug, Clone)]
79pub struct PersonaGraph {
80    /// Graph nodes indexed by persona ID
81    nodes: Arc<RwLock<HashMap<String, PersonaNode>>>,
82    /// Graph edges indexed by source persona ID
83    edges: Arc<RwLock<HashMap<String, Vec<Edge>>>>,
84    /// Reverse edges for efficient backward traversal
85    reverse_edges: Arc<RwLock<HashMap<String, Vec<Edge>>>>,
86}
87
88impl PersonaGraph {
89    /// Create a new empty persona graph
90    pub fn new() -> Self {
91        Self {
92            nodes: Arc::new(RwLock::new(HashMap::new())),
93            edges: Arc::new(RwLock::new(HashMap::new())),
94            reverse_edges: Arc::new(RwLock::new(HashMap::new())),
95        }
96    }
97
98    /// Add a persona node to the graph
99    pub fn add_node(&self, node: PersonaNode) {
100        let mut nodes = self.nodes.write().unwrap();
101        nodes.insert(node.persona_id.clone(), node);
102    }
103
104    /// Get a node by persona ID
105    pub fn get_node(&self, persona_id: &str) -> Option<PersonaNode> {
106        let nodes = self.nodes.read().unwrap();
107        nodes.get(persona_id).cloned()
108    }
109
110    /// Add an edge between two personas
111    pub fn add_edge(&self, from: String, to: String, relationship_type: String) {
112        let to_clone = to.clone();
113        let edge = Edge {
114            from: from.clone(),
115            to: to_clone.clone(),
116            relationship_type: relationship_type.clone(),
117            weight: 1.0,
118        };
119
120        // Add forward edge
121        let mut edges = self.edges.write().unwrap();
122        edges.entry(from.clone()).or_default().push(edge.clone());
123
124        // Add reverse edge
125        let mut reverse_edges = self.reverse_edges.write().unwrap();
126        reverse_edges.entry(to_clone.clone()).or_default().push(edge);
127
128        // Update node relationships
129        if let Some(node) = self.get_node(&from) {
130            let mut updated_node = node;
131            updated_node.add_relationship(relationship_type, to_clone);
132            self.add_node(updated_node);
133        }
134    }
135
136    /// Get all edges from a persona
137    pub fn get_edges_from(&self, persona_id: &str) -> Vec<Edge> {
138        let edges = self.edges.read().unwrap();
139        edges.get(persona_id).cloned().unwrap_or_default()
140    }
141
142    /// Get all edges to a persona
143    pub fn get_edges_to(&self, persona_id: &str) -> Vec<Edge> {
144        let reverse_edges = self.reverse_edges.read().unwrap();
145        reverse_edges.get(persona_id).cloned().unwrap_or_default()
146    }
147
148    /// Find all related personas using BFS traversal
149    ///
150    /// Traverses the graph starting from the given persona ID,
151    /// following relationships of the specified types.
152    ///
153    /// # Arguments
154    /// * `start_persona_id` - Starting persona ID
155    /// * `relationship_types` - Optional filter for relationship types to follow
156    /// * `max_depth` - Maximum traversal depth (None = unlimited)
157    ///
158    /// # Returns
159    /// Vector of persona IDs reachable from the start persona
160    pub fn find_related_bfs(
161        &self,
162        start_persona_id: &str,
163        relationship_types: Option<&[String]>,
164        max_depth: Option<usize>,
165    ) -> Vec<String> {
166        let mut visited = HashSet::new();
167        let mut queue = VecDeque::new();
168        let mut result = Vec::new();
169
170        queue.push_back((start_persona_id.to_string(), 0));
171        visited.insert(start_persona_id.to_string());
172
173        while let Some((current_id, depth)) = queue.pop_front() {
174            if let Some(max) = max_depth {
175                if depth >= max {
176                    continue;
177                }
178            }
179
180            let edges = self.get_edges_from(&current_id);
181            for edge in edges {
182                // Filter by relationship type if specified
183                if let Some(types) = relationship_types {
184                    if !types.contains(&edge.relationship_type) {
185                        continue;
186                    }
187                }
188
189                if !visited.contains(&edge.to) {
190                    visited.insert(edge.to.clone());
191                    result.push(edge.to.clone());
192                    queue.push_back((edge.to.clone(), depth + 1));
193                }
194            }
195        }
196
197        result
198    }
199
200    /// Find all related personas using DFS traversal
201    ///
202    /// Traverses the graph starting from the given persona ID,
203    /// following relationships of the specified types.
204    ///
205    /// # Arguments
206    /// * `start_persona_id` - Starting persona ID
207    /// * `relationship_types` - Optional filter for relationship types to follow
208    /// * `max_depth` - Maximum traversal depth (None = unlimited)
209    ///
210    /// # Returns
211    /// Vector of persona IDs reachable from the start persona
212    pub fn find_related_dfs(
213        &self,
214        start_persona_id: &str,
215        relationship_types: Option<&[String]>,
216        max_depth: Option<usize>,
217    ) -> Vec<String> {
218        let mut visited = HashSet::new();
219        let mut result = Vec::new();
220
221        self.dfs_recursive(
222            start_persona_id,
223            relationship_types,
224            max_depth,
225            0,
226            &mut visited,
227            &mut result,
228        );
229
230        result
231    }
232
233    /// Recursive helper for DFS traversal
234    fn dfs_recursive(
235        &self,
236        current_id: &str,
237        relationship_types: Option<&[String]>,
238        max_depth: Option<usize>,
239        current_depth: usize,
240        visited: &mut HashSet<String>,
241        result: &mut Vec<String>,
242    ) {
243        if visited.contains(current_id) {
244            return;
245        }
246
247        if let Some(max) = max_depth {
248            if current_depth >= max {
249                return;
250            }
251        }
252
253        visited.insert(current_id.to_string());
254        if current_depth > 0 {
255            // Don't include the start node in results
256            result.push(current_id.to_string());
257        }
258
259        let edges = self.get_edges_from(current_id);
260        for edge in edges {
261            // Filter by relationship type if specified
262            if let Some(types) = relationship_types {
263                if !types.contains(&edge.relationship_type) {
264                    continue;
265                }
266            }
267
268            self.dfs_recursive(
269                &edge.to,
270                relationship_types,
271                max_depth,
272                current_depth + 1,
273                visited,
274                result,
275            );
276        }
277    }
278
279    /// Get the entire subgraph starting from a persona
280    ///
281    /// Returns all nodes and edges reachable from the start persona.
282    pub fn get_subgraph(&self, start_persona_id: &str) -> (Vec<PersonaNode>, Vec<Edge>) {
283        let related_ids = self.find_related_bfs(start_persona_id, None, None);
284        let mut all_ids = vec![start_persona_id.to_string()];
285        all_ids.extend(related_ids);
286
287        let nodes = self.nodes.read().unwrap();
288        let edges = self.edges.read().unwrap();
289
290        let subgraph_nodes: Vec<PersonaNode> =
291            all_ids.iter().filter_map(|id| nodes.get(id).cloned()).collect();
292
293        let subgraph_edges: Vec<Edge> = all_ids
294            .iter()
295            .flat_map(|id| edges.get(id).cloned().unwrap_or_default())
296            .filter(|edge| all_ids.contains(&edge.to))
297            .collect();
298
299        (subgraph_nodes, subgraph_edges)
300    }
301
302    /// Get all nodes in the graph
303    pub fn get_all_nodes(&self) -> Vec<PersonaNode> {
304        let nodes = self.nodes.read().unwrap();
305        nodes.values().cloned().collect()
306    }
307
308    /// Remove a node and all its edges
309    pub fn remove_node(&self, persona_id: &str) {
310        let mut nodes = self.nodes.write().unwrap();
311        nodes.remove(persona_id);
312
313        // Remove forward edges
314        let mut edges = self.edges.write().unwrap();
315        edges.remove(persona_id);
316
317        // Remove reverse edges
318        let mut reverse_edges = self.reverse_edges.write().unwrap();
319        reverse_edges.remove(persona_id);
320
321        // Remove edges pointing to this node
322        for edges_list in edges.values_mut() {
323            edges_list.retain(|e| e.to != persona_id);
324        }
325        for edges_list in reverse_edges.values_mut() {
326            edges_list.retain(|e| e.from != persona_id);
327        }
328    }
329
330    /// Clear the entire graph
331    pub fn clear(&self) {
332        let mut nodes = self.nodes.write().unwrap();
333        nodes.clear();
334
335        let mut edges = self.edges.write().unwrap();
336        edges.clear();
337
338        let mut reverse_edges = self.reverse_edges.write().unwrap();
339        reverse_edges.clear();
340    }
341
342    /// Get graph statistics
343    pub fn get_stats(&self) -> GraphStats {
344        let nodes = self.nodes.read().unwrap();
345        let edges = self.edges.read().unwrap();
346
347        let mut relationship_type_counts = HashMap::new();
348        for edges_list in edges.values() {
349            for edge in edges_list {
350                *relationship_type_counts.entry(edge.relationship_type.clone()).or_insert(0) += 1;
351            }
352        }
353
354        GraphStats {
355            node_count: nodes.len(),
356            edge_count: edges.values().map(|e| e.len()).sum(),
357            relationship_types: relationship_type_counts,
358        }
359    }
360
361    /// Link personas across entity types automatically
362    ///
363    /// Creates relationships between personas based on common entity type patterns:
364    /// - user → has_orders → order
365    /// - user → has_accounts → account
366    /// - order → has_payments → payment
367    /// - user → has_webhooks → webhook
368    /// - user → has_tcp_messages → tcp_message
369    ///
370    /// # Arguments
371    /// * `from_persona_id` - Source persona ID
372    /// * `from_entity_type` - Source entity type (e.g., "user", "order")
373    /// * `to_persona_id` - Target persona ID
374    /// * `to_entity_type` - Target entity type (e.g., "order", "payment")
375    pub fn link_entity_types(
376        &self,
377        from_persona_id: &str,
378        from_entity_type: &str,
379        to_persona_id: &str,
380        to_entity_type: &str,
381    ) {
382        // Determine relationship type based on entity types
383        let relationship_type: String = match (from_entity_type, to_entity_type) {
384            ("user", "order") | ("user", "orders") => "has_orders".to_string(),
385            ("user", "account") | ("user", "accounts") => "has_accounts".to_string(),
386            ("user", "webhook") | ("user", "webhooks") => "has_webhooks".to_string(),
387            ("user", "tcp_message") | ("user", "tcp_messages") => "has_tcp_messages".to_string(),
388            ("order", "payment") | ("order", "payments") => "has_payments".to_string(),
389            ("account", "order") | ("account", "orders") => "has_orders".to_string(),
390            ("account", "payment") | ("account", "payments") => "has_payments".to_string(),
391            _ => {
392                // Generic relationship: from_entity_type -> to_entity_type
393                format!("has_{}", to_entity_type.to_lowercase().trim_end_matches('s'))
394            }
395        };
396
397        // Ensure both nodes exist
398        if self.get_node(from_persona_id).is_none() {
399            let node = PersonaNode::new(from_persona_id.to_string(), from_entity_type.to_string());
400            self.add_node(node);
401        }
402
403        if self.get_node(to_persona_id).is_none() {
404            let node = PersonaNode::new(to_persona_id.to_string(), to_entity_type.to_string());
405            self.add_node(node);
406        }
407
408        // Add the edge
409        self.add_edge(
410            from_persona_id.to_string(),
411            to_persona_id.to_string(),
412            relationship_type.to_string(),
413        );
414    }
415
416    /// Find all related personas of a specific entity type
417    ///
418    /// Traverses the graph to find all personas of the specified entity type
419    /// that are related to the starting persona.
420    ///
421    /// # Arguments
422    /// * `start_persona_id` - Starting persona ID
423    /// * `target_entity_type` - Entity type to find (e.g., "order", "payment")
424    /// * `relationship_type` - Optional relationship type filter (e.g., "has_orders")
425    ///
426    /// # Returns
427    /// Vector of persona IDs matching the criteria
428    pub fn find_related_by_entity_type(
429        &self,
430        start_persona_id: &str,
431        target_entity_type: &str,
432        relationship_type: Option<&str>,
433    ) -> Vec<String> {
434        let related_ids = if let Some(rel_type) = relationship_type {
435            let rel_types = vec![rel_type.to_string()];
436            self.find_related_bfs(start_persona_id, Some(&rel_types), Some(2))
437        } else {
438            self.find_related_bfs(start_persona_id, None, Some(2))
439        };
440
441        // Filter by entity type
442        related_ids
443            .into_iter()
444            .filter_map(|persona_id| {
445                if let Some(node) = self.get_node(&persona_id) {
446                    if node.entity_type.to_lowercase() == target_entity_type.to_lowercase() {
447                        Some(persona_id)
448                    } else {
449                        None
450                    }
451                } else {
452                    None
453                }
454            })
455            .collect()
456    }
457
458    /// Get or create a persona node and link it to related entities
459    ///
460    /// This is a convenience method that creates a persona node if it doesn't exist
461    /// and automatically establishes relationships based on entity type patterns.
462    ///
463    /// # Arguments
464    /// * `persona_id` - Persona ID
465    /// * `entity_type` - Entity type (e.g., "user", "order", "payment")
466    /// * `related_entity_id` - Optional related entity ID to link to
467    /// * `related_entity_type` - Optional related entity type
468    pub fn get_or_create_node_with_links(
469        &self,
470        persona_id: &str,
471        entity_type: &str,
472        related_entity_id: Option<&str>,
473        related_entity_type: Option<&str>,
474    ) -> PersonaNode {
475        // Get or create the node
476        let node = if let Some(existing) = self.get_node(persona_id) {
477            existing
478        } else {
479            let new_node = PersonaNode::new(persona_id.to_string(), entity_type.to_string());
480            self.add_node(new_node.clone());
481            new_node
482        };
483
484        // Link to related entity if provided
485        if let (Some(related_id), Some(related_type)) = (related_entity_id, related_entity_type) {
486            self.link_entity_types(persona_id, entity_type, related_id, related_type);
487        }
488
489        node
490    }
491}
492
493impl Default for PersonaGraph {
494    fn default() -> Self {
495        Self::new()
496    }
497}
498
499/// Graph statistics
500#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct GraphStats {
502    /// Number of nodes in the graph
503    pub node_count: usize,
504    /// Number of edges in the graph
505    pub edge_count: usize,
506    /// Count of edges by relationship type
507    pub relationship_types: HashMap<String, usize>,
508}
509
510/// Graph visualization data structure
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct GraphVisualization {
513    /// Nodes in the graph
514    pub nodes: Vec<VisualizationNode>,
515    /// Edges in the graph
516    pub edges: Vec<VisualizationEdge>,
517}
518
519/// Node for visualization
520#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct VisualizationNode {
522    /// Persona ID
523    pub id: String,
524    /// Entity type
525    pub entity_type: String,
526    /// Display label
527    pub label: String,
528    /// Node position (for layout algorithms)
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub position: Option<(f64, f64)>,
531}
532
533/// Edge for visualization
534#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct VisualizationEdge {
536    /// Source persona ID
537    pub from: String,
538    /// Target persona ID
539    pub to: String,
540    /// Relationship type
541    pub relationship_type: String,
542    /// Display label
543    pub label: String,
544}
545
546impl PersonaGraph {
547    /// Generate visualization data for the graph
548    pub fn to_visualization(&self) -> GraphVisualization {
549        let nodes = self.get_all_nodes();
550        let edges = self.edges.read().unwrap();
551
552        let vis_nodes: Vec<VisualizationNode> = nodes
553            .iter()
554            .map(|node| VisualizationNode {
555                id: node.persona_id.clone(),
556                entity_type: node.entity_type.clone(),
557                label: format!("{} ({})", node.persona_id, node.entity_type),
558                position: None,
559            })
560            .collect();
561
562        let vis_edges: Vec<VisualizationEdge> = edges
563            .values()
564            .flatten()
565            .map(|edge| VisualizationEdge {
566                from: edge.from.clone(),
567                to: edge.to.clone(),
568                relationship_type: edge.relationship_type.clone(),
569                label: edge.relationship_type.clone(),
570            })
571            .collect();
572
573        GraphVisualization {
574            nodes: vis_nodes,
575            edges: vis_edges,
576        }
577    }
578}