1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Entity {
29 pub id: String,
31 pub name: String,
33 pub entity_type: EntityType,
35 pub properties: HashMap<String, serde_json::Value>,
37 pub created_at: DateTime<Utc>,
39 pub updated_at: DateTime<Utc>,
41 pub confidence: f64,
43 pub source: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
49pub enum EntityType {
50 Person,
52 Organization,
54 Concept,
56 Tool,
58 File,
60 Location,
62 Event,
64 Fact,
66 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#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct Relationship {
93 pub id: String,
95 pub from_entity: String,
97 pub to_entity: String,
99 pub relation_type: RelationType,
101 pub properties: HashMap<String, serde_json::Value>,
103 pub weight: f64,
105 pub created_at: DateTime<Utc>,
107 pub source: String,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
113pub enum RelationType {
114 IsA,
116 HasProperty,
118 RelatedTo,
120 DependsOn,
122 CreatedBy,
124 Contains,
126 WorksWith,
128 Mentions,
130 UsedTool,
132 ProducedOutput,
134 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#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct GraphSummary {
163 pub entity_count: usize,
165 pub relationship_count: usize,
167 pub entity_types: HashMap<String, usize>,
169 pub relationship_types: HashMap<String, usize>,
171 pub most_connected: Vec<(String, usize)>,
173}
174
175#[derive(Serialize, Deserialize)]
180struct SerializableGraph {
181 entities: Vec<Entity>,
182 relationships: Vec<Relationship>,
183}
184
185pub struct KnowledgeGraph {
191 entities: HashMap<String, Entity>,
192 relationships: Vec<Relationship>,
193 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 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 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 self.entity_by_name
235 .entry(name_lower)
236 .or_default()
237 .push(id.clone());
238
239 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 pub fn get_entity(&self, id: &str) -> Option<&Entity> {
251 self.entities.get(id)
252 }
253
254 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 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 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 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 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 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 self.relationships
316 .retain(|r| r.from_entity != id && r.to_entity != id);
317 self.rebuild_relation_indexes();
318
319 true
320 }
321
322 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 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 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 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 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 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 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 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 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 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 if let Some(e) = self.entities.get(entity_id) {
520 result.push(e);
521 }
522
523 while let Some(current) = queue.pop_front() {
524 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 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 pub fn entity_count(&self) -> usize {
560 self.entities.len()
561 }
562
563 pub fn relationship_count(&self) -> usize {
565 self.relationships.len()
566 }
567
568 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 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 let mut seen: HashSet<String> = HashSet::new();
610
611 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 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 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 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 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 for cap in cap_phrase_re.captures_iter(text) {
722 let phrase = cap[1].to_string();
723 if seen.insert(phrase.clone()) {
724 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 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 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 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 self.relationships
791 .retain(|r| !(r.from_entity == r.to_entity && r.from_entity == target_id));
792
793 self.entities.remove(source_id);
796
797 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 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 self.rebuild_relation_indexes();
816
817 Ok(())
818 }
819
820 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 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 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 out.push_str(&format!(
877 "Entity: {} ({})\n",
878 entity.name, entity.entity_type
879 ));
880
881 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 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 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; }
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 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 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 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 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#[cfg(test)]
1040#[allow(clippy::unwrap_used, clippy::expect_used)]
1041mod tests {
1042 use super::*;
1043
1044 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 #[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 #[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 #[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); 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")); }
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); 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")); }
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 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 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)); 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); 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); }
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 #[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 #[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 assert_eq!(
1672 merged.properties.get("email").unwrap(),
1673 &serde_json::json!("target@x.com")
1674 );
1675 }
1676
1677 #[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 #[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 assert!(!summary.most_connected.is_empty());
1760 }
1761
1762 #[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 #[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 #[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}