Skip to main content

argentor_memory/
knowledge_graph.rs

1//! In-memory knowledge graph for entity-relationship storage.
2//!
3//! Tracks entities (people, concepts, tools, files), relationships between them,
4//! and facts with temporal metadata. Supports querying by entity, relationship type,
5//! and neighborhood traversal.
6//!
7//! # Main types
8//!
9//! - [`Entity`] — A node in the knowledge graph.
10//! - [`Relationship`] — A directed edge between two entities.
11//! - [`KnowledgeGraph`] — The graph container with CRUD, traversal, and persistence.
12//! - [`GraphSummary`] — Aggregate statistics about the graph.
13
14use argentor_core::{ArgentorError, ArgentorResult};
15use chrono::{DateTime, Utc};
16use regex::Regex;
17use serde::{Deserialize, Serialize};
18use std::collections::{HashMap, HashSet, VecDeque};
19use std::path::Path;
20use uuid::Uuid;
21
22// ---------------------------------------------------------------------------
23// Entity types
24// ---------------------------------------------------------------------------
25
26/// A node in the knowledge graph.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Entity {
29    /// Unique identifier for this entity.
30    pub id: String,
31    /// Human-readable name.
32    pub name: String,
33    /// The kind of entity (Person, Concept, Tool, etc.).
34    pub entity_type: EntityType,
35    /// Arbitrary key-value properties associated with this entity.
36    pub properties: HashMap<String, serde_json::Value>,
37    /// When this entity was first created.
38    pub created_at: DateTime<Utc>,
39    /// When this entity was last modified.
40    pub updated_at: DateTime<Utc>,
41    /// Confidence score (0.0 -- 1.0) representing extraction reliability.
42    pub confidence: f64,
43    /// Origin of this entity: "user", "agent", "tool_result", "extracted".
44    pub source: String,
45}
46
47/// Classification of entity types.
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
49pub enum EntityType {
50    /// A human individual.
51    Person,
52    /// A company, team, or institution.
53    Organization,
54    /// An abstract concept or topic.
55    Concept,
56    /// A tool, skill, or command.
57    Tool,
58    /// A file or path on disk.
59    File,
60    /// A geographic or network location.
61    Location,
62    /// A discrete event with temporal bounds.
63    Event,
64    /// A standalone factual assertion.
65    Fact,
66    /// Application-defined entity type.
67    Custom(String),
68}
69
70impl std::fmt::Display for EntityType {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        match self {
73            Self::Person => write!(f, "Person"),
74            Self::Organization => write!(f, "Organization"),
75            Self::Concept => write!(f, "Concept"),
76            Self::Tool => write!(f, "Tool"),
77            Self::File => write!(f, "File"),
78            Self::Location => write!(f, "Location"),
79            Self::Event => write!(f, "Event"),
80            Self::Fact => write!(f, "Fact"),
81            Self::Custom(s) => write!(f, "Custom({s})"),
82        }
83    }
84}
85
86// ---------------------------------------------------------------------------
87// Relationship types
88// ---------------------------------------------------------------------------
89
90/// A directed edge between two entities.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct Relationship {
93    /// Unique identifier for this relationship.
94    pub id: String,
95    /// Source entity ID.
96    pub from_entity: String,
97    /// Target entity ID.
98    pub to_entity: String,
99    /// Kind of relationship.
100    pub relation_type: RelationType,
101    /// Arbitrary properties on the edge.
102    pub properties: HashMap<String, serde_json::Value>,
103    /// Importance / confidence weight (0.0 -- 1.0).
104    pub weight: f64,
105    /// When this relationship was created.
106    pub created_at: DateTime<Utc>,
107    /// Origin of this relationship.
108    pub source: String,
109}
110
111/// Classification of relationship types.
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
113pub enum RelationType {
114    /// Subsumption: "Dog IsA Animal".
115    IsA,
116    /// Property attribution: "User HasProperty email".
117    HasProperty,
118    /// Generic association.
119    RelatedTo,
120    /// Dependency: "TaskA DependsOn TaskB".
121    DependsOn,
122    /// Authorship: "Report CreatedBy Agent".
123    CreatedBy,
124    /// Containment: "Project Contains File".
125    Contains,
126    /// Collaboration: "Alice WorksWith Bob".
127    WorksWith,
128    /// Reference: "Message Mentions Entity".
129    Mentions,
130    /// Tool usage: "Agent UsedTool calculator".
131    UsedTool,
132    /// Output production: "Step ProducedOutput artifact".
133    ProducedOutput,
134    /// Application-defined relationship type.
135    Custom(String),
136}
137
138impl std::fmt::Display for RelationType {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            Self::IsA => write!(f, "IsA"),
142            Self::HasProperty => write!(f, "HasProperty"),
143            Self::RelatedTo => write!(f, "RelatedTo"),
144            Self::DependsOn => write!(f, "DependsOn"),
145            Self::CreatedBy => write!(f, "CreatedBy"),
146            Self::Contains => write!(f, "Contains"),
147            Self::WorksWith => write!(f, "WorksWith"),
148            Self::Mentions => write!(f, "Mentions"),
149            Self::UsedTool => write!(f, "UsedTool"),
150            Self::ProducedOutput => write!(f, "ProducedOutput"),
151            Self::Custom(s) => write!(f, "Custom({s})"),
152        }
153    }
154}
155
156// ---------------------------------------------------------------------------
157// Graph summary
158// ---------------------------------------------------------------------------
159
160/// Aggregate statistics about the knowledge graph.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct GraphSummary {
163    /// Total number of entities.
164    pub entity_count: usize,
165    /// Total number of relationships.
166    pub relationship_count: usize,
167    /// Count of entities by type.
168    pub entity_types: HashMap<String, usize>,
169    /// Count of relationships by type.
170    pub relationship_types: HashMap<String, usize>,
171    /// Top entities by total connection count (name, count), descending.
172    pub most_connected: Vec<(String, usize)>,
173}
174
175// ---------------------------------------------------------------------------
176// Serializable graph for persistence
177// ---------------------------------------------------------------------------
178
179#[derive(Serialize, Deserialize)]
180struct SerializableGraph {
181    entities: Vec<Entity>,
182    relationships: Vec<Relationship>,
183}
184
185// ---------------------------------------------------------------------------
186// KnowledgeGraph
187// ---------------------------------------------------------------------------
188
189/// In-memory knowledge graph with indexed lookups and graph traversal.
190pub struct KnowledgeGraph {
191    entities: HashMap<String, Entity>,
192    relationships: Vec<Relationship>,
193    // Indexes
194    entity_by_name: HashMap<String, Vec<String>>,
195    entity_by_type: HashMap<EntityType, Vec<String>>,
196    relations_from: HashMap<String, Vec<usize>>,
197    relations_to: HashMap<String, Vec<usize>>,
198}
199
200impl Default for KnowledgeGraph {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206impl KnowledgeGraph {
207    /// Create an empty knowledge graph.
208    pub fn new() -> Self {
209        Self {
210            entities: HashMap::new(),
211            relationships: Vec::new(),
212            entity_by_name: HashMap::new(),
213            entity_by_type: HashMap::new(),
214            relations_from: HashMap::new(),
215            relations_to: HashMap::new(),
216        }
217    }
218
219    // -----------------------------------------------------------------------
220    // Entity CRUD
221    // -----------------------------------------------------------------------
222
223    /// Add an entity to the graph. Returns the entity ID.
224    ///
225    /// If the entity's `id` field is empty, a new UUID is generated.
226    pub fn add_entity(&mut self, mut entity: Entity) -> String {
227        if entity.id.is_empty() {
228            entity.id = Uuid::new_v4().to_string();
229        }
230        let id = entity.id.clone();
231        let name_lower = entity.name.to_lowercase();
232
233        // Update name index
234        self.entity_by_name
235            .entry(name_lower)
236            .or_default()
237            .push(id.clone());
238
239        // Update type index
240        self.entity_by_type
241            .entry(entity.entity_type.clone())
242            .or_default()
243            .push(id.clone());
244
245        self.entities.insert(id.clone(), entity);
246        id
247    }
248
249    /// Retrieve an entity by ID.
250    pub fn get_entity(&self, id: &str) -> Option<&Entity> {
251        self.entities.get(id)
252    }
253
254    /// Find all entities whose name matches (case-insensitive substring).
255    pub fn find_entities(&self, name: &str) -> Vec<&Entity> {
256        let query = name.to_lowercase();
257        self.entity_by_name
258            .iter()
259            .filter(|(k, _)| k.contains(&query))
260            .flat_map(|(_, ids)| ids.iter().filter_map(|id| self.entities.get(id)))
261            .collect()
262    }
263
264    /// Find all entities of a given type.
265    pub fn find_by_type(&self, entity_type: &EntityType) -> Vec<&Entity> {
266        self.entity_by_type
267            .get(entity_type)
268            .map(|ids| ids.iter().filter_map(|id| self.entities.get(id)).collect())
269            .unwrap_or_default()
270    }
271
272    /// Update an entity's properties. Returns `true` if the entity was found and updated.
273    pub fn update_entity(
274        &mut self,
275        id: &str,
276        properties: HashMap<String, serde_json::Value>,
277    ) -> bool {
278        if let Some(entity) = self.entities.get_mut(id) {
279            for (k, v) in properties {
280                entity.properties.insert(k, v);
281            }
282            entity.updated_at = Utc::now();
283            true
284        } else {
285            false
286        }
287    }
288
289    /// Remove an entity and all its incident relationships.
290    /// Returns `true` if the entity existed.
291    pub fn remove_entity(&mut self, id: &str) -> bool {
292        let entity = match self.entities.remove(id) {
293            Some(e) => e,
294            None => return false,
295        };
296
297        // Remove from name index
298        let name_lower = entity.name.to_lowercase();
299        if let Some(ids) = self.entity_by_name.get_mut(&name_lower) {
300            ids.retain(|i| i != id);
301            if ids.is_empty() {
302                self.entity_by_name.remove(&name_lower);
303            }
304        }
305
306        // Remove from type index
307        if let Some(ids) = self.entity_by_type.get_mut(&entity.entity_type) {
308            ids.retain(|i| i != id);
309            if ids.is_empty() {
310                self.entity_by_type.remove(&entity.entity_type);
311            }
312        }
313
314        // Remove incident relationships (rebuild to avoid index invalidation)
315        self.relationships
316            .retain(|r| r.from_entity != id && r.to_entity != id);
317        self.rebuild_relation_indexes();
318
319        true
320    }
321
322    // -----------------------------------------------------------------------
323    // Relationship CRUD
324    // -----------------------------------------------------------------------
325
326    /// Add a relationship to the graph. Returns the relationship ID.
327    ///
328    /// If the relationship's `id` field is empty, a new UUID is generated.
329    pub fn add_relationship(&mut self, mut rel: Relationship) -> String {
330        if rel.id.is_empty() {
331            rel.id = Uuid::new_v4().to_string();
332        }
333        let id = rel.id.clone();
334        let idx = self.relationships.len();
335
336        self.relations_from
337            .entry(rel.from_entity.clone())
338            .or_default()
339            .push(idx);
340        self.relations_to
341            .entry(rel.to_entity.clone())
342            .or_default()
343            .push(idx);
344
345        self.relationships.push(rel);
346        id
347    }
348
349    /// Get all relationships originating from a given entity.
350    pub fn get_relationships_from(&self, entity_id: &str) -> Vec<&Relationship> {
351        self.relations_from
352            .get(entity_id)
353            .map(|idxs| {
354                idxs.iter()
355                    .filter_map(|&i| self.relationships.get(i))
356                    .collect()
357            })
358            .unwrap_or_default()
359    }
360
361    /// Get all relationships pointing to a given entity.
362    pub fn get_relationships_to(&self, entity_id: &str) -> Vec<&Relationship> {
363        self.relations_to
364            .get(entity_id)
365            .map(|idxs| {
366                idxs.iter()
367                    .filter_map(|&i| self.relationships.get(i))
368                    .collect()
369            })
370            .unwrap_or_default()
371    }
372
373    /// Find relationships matching optional filters.
374    pub fn find_relationships(
375        &self,
376        from: Option<&str>,
377        to: Option<&str>,
378        rel_type: Option<&RelationType>,
379    ) -> Vec<&Relationship> {
380        self.relationships
381            .iter()
382            .filter(|r| {
383                from.map_or(true, |f| r.from_entity == f)
384                    && to.map_or(true, |t| r.to_entity == t)
385                    && rel_type.map_or(true, |rt| &r.relation_type == rt)
386            })
387            .collect()
388    }
389
390    /// Remove a relationship by ID. Returns `true` if it was found and removed.
391    pub fn remove_relationship(&mut self, id: &str) -> bool {
392        let before = self.relationships.len();
393        self.relationships.retain(|r| r.id != id);
394        if self.relationships.len() < before {
395            self.rebuild_relation_indexes();
396            true
397        } else {
398            false
399        }
400    }
401
402    // -----------------------------------------------------------------------
403    // Graph queries
404    // -----------------------------------------------------------------------
405
406    /// BFS traversal to collect all neighbor entities within a given depth.
407    ///
408    /// Treats the graph as undirected for traversal purposes.
409    pub fn neighbors(&self, entity_id: &str, depth: usize) -> Vec<&Entity> {
410        if !self.entities.contains_key(entity_id) {
411            return Vec::new();
412        }
413
414        let mut visited: HashSet<&str> = HashSet::new();
415        visited.insert(entity_id);
416
417        let mut queue: VecDeque<(&str, usize)> = VecDeque::new();
418        queue.push_back((entity_id, 0));
419
420        let mut result = Vec::new();
421
422        while let Some((current, d)) = queue.pop_front() {
423            if d >= depth {
424                continue;
425            }
426
427            // Outgoing edges
428            if let Some(idxs) = self.relations_from.get(current) {
429                for &idx in idxs {
430                    if let Some(rel) = self.relationships.get(idx) {
431                        let neighbor = rel.to_entity.as_str();
432                        if visited.insert(neighbor) {
433                            if let Some(entity) = self.entities.get(neighbor) {
434                                result.push(entity);
435                                queue.push_back((neighbor, d + 1));
436                            }
437                        }
438                    }
439                }
440            }
441
442            // Incoming edges (undirected traversal)
443            if let Some(idxs) = self.relations_to.get(current) {
444                for &idx in idxs {
445                    if let Some(rel) = self.relationships.get(idx) {
446                        let neighbor = rel.from_entity.as_str();
447                        if visited.insert(neighbor) {
448                            if let Some(entity) = self.entities.get(neighbor) {
449                                result.push(entity);
450                                queue.push_back((neighbor, d + 1));
451                            }
452                        }
453                    }
454                }
455            }
456        }
457
458        result
459    }
460
461    /// BFS shortest path between two entities. Returns the list of entity IDs along the path
462    /// (including start and end), or `None` if no path exists.
463    ///
464    /// Treats the graph as undirected.
465    pub fn shortest_path(&self, from: &str, to: &str) -> Option<Vec<String>> {
466        if from == to {
467            return Some(vec![from.to_string()]);
468        }
469        if !self.entities.contains_key(from) || !self.entities.contains_key(to) {
470            return None;
471        }
472
473        let mut visited: HashSet<String> = HashSet::new();
474        visited.insert(from.to_string());
475
476        let mut queue: VecDeque<Vec<String>> = VecDeque::new();
477        queue.push_back(vec![from.to_string()]);
478
479        while let Some(path) = queue.pop_front() {
480            let Some(last) = path.last() else {
481                continue;
482            };
483            let current = last.as_str();
484
485            let neighbor_ids = self.adjacent_ids(current);
486            for neighbor in neighbor_ids {
487                if neighbor == to {
488                    let mut full = path.clone();
489                    full.push(neighbor);
490                    return Some(full);
491                }
492                if visited.insert(neighbor.clone()) {
493                    let mut new_path = path.clone();
494                    new_path.push(neighbor);
495                    queue.push_back(new_path);
496                }
497            }
498        }
499
500        None
501    }
502
503    /// Return all entities in the connected component containing `entity_id`.
504    /// Treats the graph as undirected.
505    pub fn connected_component(&self, entity_id: &str) -> Vec<&Entity> {
506        if !self.entities.contains_key(entity_id) {
507            return Vec::new();
508        }
509
510        let mut visited: HashSet<&str> = HashSet::new();
511        visited.insert(entity_id);
512
513        let mut queue: VecDeque<&str> = VecDeque::new();
514        queue.push_back(entity_id);
515
516        let mut result = Vec::new();
517
518        // Include the starting entity itself
519        if let Some(e) = self.entities.get(entity_id) {
520            result.push(e);
521        }
522
523        while let Some(current) = queue.pop_front() {
524            // Outgoing
525            if let Some(idxs) = self.relations_from.get(current) {
526                for &idx in idxs {
527                    if let Some(rel) = self.relationships.get(idx) {
528                        let neighbor = rel.to_entity.as_str();
529                        if visited.insert(neighbor) {
530                            if let Some(entity) = self.entities.get(neighbor) {
531                                result.push(entity);
532                                queue.push_back(neighbor);
533                            }
534                        }
535                    }
536                }
537            }
538
539            // Incoming
540            if let Some(idxs) = self.relations_to.get(current) {
541                for &idx in idxs {
542                    if let Some(rel) = self.relationships.get(idx) {
543                        let neighbor = rel.from_entity.as_str();
544                        if visited.insert(neighbor) {
545                            if let Some(entity) = self.entities.get(neighbor) {
546                                result.push(entity);
547                                queue.push_back(neighbor);
548                            }
549                        }
550                    }
551                }
552            }
553        }
554
555        result
556    }
557
558    /// Total number of entities.
559    pub fn entity_count(&self) -> usize {
560        self.entities.len()
561    }
562
563    /// Total number of relationships.
564    pub fn relationship_count(&self) -> usize {
565        self.relationships.len()
566    }
567
568    // -----------------------------------------------------------------------
569    // Knowledge extraction
570    // -----------------------------------------------------------------------
571
572    /// Extract entities from free text using regex-based heuristics.
573    ///
574    /// Detects:
575    /// - Capitalized multi-word phrases (potential names / organizations)
576    /// - Email addresses (creates Person entities)
577    /// - URLs (creates Custom("Url") entities)
578    /// - @mentions (creates Person entities)
579    /// - #hashtags (creates Concept entities)
580    /// - File paths (creates File entities)
581    ///
582    /// Returns the IDs of all newly created entities.
583    pub fn extract_entities_from_text(&mut self, text: &str, source: &str) -> Vec<String> {
584        let mut ids = Vec::new();
585        let now = Utc::now();
586
587        // All patterns are compile-time constant strings; return empty if any
588        // somehow fails (should never happen).
589        let Ok(email_re) = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}") else {
590            return ids;
591        };
592        let Ok(url_re) = Regex::new(r"https?://[a-zA-Z0-9._~:/?#\[\]@!$&'()*+,;=%-]+") else {
593            return ids;
594        };
595        let Ok(mention_re) = Regex::new(r"@([a-zA-Z0-9_]+)") else {
596            return ids;
597        };
598        let Ok(hashtag_re) = Regex::new(r"#([a-zA-Z0-9_]+)") else {
599            return ids;
600        };
601        let Ok(filepath_re) = Regex::new(r"(?:^|[\s(])(/[a-zA-Z0-9._/-]+\.[a-zA-Z0-9]+)") else {
602            return ids;
603        };
604        let Ok(cap_phrase_re) = Regex::new(r"\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b") else {
605            return ids;
606        };
607
608        // Track what we already extracted to avoid duplicates within one call
609        let mut seen: HashSet<String> = HashSet::new();
610
611        // Emails -> Person
612        for m in email_re.find_iter(text) {
613            let email = m.as_str().to_string();
614            if seen.insert(email.clone()) {
615                let name = email.split('@').next().unwrap_or(&email).to_string();
616                let entity = Entity {
617                    id: String::new(),
618                    name: name.clone(),
619                    entity_type: EntityType::Person,
620                    properties: HashMap::from([(
621                        "email".to_string(),
622                        serde_json::Value::String(email),
623                    )]),
624                    created_at: now,
625                    updated_at: now,
626                    confidence: 0.8,
627                    source: source.to_string(),
628                };
629                ids.push(self.add_entity(entity));
630            }
631        }
632
633        // URLs -> Custom("Url")
634        for m in url_re.find_iter(text) {
635            let url = m.as_str().to_string();
636            if seen.insert(url.clone()) {
637                let entity = Entity {
638                    id: String::new(),
639                    name: url.clone(),
640                    entity_type: EntityType::Custom("Url".to_string()),
641                    properties: HashMap::from([(
642                        "url".to_string(),
643                        serde_json::Value::String(url),
644                    )]),
645                    created_at: now,
646                    updated_at: now,
647                    confidence: 0.9,
648                    source: source.to_string(),
649                };
650                ids.push(self.add_entity(entity));
651            }
652        }
653
654        // @mentions -> Person
655        for cap in mention_re.captures_iter(text) {
656            let username = cap[1].to_string();
657            let key = format!("@{username}");
658            if seen.insert(key) {
659                let entity = Entity {
660                    id: String::new(),
661                    name: username.clone(),
662                    entity_type: EntityType::Person,
663                    properties: HashMap::from([(
664                        "mention".to_string(),
665                        serde_json::Value::String(format!("@{username}")),
666                    )]),
667                    created_at: now,
668                    updated_at: now,
669                    confidence: 0.7,
670                    source: source.to_string(),
671                };
672                ids.push(self.add_entity(entity));
673            }
674        }
675
676        // #hashtags -> Concept
677        for cap in hashtag_re.captures_iter(text) {
678            let tag = cap[1].to_string();
679            let key = format!("#{tag}");
680            if seen.insert(key) {
681                let entity = Entity {
682                    id: String::new(),
683                    name: tag.clone(),
684                    entity_type: EntityType::Concept,
685                    properties: HashMap::from([(
686                        "hashtag".to_string(),
687                        serde_json::Value::String(format!("#{tag}")),
688                    )]),
689                    created_at: now,
690                    updated_at: now,
691                    confidence: 0.8,
692                    source: source.to_string(),
693                };
694                ids.push(self.add_entity(entity));
695            }
696        }
697
698        // File paths -> File
699        for cap in filepath_re.captures_iter(text) {
700            let path = cap[1].to_string();
701            if seen.insert(path.clone()) {
702                let file_name = path.rsplit('/').next().unwrap_or(&path).to_string();
703                let entity = Entity {
704                    id: String::new(),
705                    name: file_name,
706                    entity_type: EntityType::File,
707                    properties: HashMap::from([(
708                        "path".to_string(),
709                        serde_json::Value::String(path),
710                    )]),
711                    created_at: now,
712                    updated_at: now,
713                    confidence: 0.85,
714                    source: source.to_string(),
715                };
716                ids.push(self.add_entity(entity));
717            }
718        }
719
720        // Capitalized phrases -> Person (heuristic: multi-word capitalized = name/org)
721        for cap in cap_phrase_re.captures_iter(text) {
722            let phrase = cap[1].to_string();
723            if seen.insert(phrase.clone()) {
724                // Skip if it is just two very common words
725                let entity = Entity {
726                    id: String::new(),
727                    name: phrase,
728                    entity_type: EntityType::Person,
729                    properties: HashMap::new(),
730                    created_at: now,
731                    updated_at: now,
732                    confidence: 0.5,
733                    source: source.to_string(),
734                };
735                ids.push(self.add_entity(entity));
736            }
737        }
738
739        ids
740    }
741
742    // -----------------------------------------------------------------------
743    // Entity merge (deduplication)
744    // -----------------------------------------------------------------------
745
746    /// Merge `source_id` entity into `target_id`.
747    ///
748    /// All properties from the source are copied to the target (existing keys are
749    /// not overwritten). All relationships that reference the source entity are
750    /// redirected to the target. The source entity is then removed.
751    pub fn merge_entity(&mut self, source_id: &str, target_id: &str) -> ArgentorResult<()> {
752        if source_id == target_id {
753            return Err(ArgentorError::Agent(
754                "Cannot merge an entity with itself".to_string(),
755            ));
756        }
757
758        let source = self
759            .entities
760            .get(source_id)
761            .ok_or_else(|| ArgentorError::Agent(format!("Source entity '{source_id}' not found")))?
762            .clone();
763
764        let target = self.entities.get(target_id).ok_or_else(|| {
765            ArgentorError::Agent(format!("Target entity '{target_id}' not found"))
766        })?;
767
768        // Merge properties (source into target, don't overwrite existing)
769        let mut merged_props = target.properties.clone();
770        for (k, v) in &source.properties {
771            merged_props.entry(k.clone()).or_insert_with(|| v.clone());
772        }
773
774        if let Some(target_mut) = self.entities.get_mut(target_id) {
775            target_mut.properties = merged_props;
776            target_mut.updated_at = Utc::now();
777        }
778
779        // Redirect relationships
780        for rel in &mut self.relationships {
781            if rel.from_entity == source_id {
782                rel.from_entity = target_id.to_string();
783            }
784            if rel.to_entity == source_id {
785                rel.to_entity = target_id.to_string();
786            }
787        }
788
789        // Remove self-loops that may have been created by the merge
790        self.relationships
791            .retain(|r| !(r.from_entity == r.to_entity && r.from_entity == target_id));
792
793        // Remove source entity from maps (not using remove_entity to avoid
794        // double-removing relationships we already redirected).
795        self.entities.remove(source_id);
796
797        // Remove from name index
798        let name_lower = source.name.to_lowercase();
799        if let Some(ids) = self.entity_by_name.get_mut(&name_lower) {
800            ids.retain(|i| i != source_id);
801            if ids.is_empty() {
802                self.entity_by_name.remove(&name_lower);
803            }
804        }
805
806        // Remove from type index
807        if let Some(ids) = self.entity_by_type.get_mut(&source.entity_type) {
808            ids.retain(|i| i != source_id);
809            if ids.is_empty() {
810                self.entity_by_type.remove(&source.entity_type);
811            }
812        }
813
814        // Rebuild relation indexes since we mutated from/to
815        self.rebuild_relation_indexes();
816
817        Ok(())
818    }
819
820    // -----------------------------------------------------------------------
821    // Summary & context
822    // -----------------------------------------------------------------------
823
824    /// Compute aggregate statistics about the graph.
825    pub fn summarize(&self) -> GraphSummary {
826        let mut entity_types: HashMap<String, usize> = HashMap::new();
827        for entity in self.entities.values() {
828            *entity_types
829                .entry(entity.entity_type.to_string())
830                .or_default() += 1;
831        }
832
833        let mut relationship_types: HashMap<String, usize> = HashMap::new();
834        for rel in &self.relationships {
835            *relationship_types
836                .entry(rel.relation_type.to_string())
837                .or_default() += 1;
838        }
839
840        // Connection count per entity (outgoing + incoming)
841        let mut connection_count: HashMap<&str, usize> = HashMap::new();
842        for rel in &self.relationships {
843            *connection_count.entry(&rel.from_entity).or_default() += 1;
844            *connection_count.entry(&rel.to_entity).or_default() += 1;
845        }
846
847        let mut most_connected: Vec<(String, usize)> = connection_count
848            .into_iter()
849            .filter_map(|(id, count)| self.entities.get(id).map(|e| (e.name.clone(), count)))
850            .collect();
851        most_connected.sort_by_key(|entry| std::cmp::Reverse(entry.1));
852        most_connected.truncate(10);
853
854        GraphSummary {
855            entity_count: self.entities.len(),
856            relationship_count: self.relationships.len(),
857            entity_types,
858            relationship_types,
859            most_connected,
860        }
861    }
862
863    /// Generate a human-readable context string about an entity and its neighborhood.
864    ///
865    /// Output includes entity properties, direct relationships, and optionally
866    /// relationships of neighbors up to the given depth.
867    pub fn to_context_string(&self, entity_id: &str, depth: usize) -> String {
868        let entity = match self.entities.get(entity_id) {
869            Some(e) => e,
870            None => return format!("Entity '{entity_id}' not found."),
871        };
872
873        let mut out = String::new();
874
875        // Entity header
876        out.push_str(&format!(
877            "Entity: {} ({})\n",
878            entity.name, entity.entity_type
879        ));
880
881        // Properties
882        if !entity.properties.is_empty() {
883            let props: Vec<String> = entity
884                .properties
885                .iter()
886                .map(|(k, v)| {
887                    let val = match v {
888                        serde_json::Value::String(s) => s.clone(),
889                        other => other.to_string(),
890                    };
891                    format!("{k}={val}")
892                })
893                .collect();
894            out.push_str(&format!("  Properties: {}\n", props.join(", ")));
895        }
896
897        // Direct relationships (outgoing)
898        let outgoing = self.get_relationships_from(entity_id);
899        let incoming = self.get_relationships_to(entity_id);
900
901        if !outgoing.is_empty() || !incoming.is_empty() {
902            out.push_str("  Relationships:\n");
903            for rel in &outgoing {
904                let target_name = self
905                    .entities
906                    .get(&rel.to_entity)
907                    .map(|e| format!("{} ({})", e.name, e.entity_type))
908                    .unwrap_or_else(|| rel.to_entity.clone());
909                out.push_str(&format!(
910                    "    -> {} -> {}\n",
911                    rel.relation_type, target_name
912                ));
913            }
914            for rel in &incoming {
915                let source_name = self
916                    .entities
917                    .get(&rel.from_entity)
918                    .map(|e| format!("{} ({})", e.name, e.entity_type))
919                    .unwrap_or_else(|| rel.from_entity.clone());
920                out.push_str(&format!(
921                    "    <- {} <- {}\n",
922                    rel.relation_type, source_name
923                ));
924            }
925        }
926
927        // Neighbor relationships at deeper levels
928        if depth > 1 {
929            let neighbors = self.neighbors(entity_id, depth);
930            if !neighbors.is_empty() {
931                out.push_str(&format!("  Neighbors (depth {depth}):\n"));
932                for neighbor in &neighbors {
933                    let n_out = self.get_relationships_from(&neighbor.id);
934                    for rel in n_out {
935                        if rel.to_entity == entity_id {
936                            continue; // skip back-link to origin
937                        }
938                        let target_name = self
939                            .entities
940                            .get(&rel.to_entity)
941                            .map(|e| e.name.as_str())
942                            .unwrap_or("?");
943                        out.push_str(&format!(
944                            "    {} -> {} -> {}\n",
945                            neighbor.name, rel.relation_type, target_name
946                        ));
947                    }
948                }
949            }
950        }
951
952        out
953    }
954
955    // -----------------------------------------------------------------------
956    // Persistence
957    // -----------------------------------------------------------------------
958
959    /// Save the graph to a JSON file.
960    pub fn save(&self, path: &Path) -> ArgentorResult<()> {
961        let data = SerializableGraph {
962            entities: self.entities.values().cloned().collect(),
963            relationships: self.relationships.clone(),
964        };
965        let json = serde_json::to_string_pretty(&data)
966            .map_err(|e| ArgentorError::Session(format!("Failed to serialize graph: {e}")))?;
967
968        if let Some(parent) = path.parent() {
969            std::fs::create_dir_all(parent)
970                .map_err(|e| ArgentorError::Session(format!("Failed to create dir: {e}")))?;
971        }
972        std::fs::write(path, json)
973            .map_err(|e| ArgentorError::Session(format!("Failed to write graph: {e}")))?;
974        Ok(())
975    }
976
977    /// Load a graph from a JSON file.
978    pub fn load(path: &Path) -> ArgentorResult<Self> {
979        let data = std::fs::read_to_string(path)
980            .map_err(|e| ArgentorError::Session(format!("Failed to read graph: {e}")))?;
981        let sg: SerializableGraph = serde_json::from_str(&data)
982            .map_err(|e| ArgentorError::Session(format!("Failed to deserialize graph: {e}")))?;
983
984        let mut graph = Self::new();
985        for entity in sg.entities {
986            graph.add_entity(entity);
987        }
988        for rel in sg.relationships {
989            graph.add_relationship(rel);
990        }
991        Ok(graph)
992    }
993
994    // -----------------------------------------------------------------------
995    // Internal helpers
996    // -----------------------------------------------------------------------
997
998    /// Collect IDs of all adjacent entities (both directions).
999    fn adjacent_ids(&self, entity_id: &str) -> Vec<String> {
1000        let mut result = Vec::new();
1001        if let Some(idxs) = self.relations_from.get(entity_id) {
1002            for &idx in idxs {
1003                if let Some(rel) = self.relationships.get(idx) {
1004                    result.push(rel.to_entity.clone());
1005                }
1006            }
1007        }
1008        if let Some(idxs) = self.relations_to.get(entity_id) {
1009            for &idx in idxs {
1010                if let Some(rel) = self.relationships.get(idx) {
1011                    result.push(rel.from_entity.clone());
1012                }
1013            }
1014        }
1015        result
1016    }
1017
1018    /// Rebuild the from/to relationship index maps from scratch.
1019    fn rebuild_relation_indexes(&mut self) {
1020        self.relations_from.clear();
1021        self.relations_to.clear();
1022        for (idx, rel) in self.relationships.iter().enumerate() {
1023            self.relations_from
1024                .entry(rel.from_entity.clone())
1025                .or_default()
1026                .push(idx);
1027            self.relations_to
1028                .entry(rel.to_entity.clone())
1029                .or_default()
1030                .push(idx);
1031        }
1032    }
1033}
1034
1035// ===========================================================================
1036// Tests
1037// ===========================================================================
1038
1039#[cfg(test)]
1040#[allow(clippy::unwrap_used, clippy::expect_used)]
1041mod tests {
1042    use super::*;
1043
1044    // Helpers ----------------------------------------------------------------
1045
1046    fn make_entity(name: &str, etype: EntityType) -> Entity {
1047        Entity {
1048            id: String::new(),
1049            name: name.to_string(),
1050            entity_type: etype,
1051            properties: HashMap::new(),
1052            created_at: Utc::now(),
1053            updated_at: Utc::now(),
1054            confidence: 1.0,
1055            source: "test".to_string(),
1056        }
1057    }
1058
1059    fn make_entity_with_props(
1060        name: &str,
1061        etype: EntityType,
1062        props: HashMap<String, serde_json::Value>,
1063    ) -> Entity {
1064        Entity {
1065            id: String::new(),
1066            name: name.to_string(),
1067            entity_type: etype,
1068            properties: props,
1069            created_at: Utc::now(),
1070            updated_at: Utc::now(),
1071            confidence: 1.0,
1072            source: "test".to_string(),
1073        }
1074    }
1075
1076    fn make_rel(from: &str, to: &str, rtype: RelationType) -> Relationship {
1077        Relationship {
1078            id: String::new(),
1079            from_entity: from.to_string(),
1080            to_entity: to.to_string(),
1081            relation_type: rtype,
1082            properties: HashMap::new(),
1083            weight: 1.0,
1084            created_at: Utc::now(),
1085            source: "test".to_string(),
1086        }
1087    }
1088
1089    // -----------------------------------------------------------------------
1090    // Entity CRUD tests
1091    // -----------------------------------------------------------------------
1092
1093    #[test]
1094    fn test_add_entity_generates_id() {
1095        let mut graph = KnowledgeGraph::new();
1096        let id = graph.add_entity(make_entity("Alice", EntityType::Person));
1097        assert!(!id.is_empty());
1098        assert_eq!(graph.entity_count(), 1);
1099    }
1100
1101    #[test]
1102    fn test_add_entity_custom_id() {
1103        let mut graph = KnowledgeGraph::new();
1104        let mut e = make_entity("Bob", EntityType::Person);
1105        e.id = "custom-id".to_string();
1106        let id = graph.add_entity(e);
1107        assert_eq!(id, "custom-id");
1108    }
1109
1110    #[test]
1111    fn test_get_entity() {
1112        let mut graph = KnowledgeGraph::new();
1113        let id = graph.add_entity(make_entity("Alice", EntityType::Person));
1114        let entity = graph.get_entity(&id).unwrap();
1115        assert_eq!(entity.name, "Alice");
1116    }
1117
1118    #[test]
1119    fn test_get_entity_not_found() {
1120        let graph = KnowledgeGraph::new();
1121        assert!(graph.get_entity("nonexistent").is_none());
1122    }
1123
1124    #[test]
1125    fn test_find_entities_by_name() {
1126        let mut graph = KnowledgeGraph::new();
1127        graph.add_entity(make_entity("Alice Smith", EntityType::Person));
1128        graph.add_entity(make_entity("Bob Jones", EntityType::Person));
1129        graph.add_entity(make_entity("Alice Cooper", EntityType::Person));
1130
1131        let results = graph.find_entities("alice");
1132        assert_eq!(results.len(), 2);
1133    }
1134
1135    #[test]
1136    fn test_find_entities_case_insensitive() {
1137        let mut graph = KnowledgeGraph::new();
1138        graph.add_entity(make_entity("Rust Language", EntityType::Concept));
1139
1140        let results = graph.find_entities("RUST");
1141        assert_eq!(results.len(), 1);
1142        assert_eq!(results[0].name, "Rust Language");
1143    }
1144
1145    #[test]
1146    fn test_find_entities_no_match() {
1147        let mut graph = KnowledgeGraph::new();
1148        graph.add_entity(make_entity("Alice", EntityType::Person));
1149
1150        let results = graph.find_entities("zzz");
1151        assert!(results.is_empty());
1152    }
1153
1154    #[test]
1155    fn test_find_by_type() {
1156        let mut graph = KnowledgeGraph::new();
1157        graph.add_entity(make_entity("Alice", EntityType::Person));
1158        graph.add_entity(make_entity("Rust", EntityType::Concept));
1159        graph.add_entity(make_entity("Bob", EntityType::Person));
1160
1161        let people = graph.find_by_type(&EntityType::Person);
1162        assert_eq!(people.len(), 2);
1163
1164        let concepts = graph.find_by_type(&EntityType::Concept);
1165        assert_eq!(concepts.len(), 1);
1166
1167        let tools = graph.find_by_type(&EntityType::Tool);
1168        assert!(tools.is_empty());
1169    }
1170
1171    #[test]
1172    fn test_update_entity() {
1173        let mut graph = KnowledgeGraph::new();
1174        let id = graph.add_entity(make_entity("Alice", EntityType::Person));
1175
1176        let mut props = HashMap::new();
1177        props.insert("role".to_string(), serde_json::json!("engineer"));
1178        assert!(graph.update_entity(&id, props));
1179
1180        let updated = graph.get_entity(&id).unwrap();
1181        assert_eq!(
1182            updated.properties.get("role").unwrap(),
1183            &serde_json::json!("engineer")
1184        );
1185    }
1186
1187    #[test]
1188    fn test_update_entity_not_found() {
1189        let mut graph = KnowledgeGraph::new();
1190        assert!(!graph.update_entity("nonexistent", HashMap::new()));
1191    }
1192
1193    #[test]
1194    fn test_update_entity_preserves_existing_props() {
1195        let mut graph = KnowledgeGraph::new();
1196        let props = HashMap::from([("email".to_string(), serde_json::json!("a@b.com"))]);
1197        let id = graph.add_entity(make_entity_with_props("Alice", EntityType::Person, props));
1198
1199        let new_props = HashMap::from([("role".to_string(), serde_json::json!("lead"))]);
1200        graph.update_entity(&id, new_props);
1201
1202        let e = graph.get_entity(&id).unwrap();
1203        assert!(e.properties.contains_key("email"));
1204        assert!(e.properties.contains_key("role"));
1205    }
1206
1207    #[test]
1208    fn test_remove_entity() {
1209        let mut graph = KnowledgeGraph::new();
1210        let id = graph.add_entity(make_entity("Alice", EntityType::Person));
1211        assert!(graph.remove_entity(&id));
1212        assert_eq!(graph.entity_count(), 0);
1213        assert!(graph.get_entity(&id).is_none());
1214    }
1215
1216    #[test]
1217    fn test_remove_entity_not_found() {
1218        let mut graph = KnowledgeGraph::new();
1219        assert!(!graph.remove_entity("nonexistent"));
1220    }
1221
1222    #[test]
1223    fn test_remove_entity_cascades_relationships() {
1224        let mut graph = KnowledgeGraph::new();
1225        let alice = graph.add_entity(make_entity("Alice", EntityType::Person));
1226        let bob = graph.add_entity(make_entity("Bob", EntityType::Person));
1227        graph.add_relationship(make_rel(&alice, &bob, RelationType::WorksWith));
1228        assert_eq!(graph.relationship_count(), 1);
1229
1230        graph.remove_entity(&alice);
1231        assert_eq!(graph.relationship_count(), 0);
1232        assert_eq!(graph.entity_count(), 1);
1233    }
1234
1235    // -----------------------------------------------------------------------
1236    // Relationship CRUD tests
1237    // -----------------------------------------------------------------------
1238
1239    #[test]
1240    fn test_add_relationship() {
1241        let mut graph = KnowledgeGraph::new();
1242        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1243        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1244        let rel_id = graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1245        assert!(!rel_id.is_empty());
1246        assert_eq!(graph.relationship_count(), 1);
1247    }
1248
1249    #[test]
1250    fn test_get_relationships_from() {
1251        let mut graph = KnowledgeGraph::new();
1252        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1253        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1254        let c = graph.add_entity(make_entity("C", EntityType::Concept));
1255        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1256        graph.add_relationship(make_rel(&a, &c, RelationType::DependsOn));
1257
1258        let from_a = graph.get_relationships_from(&a);
1259        assert_eq!(from_a.len(), 2);
1260    }
1261
1262    #[test]
1263    fn test_get_relationships_to() {
1264        let mut graph = KnowledgeGraph::new();
1265        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1266        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1267        let c = graph.add_entity(make_entity("C", EntityType::Concept));
1268        graph.add_relationship(make_rel(&a, &c, RelationType::RelatedTo));
1269        graph.add_relationship(make_rel(&b, &c, RelationType::DependsOn));
1270
1271        let to_c = graph.get_relationships_to(&c);
1272        assert_eq!(to_c.len(), 2);
1273    }
1274
1275    #[test]
1276    fn test_find_relationships_by_from() {
1277        let mut graph = KnowledgeGraph::new();
1278        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1279        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1280        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1281        graph.add_relationship(make_rel(&b, &a, RelationType::DependsOn));
1282
1283        let rels = graph.find_relationships(Some(&a), None, None);
1284        assert_eq!(rels.len(), 1);
1285        assert_eq!(rels[0].from_entity, a);
1286    }
1287
1288    #[test]
1289    fn test_find_relationships_by_type() {
1290        let mut graph = KnowledgeGraph::new();
1291        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1292        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1293        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1294        graph.add_relationship(make_rel(&a, &b, RelationType::DependsOn));
1295
1296        let rels = graph.find_relationships(None, None, Some(&RelationType::DependsOn));
1297        assert_eq!(rels.len(), 1);
1298    }
1299
1300    #[test]
1301    fn test_find_relationships_combined_filters() {
1302        let mut graph = KnowledgeGraph::new();
1303        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1304        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1305        let c = graph.add_entity(make_entity("C", EntityType::Concept));
1306        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1307        graph.add_relationship(make_rel(&a, &c, RelationType::RelatedTo));
1308        graph.add_relationship(make_rel(&a, &b, RelationType::DependsOn));
1309
1310        let rels = graph.find_relationships(Some(&a), Some(&b), Some(&RelationType::RelatedTo));
1311        assert_eq!(rels.len(), 1);
1312    }
1313
1314    #[test]
1315    fn test_remove_relationship() {
1316        let mut graph = KnowledgeGraph::new();
1317        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1318        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1319        let rel_id = graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1320        assert!(graph.remove_relationship(&rel_id));
1321        assert_eq!(graph.relationship_count(), 0);
1322    }
1323
1324    #[test]
1325    fn test_remove_relationship_not_found() {
1326        let mut graph = KnowledgeGraph::new();
1327        assert!(!graph.remove_relationship("nonexistent"));
1328    }
1329
1330    // -----------------------------------------------------------------------
1331    // Graph query tests
1332    // -----------------------------------------------------------------------
1333
1334    #[test]
1335    fn test_neighbors_depth_1() {
1336        let mut graph = KnowledgeGraph::new();
1337        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1338        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1339        let c = graph.add_entity(make_entity("C", EntityType::Concept));
1340        let d = graph.add_entity(make_entity("D", EntityType::Concept));
1341        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1342        graph.add_relationship(make_rel(&a, &c, RelationType::RelatedTo));
1343        graph.add_relationship(make_rel(&b, &d, RelationType::RelatedTo));
1344
1345        let neighbors = graph.neighbors(&a, 1);
1346        assert_eq!(neighbors.len(), 2); // B and C only
1347        let names: HashSet<&str> = neighbors.iter().map(|e| e.name.as_str()).collect();
1348        assert!(names.contains("B"));
1349        assert!(names.contains("C"));
1350        assert!(!names.contains("D")); // depth 2
1351    }
1352
1353    #[test]
1354    fn test_neighbors_depth_2() {
1355        let mut graph = KnowledgeGraph::new();
1356        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1357        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1358        let c = graph.add_entity(make_entity("C", EntityType::Concept));
1359        let d = graph.add_entity(make_entity("D", EntityType::Concept));
1360        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1361        graph.add_relationship(make_rel(&b, &c, RelationType::RelatedTo));
1362        graph.add_relationship(make_rel(&c, &d, RelationType::RelatedTo));
1363
1364        let neighbors = graph.neighbors(&a, 2);
1365        assert_eq!(neighbors.len(), 2); // B (depth 1) and C (depth 2)
1366        let names: HashSet<&str> = neighbors.iter().map(|e| e.name.as_str()).collect();
1367        assert!(names.contains("B"));
1368        assert!(names.contains("C"));
1369        assert!(!names.contains("D")); // depth 3
1370    }
1371
1372    #[test]
1373    fn test_neighbors_nonexistent_entity() {
1374        let graph = KnowledgeGraph::new();
1375        let neighbors = graph.neighbors("nope", 1);
1376        assert!(neighbors.is_empty());
1377    }
1378
1379    #[test]
1380    fn test_neighbors_zero_depth() {
1381        let mut graph = KnowledgeGraph::new();
1382        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1383        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1384        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1385
1386        let neighbors = graph.neighbors(&a, 0);
1387        assert!(neighbors.is_empty());
1388    }
1389
1390    #[test]
1391    fn test_neighbors_undirected() {
1392        let mut graph = KnowledgeGraph::new();
1393        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1394        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1395        // Only B->A edge, but neighbors should still find B from A
1396        graph.add_relationship(make_rel(&b, &a, RelationType::RelatedTo));
1397
1398        let neighbors = graph.neighbors(&a, 1);
1399        assert_eq!(neighbors.len(), 1);
1400        assert_eq!(neighbors[0].name, "B");
1401    }
1402
1403    #[test]
1404    fn test_shortest_path_direct() {
1405        let mut graph = KnowledgeGraph::new();
1406        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1407        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1408        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1409
1410        let path = graph.shortest_path(&a, &b).unwrap();
1411        assert_eq!(path.len(), 2);
1412        assert_eq!(path[0], a);
1413        assert_eq!(path[1], b);
1414    }
1415
1416    #[test]
1417    fn test_shortest_path_multi_hop() {
1418        let mut graph = KnowledgeGraph::new();
1419        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1420        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1421        let c = graph.add_entity(make_entity("C", EntityType::Concept));
1422        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1423        graph.add_relationship(make_rel(&b, &c, RelationType::RelatedTo));
1424
1425        let path = graph.shortest_path(&a, &c).unwrap();
1426        assert_eq!(path.len(), 3);
1427        assert_eq!(path, vec![a, b, c]);
1428    }
1429
1430    #[test]
1431    fn test_shortest_path_same_node() {
1432        let mut graph = KnowledgeGraph::new();
1433        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1434
1435        let path = graph.shortest_path(&a, &a).unwrap();
1436        assert_eq!(path.len(), 1);
1437    }
1438
1439    #[test]
1440    fn test_shortest_path_no_path() {
1441        let mut graph = KnowledgeGraph::new();
1442        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1443        let _b = graph.add_entity(make_entity("B", EntityType::Concept));
1444        // No edge between A and B
1445        assert!(graph.shortest_path(&a, &_b).is_none());
1446    }
1447
1448    #[test]
1449    fn test_shortest_path_nonexistent() {
1450        let graph = KnowledgeGraph::new();
1451        assert!(graph.shortest_path("x", "y").is_none());
1452    }
1453
1454    #[test]
1455    fn test_connected_component() {
1456        let mut graph = KnowledgeGraph::new();
1457        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1458        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1459        let c = graph.add_entity(make_entity("C", EntityType::Concept));
1460        let d = graph.add_entity(make_entity("D", EntityType::Concept)); // isolated
1461        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1462        graph.add_relationship(make_rel(&b, &c, RelationType::RelatedTo));
1463
1464        let comp = graph.connected_component(&a);
1465        assert_eq!(comp.len(), 3); // A, B, C
1466        let names: HashSet<&str> = comp.iter().map(|e| e.name.as_str()).collect();
1467        assert!(names.contains("A"));
1468        assert!(names.contains("B"));
1469        assert!(names.contains("C"));
1470        assert!(!names.contains("D"));
1471
1472        let comp_d = graph.connected_component(&d);
1473        assert_eq!(comp_d.len(), 1); // just D itself
1474    }
1475
1476    #[test]
1477    fn test_connected_component_nonexistent() {
1478        let graph = KnowledgeGraph::new();
1479        assert!(graph.connected_component("nope").is_empty());
1480    }
1481
1482    #[test]
1483    fn test_entity_count_and_relationship_count() {
1484        let mut graph = KnowledgeGraph::new();
1485        assert_eq!(graph.entity_count(), 0);
1486        assert_eq!(graph.relationship_count(), 0);
1487
1488        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1489        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1490        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1491
1492        assert_eq!(graph.entity_count(), 2);
1493        assert_eq!(graph.relationship_count(), 1);
1494    }
1495
1496    // -----------------------------------------------------------------------
1497    // Entity extraction tests
1498    // -----------------------------------------------------------------------
1499
1500    #[test]
1501    fn test_extract_emails() {
1502        let mut graph = KnowledgeGraph::new();
1503        let ids = graph.extract_entities_from_text("Contact alice@example.com for details", "test");
1504        assert!(!ids.is_empty());
1505        let entity = graph.get_entity(&ids[0]).unwrap();
1506        assert_eq!(entity.entity_type, EntityType::Person);
1507        assert_eq!(
1508            entity.properties.get("email").unwrap(),
1509            &serde_json::json!("alice@example.com")
1510        );
1511    }
1512
1513    #[test]
1514    fn test_extract_urls() {
1515        let mut graph = KnowledgeGraph::new();
1516        let ids = graph.extract_entities_from_text(
1517            "Check https://github.com/fboiero/Argentor for source",
1518            "test",
1519        );
1520        let url_entities: Vec<&Entity> = ids
1521            .iter()
1522            .filter_map(|id| graph.get_entity(id))
1523            .filter(|e| e.entity_type == EntityType::Custom("Url".to_string()))
1524            .collect();
1525        assert_eq!(url_entities.len(), 1);
1526    }
1527
1528    #[test]
1529    fn test_extract_mentions() {
1530        let mut graph = KnowledgeGraph::new();
1531        let ids = graph.extract_entities_from_text("Thanks @johndoe and @janedoe", "test");
1532        let mention_entities: Vec<&Entity> = ids
1533            .iter()
1534            .filter_map(|id| graph.get_entity(id))
1535            .filter(|e| e.properties.contains_key("mention"))
1536            .collect();
1537        assert_eq!(mention_entities.len(), 2);
1538    }
1539
1540    #[test]
1541    fn test_extract_hashtags() {
1542        let mut graph = KnowledgeGraph::new();
1543        let ids = graph.extract_entities_from_text("Discussing #rust and #wasm today", "test");
1544        let tag_entities: Vec<&Entity> = ids
1545            .iter()
1546            .filter_map(|id| graph.get_entity(id))
1547            .filter(|e| e.entity_type == EntityType::Concept)
1548            .collect();
1549        assert_eq!(tag_entities.len(), 2);
1550    }
1551
1552    #[test]
1553    fn test_extract_file_paths() {
1554        let mut graph = KnowledgeGraph::new();
1555        let ids =
1556            graph.extract_entities_from_text("Edit the file /src/main.rs to fix the bug", "test");
1557        let file_entities: Vec<&Entity> = ids
1558            .iter()
1559            .filter_map(|id| graph.get_entity(id))
1560            .filter(|e| e.entity_type == EntityType::File)
1561            .collect();
1562        assert_eq!(file_entities.len(), 1);
1563        assert_eq!(file_entities[0].name, "main.rs");
1564    }
1565
1566    #[test]
1567    fn test_extract_capitalized_phrases() {
1568        let mut graph = KnowledgeGraph::new();
1569        let ids = graph.extract_entities_from_text(
1570            "I met Alice Cooper at the event and saw Bob Dylan too",
1571            "test",
1572        );
1573        let person_entities: Vec<&Entity> = ids
1574            .iter()
1575            .filter_map(|id| graph.get_entity(id))
1576            .filter(|e| e.entity_type == EntityType::Person && e.confidence == 0.5)
1577            .collect();
1578        assert!(person_entities.len() >= 2);
1579        let names: HashSet<&str> = person_entities.iter().map(|e| e.name.as_str()).collect();
1580        assert!(names.contains("Alice Cooper"));
1581        assert!(names.contains("Bob Dylan"));
1582    }
1583
1584    #[test]
1585    fn test_extract_no_duplicates() {
1586        let mut graph = KnowledgeGraph::new();
1587        let ids = graph.extract_entities_from_text(
1588            "Contact alice@example.com and alice@example.com again",
1589            "test",
1590        );
1591        let email_entities: Vec<&Entity> = ids
1592            .iter()
1593            .filter_map(|id| graph.get_entity(id))
1594            .filter(|e| e.properties.contains_key("email"))
1595            .collect();
1596        assert_eq!(email_entities.len(), 1);
1597    }
1598
1599    #[test]
1600    fn test_extract_empty_text() {
1601        let mut graph = KnowledgeGraph::new();
1602        let ids = graph.extract_entities_from_text("", "test");
1603        assert!(ids.is_empty());
1604    }
1605
1606    // -----------------------------------------------------------------------
1607    // Entity merge tests
1608    // -----------------------------------------------------------------------
1609
1610    #[test]
1611    fn test_merge_entity_basic() {
1612        let mut graph = KnowledgeGraph::new();
1613        let props_a = HashMap::from([("email".to_string(), serde_json::json!("a@x.com"))]);
1614        let props_b = HashMap::from([("role".to_string(), serde_json::json!("dev"))]);
1615        let a = graph.add_entity(make_entity_with_props("Alice", EntityType::Person, props_a));
1616        let b = graph.add_entity(make_entity_with_props(
1617            "Alice Dup",
1618            EntityType::Person,
1619            props_b,
1620        ));
1621
1622        graph.merge_entity(&b, &a).unwrap();
1623        assert_eq!(graph.entity_count(), 1);
1624
1625        let merged = graph.get_entity(&a).unwrap();
1626        assert!(merged.properties.contains_key("email"));
1627        assert!(merged.properties.contains_key("role"));
1628    }
1629
1630    #[test]
1631    fn test_merge_entity_redirects_relationships() {
1632        let mut graph = KnowledgeGraph::new();
1633        let a = graph.add_entity(make_entity("Alice", EntityType::Person));
1634        let dup = graph.add_entity(make_entity("Alice Dup", EntityType::Person));
1635        let bob = graph.add_entity(make_entity("Bob", EntityType::Person));
1636        graph.add_relationship(make_rel(&dup, &bob, RelationType::WorksWith));
1637
1638        graph.merge_entity(&dup, &a).unwrap();
1639
1640        let rels = graph.get_relationships_from(&a);
1641        assert_eq!(rels.len(), 1);
1642        assert_eq!(rels[0].to_entity, bob);
1643    }
1644
1645    #[test]
1646    fn test_merge_entity_self_error() {
1647        let mut graph = KnowledgeGraph::new();
1648        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1649        assert!(graph.merge_entity(&a, &a).is_err());
1650    }
1651
1652    #[test]
1653    fn test_merge_entity_not_found() {
1654        let mut graph = KnowledgeGraph::new();
1655        let a = graph.add_entity(make_entity("A", EntityType::Concept));
1656        assert!(graph.merge_entity("nonexistent", &a).is_err());
1657        assert!(graph.merge_entity(&a, "nonexistent").is_err());
1658    }
1659
1660    #[test]
1661    fn test_merge_entity_no_overwrite_existing() {
1662        let mut graph = KnowledgeGraph::new();
1663        let props_a = HashMap::from([("email".to_string(), serde_json::json!("target@x.com"))]);
1664        let props_b = HashMap::from([("email".to_string(), serde_json::json!("source@x.com"))]);
1665        let a = graph.add_entity(make_entity_with_props("A", EntityType::Person, props_a));
1666        let b = graph.add_entity(make_entity_with_props("B", EntityType::Person, props_b));
1667
1668        graph.merge_entity(&b, &a).unwrap();
1669        let merged = graph.get_entity(&a).unwrap();
1670        // Target's email should be preserved (not overwritten)
1671        assert_eq!(
1672            merged.properties.get("email").unwrap(),
1673            &serde_json::json!("target@x.com")
1674        );
1675    }
1676
1677    // -----------------------------------------------------------------------
1678    // Context string tests
1679    // -----------------------------------------------------------------------
1680
1681    #[test]
1682    fn test_to_context_string_basic() {
1683        let mut graph = KnowledgeGraph::new();
1684        let props = HashMap::from([
1685            ("email".to_string(), serde_json::json!("alice@example.com")),
1686            ("role".to_string(), serde_json::json!("engineer")),
1687        ]);
1688        let alice = graph.add_entity(make_entity_with_props("Alice", EntityType::Person, props));
1689        let bob = graph.add_entity(make_entity("Bob", EntityType::Person));
1690        let rust = graph.add_entity(make_entity("Rust", EntityType::Concept));
1691        graph.add_relationship(make_rel(&alice, &bob, RelationType::WorksWith));
1692        graph.add_relationship(make_rel(&alice, &rust, RelationType::Mentions));
1693
1694        let ctx = graph.to_context_string(&alice, 1);
1695        assert!(ctx.contains("Entity: Alice (Person)"));
1696        assert!(ctx.contains("Properties:"));
1697        assert!(ctx.contains("email=alice@example.com"));
1698        assert!(ctx.contains("Relationships:"));
1699        assert!(ctx.contains("WorksWith"));
1700        assert!(ctx.contains("Bob"));
1701        assert!(ctx.contains("Mentions"));
1702        assert!(ctx.contains("Rust"));
1703    }
1704
1705    #[test]
1706    fn test_to_context_string_not_found() {
1707        let graph = KnowledgeGraph::new();
1708        let ctx = graph.to_context_string("nonexistent", 1);
1709        assert!(ctx.contains("not found"));
1710    }
1711
1712    #[test]
1713    fn test_to_context_string_depth_2() {
1714        let mut graph = KnowledgeGraph::new();
1715        let alice = graph.add_entity(make_entity("Alice", EntityType::Person));
1716        let bob = graph.add_entity(make_entity("Bob", EntityType::Person));
1717        let charlie = graph.add_entity(make_entity("Charlie", EntityType::Person));
1718        graph.add_relationship(make_rel(&alice, &bob, RelationType::WorksWith));
1719        graph.add_relationship(make_rel(&bob, &charlie, RelationType::WorksWith));
1720
1721        let ctx = graph.to_context_string(&alice, 2);
1722        assert!(ctx.contains("Neighbors (depth 2)"));
1723        assert!(ctx.contains("Charlie"));
1724    }
1725
1726    // -----------------------------------------------------------------------
1727    // Summary tests
1728    // -----------------------------------------------------------------------
1729
1730    #[test]
1731    fn test_summarize_empty() {
1732        let graph = KnowledgeGraph::new();
1733        let summary = graph.summarize();
1734        assert_eq!(summary.entity_count, 0);
1735        assert_eq!(summary.relationship_count, 0);
1736        assert!(summary.most_connected.is_empty());
1737    }
1738
1739    #[test]
1740    fn test_summarize_populated() {
1741        let mut graph = KnowledgeGraph::new();
1742        let a = graph.add_entity(make_entity("A", EntityType::Person));
1743        let b = graph.add_entity(make_entity("B", EntityType::Concept));
1744        let c = graph.add_entity(make_entity("C", EntityType::Person));
1745        graph.add_relationship(make_rel(&a, &b, RelationType::RelatedTo));
1746        graph.add_relationship(make_rel(&a, &c, RelationType::WorksWith));
1747        graph.add_relationship(make_rel(&b, &c, RelationType::Mentions));
1748
1749        let summary = graph.summarize();
1750        assert_eq!(summary.entity_count, 3);
1751        assert_eq!(summary.relationship_count, 3);
1752        assert_eq!(summary.entity_types.get("Person"), Some(&2));
1753        assert_eq!(summary.entity_types.get("Concept"), Some(&1));
1754        assert_eq!(summary.relationship_types.get("RelatedTo"), Some(&1));
1755        assert_eq!(summary.relationship_types.get("WorksWith"), Some(&1));
1756
1757        // A has 2 outgoing rels, B has 1 outgoing + 1 incoming = 2, C has 2 incoming
1758        // All have count 2, so all three should be in most_connected
1759        assert!(!summary.most_connected.is_empty());
1760    }
1761
1762    // -----------------------------------------------------------------------
1763    // Persistence tests
1764    // -----------------------------------------------------------------------
1765
1766    #[test]
1767    fn test_save_and_load_roundtrip() {
1768        let tmp = tempfile::tempdir().unwrap();
1769        let path = tmp.path().join("graph.json");
1770
1771        let mut graph = KnowledgeGraph::new();
1772        let a = graph.add_entity(make_entity("Alice", EntityType::Person));
1773        let b = graph.add_entity(make_entity("Bob", EntityType::Person));
1774        let rel_id = graph.add_relationship(make_rel(&a, &b, RelationType::WorksWith));
1775
1776        graph.save(&path).unwrap();
1777
1778        let loaded = KnowledgeGraph::load(&path).unwrap();
1779        assert_eq!(loaded.entity_count(), 2);
1780        assert_eq!(loaded.relationship_count(), 1);
1781
1782        let loaded_alice = loaded.find_entities("alice");
1783        assert_eq!(loaded_alice.len(), 1);
1784        assert_eq!(loaded_alice[0].name, "Alice");
1785
1786        let loaded_rels = loaded.find_relationships(Some(&a), Some(&b), None);
1787        assert_eq!(loaded_rels.len(), 1);
1788        assert_eq!(loaded_rels[0].id, rel_id);
1789    }
1790
1791    #[test]
1792    fn test_load_nonexistent_file() {
1793        let result = KnowledgeGraph::load(Path::new("/tmp/does_not_exist_kg.json"));
1794        assert!(result.is_err());
1795    }
1796
1797    #[test]
1798    fn test_save_creates_parent_dirs() {
1799        let tmp = tempfile::tempdir().unwrap();
1800        let path = tmp.path().join("nested").join("dir").join("graph.json");
1801
1802        let graph = KnowledgeGraph::new();
1803        graph.save(&path).unwrap();
1804        assert!(path.exists());
1805    }
1806
1807    // -----------------------------------------------------------------------
1808    // Default trait
1809    // -----------------------------------------------------------------------
1810
1811    #[test]
1812    fn test_default_graph() {
1813        let graph = KnowledgeGraph::default();
1814        assert_eq!(graph.entity_count(), 0);
1815        assert_eq!(graph.relationship_count(), 0);
1816    }
1817
1818    // -----------------------------------------------------------------------
1819    // Display trait tests
1820    // -----------------------------------------------------------------------
1821
1822    #[test]
1823    fn test_entity_type_display() {
1824        assert_eq!(EntityType::Person.to_string(), "Person");
1825        assert_eq!(EntityType::Custom("X".to_string()).to_string(), "Custom(X)");
1826    }
1827
1828    #[test]
1829    fn test_relation_type_display() {
1830        assert_eq!(RelationType::IsA.to_string(), "IsA");
1831        assert_eq!(
1832            RelationType::Custom("Likes".to_string()).to_string(),
1833            "Custom(Likes)"
1834        );
1835    }
1836}