//! Knowledge Graph //! //! Represents entities, relationships, and inferred facts. //! The substrate for all reasoning — without this, reasoning has nothing to work with. use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet, VecDeque}; /// A knowledge graph — entities, relationships, and facts. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct KnowledgeGraph { /// Entities by name entities: HashMap, /// Relationships between entities relationships: Vec, /// Named concepts (abstract entities) concepts: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Entity { pub name: String, pub properties: HashMap, pub description: Option, pub confidence: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Concept { pub name: String, pub definition: String, pub examples: Vec, pub related_concepts: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Relationship { pub from: String, pub relation: RelationType, pub to: String, pub confidence: f64, pub source: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum RelationType { IsA, HasProperty, Causes, Enables, Prevents, PartOf, Uses, SimilarTo, OppositeOf, RelatedTo, InstanceOf, CausedBy, EnabledBy, UsedBy, HasPart, } impl RelationType { pub fn as_str(&self) -> &'static str { match self { Self::IsA => "is a", Self::HasProperty => "has property", Self::Causes => "causes", Self::Enables => "enables", Self::Prevents => "prevents", Self::PartOf => "part of", Self::Uses => "uses", Self::SimilarTo => "similar to", Self::OppositeOf => "opposite of", Self::RelatedTo => "related to", Self::InstanceOf => "instance of", Self::CausedBy => "caused by", Self::EnabledBy => "enabled by", Self::UsedBy => "used by", Self::HasPart => "has part", } } pub fn inverse(&self) -> Option { match self { Self::IsA => Some(Self::InstanceOf), Self::InstanceOf => Some(Self::IsA), Self::HasProperty => None, Self::Causes => Some(Self::CausedBy), Self::CausedBy => Some(Self::Causes), Self::Enables => Some(Self::EnabledBy), Self::EnabledBy => Some(Self::Enables), Self::Prevents => None, Self::PartOf => Some(Self::HasPart), Self::HasPart => Some(Self::PartOf), Self::Uses => Some(Self::UsedBy), Self::UsedBy => Some(Self::Uses), Self::SimilarTo => Some(Self::SimilarTo), Self::OppositeOf => Some(Self::OppositeOf), Self::RelatedTo => Some(Self::RelatedTo), } } } impl KnowledgeGraph { pub fn new() -> Self { Self::default() } /// Ingest a simple factual statement as a relationship. /// Parses "subject verb object" into a relationship between entities. /// /// Examples: /// - ingest_fact("fire", "causes", "heat", 0.9) /// - ingest_fact("star", "is", "reasoning intelligence", 0.95) pub fn ingest_fact(&mut self, subject: &str, verb: &str, object: &str, confidence: f64) { // Normalize let subject = subject.trim().to_lowercase(); let verb = verb.trim().to_lowercase(); let object = object.trim().to_lowercase(); if subject.len() < 2 || object.len() < 2 { return; } if verb.is_empty() { return; } // Map common verbs to relation types (only valid enum variants) let rel_type = match verb.as_str() { "is" | "are" | "'s" | "was" | "be" => RelationType::IsA, "causes" | "cause" | "lead to" | "leads to" => RelationType::Causes, "requires" | "need" | "needs" | "depend on" => RelationType::Causes, // closest match "produces" | "create" | "creates" | "make" | "makes" => RelationType::Causes, "enables" | "allow" | "allows" => RelationType::Enables, "uses" | "use" | "using" => RelationType::Uses, "related to" | "related" | "like" | "similar to" | "similar" => RelationType::SimilarTo, "part of" | "part" => RelationType::PartOf, "has" | "have" | "having" => RelationType::HasProperty, "can" | "able to" => RelationType::Enables, "prevents" | "stop" | "stops" => RelationType::Prevents, _ => RelationType::RelatedTo, }; // Create the relationship let rel = Relationship { from: subject.clone(), to: object.clone(), relation: rel_type, confidence, source: Some(format!("{} {} {}", subject, verb, object)), }; // Add entities self.add_entity(&subject); self.add_entity(&object); // Deduplicate: only add if not already present let key = format!("{}:{}:{}", rel.from, rel.relation.as_str(), rel.to); let exists = self .relationships .iter() .any(|r| format!("{}:{}:{}", r.from, r.relation.as_str(), r.to) == key); if !exists { self.relationships.push(rel); } } /// Extract entities (nouns and noun phrases) from text using simple pattern matching. pub fn extract_entities(&self, text: &str) -> Vec { let mut entities = Vec::new(); // Pattern 1: Capitalized words (proper nouns) let mut prev_was_cap = false; let mut current_phrase = String::new(); for word in text.split_whitespace() { let first_char = word.chars().next().unwrap_or(' '); if first_char.is_uppercase() && first_char.is_alphabetic() { if prev_was_cap && !current_phrase.is_empty() { current_phrase.push(' '); } current_phrase.push_str(word); prev_was_cap = true; } else { if !current_phrase.is_empty() && current_phrase.len() > 1 { entities.push(current_phrase.clone()); } current_phrase.clear(); prev_was_cap = false; } } if !current_phrase.is_empty() && current_phrase.len() > 1 { entities.push(current_phrase); } // Pattern 2: Quoted phrases for (i, ch) in text.char_indices() { if ch == '"' { if let Some(end) = text[i + 1..].find('"') { let phrase = &text[i + 1..i + 1 + end]; if phrase.len() > 2 { entities.push(phrase.to_string()); } } break; } } entities.dedup(); entities } /// Add an entity. pub fn add_entity(&mut self, name: &str) { if name.len() < 2 { return; } let name = name.trim_matches(|c| !char::is_alphanumeric(c)).to_string(); if name.len() < 2 { return; } self.entities.entry(name.clone()).or_insert_with(|| Entity { name: name.clone(), properties: HashMap::new(), description: None, confidence: 0.5, }); } /// Add a relationship between entities. pub fn add_relationship(&mut self, from: &str, relation: RelationType, to: &str) { if from.is_empty() || to.is_empty() { return; } let from = from.trim_matches(|c| !char::is_alphanumeric(c)).to_string(); let to = to.trim_matches(|c| !char::is_alphanumeric(c)).to_string(); if from.is_empty() || to.is_empty() { return; } self.add_entity(&from); self.add_entity(&to); // Avoid duplicate relationships if self .relationships .iter() .any(|r| r.from == from && r.relation == relation && r.to == to) { return; } self.relationships.push(Relationship { from, relation, to, confidence: 0.7, source: None, }); } /// Add a property to an entity. pub fn add_property(&mut self, entity: &str, property: &str, value: &str) { let normalized_entity = entity .trim_matches(|c| !char::is_alphanumeric(c)) .to_string(); self.add_entity(&normalized_entity); if let Some(e) = self.entities.get_mut(&normalized_entity) { e.properties.insert(property.to_string(), value.to_string()); } } /// Add a fact about an entity. pub fn add_fact(&mut self, entity: &str, fact: &str) { let normalized_entity = entity .trim_matches(|c| !char::is_alphanumeric(c)) .to_string(); self.add_entity(&normalized_entity); if let Some(e) = self.entities.get_mut(&normalized_entity) { e.description = Some(fact.to_string()); } } /// Get an entity by name. pub fn get_entity(&self, name: &str) -> Option<&Entity> { // Case-insensitive lookup self.entities .get(name) .or_else(|| self.entities.get(&name.to_lowercase())) .or_else(|| { // Try case-insensitive search through all entities let name_lower = name.to_lowercase(); let mut matches: Vec<_> = self .entities .iter() .filter(|(key, _)| key.to_lowercase() == name_lower) .collect(); matches.sort_by(|(left, _), (right, _)| left.cmp(right)); matches.first().map(|(_, value)| *value) }) } /// Get all entity names (owned Strings). pub fn entities(&self) -> Vec { let mut entities: Vec = self.entities.keys().cloned().collect(); entities.sort(); entities } /// Get all relationships from an entity (case-insensitive, owned). pub fn get_relationships_from(&self, entity: &str) -> Vec { let entity_lower = entity.to_lowercase(); let mut relationships: Vec<_> = self .relationships .iter() .filter(|r| r.from.to_lowercase() == entity_lower) .cloned() .collect(); relationships.sort_by(|left, right| { left.from .cmp(&right.from) .then_with(|| left.relation.as_str().cmp(right.relation.as_str())) .then_with(|| left.to.cmp(&right.to)) }); relationships } /// Get all relationships to an entity (case-insensitive, owned). pub fn get_relationships_to(&self, entity: &str) -> Vec { let entity_lower = entity.to_lowercase(); let mut relationships: Vec<_> = self .relationships .iter() .filter(|r| r.to.to_lowercase() == entity_lower) .cloned() .collect(); relationships.sort_by(|left, right| { left.from .cmp(&right.from) .then_with(|| left.relation.as_str().cmp(right.relation.as_str())) .then_with(|| left.to.cmp(&right.to)) }); relationships } /// Get all facts about an entity. pub fn get_facts_about(&self, entity: &str) -> Vec { let mut facts = Vec::new(); // Use case-insensitive entity lookup if let Some(e) = self.get_entity(entity) { let mut properties: Vec<_> = e.properties.values().collect(); properties.sort(); for val in properties { facts.push(format!("{} has {}", e.name, val)); } if let Some(desc) = &e.description { facts.push(desc.clone()); } } // Relationships for rel in self.get_relationships_from(entity) { facts.push(format!("{} {} {}", entity, rel.relation.as_str(), rel.to)); } for rel in self.get_relationships_to(entity) { if rel.relation == RelationType::IsA || rel.relation == RelationType::InstanceOf { facts.push(format!("{} {} {}", entity, rel.relation.as_str(), rel.to)); } } facts.sort(); facts.dedup(); facts } /// Get facts containing a term. pub fn get_facts_containing(&self, term: &str) -> Vec { let term_lower = term.to_lowercase(); let mut facts = Vec::new(); let mut entities: Vec<&String> = self.entities.keys().collect(); entities.sort(); for entity in entities { let entity_lower = entity.to_lowercase(); if entity_lower.contains(&term_lower) { facts.extend(self.get_facts_about(entity)); } } facts.sort(); facts.dedup(); facts } /// Get causes of an entity (what causes it). pub fn get_causes(&self, entity: &str) -> Vec { let mut causes: Vec<_> = self .relationships .iter() .filter(|r| r.to == entity && r.relation == RelationType::Causes) .map(|r| format!("{} {}", r.from, r.relation.as_str())) .collect(); causes.sort(); causes.dedup(); causes } /// Get effects of an entity (what it causes). pub fn get_effects(&self, entity: &str) -> Vec { let mut effects: Vec<_> = self .relationships .iter() .filter(|r| r.from == entity && r.relation == RelationType::Causes) .map(|r| format!("{} {}", r.relation.as_str(), r.to)) .collect(); effects.sort(); effects.dedup(); effects } /// Get mechanisms related to an entity. pub fn get_mechanisms(&self, entity: &str) -> Vec { let mut mechanisms = Vec::new(); for rel in self.relationships.iter() { if rel.from == entity && rel.relation == RelationType::Uses { mechanisms.push(format!("uses {}", rel.to)); } if rel.to == entity && rel.relation == RelationType::Enables { mechanisms.push(format!("enabled by {}", rel.from)); } } mechanisms.sort(); mechanisms.dedup(); mechanisms } /// Get values-related entities. pub fn get_values_related(&self, topic: &str) -> Vec { let topic_lower = topic.to_lowercase(); let mut related = Vec::new(); // Look for entities that have "good" or "value" in their relationships for rel in &self.relationships { if (rel.from.to_lowercase().contains(&topic_lower) || rel.to.to_lowercase().contains(&topic_lower)) && rel.relation == RelationType::RelatedTo { related.push(format!("{} {} {}", rel.from, rel.relation.as_str(), rel.to)); } } related.sort(); related.dedup(); related } /// Infer new relationships using transitive reasoning. pub fn infer_transitive( &self, from: &str, relation: &RelationType, depth: usize, ) -> Vec { if depth == 0 { return Vec::new(); } let mut results = Vec::new(); let mut visited = HashSet::new(); let mut queue = VecDeque::new(); queue.push_back((from.to_string(), depth)); while let Some((current, remaining)) = queue.pop_front() { if visited.contains(¤t) { continue; } visited.insert(current.clone()); for rel in self.get_relationships_from(¤t) { if &rel.relation == relation { results.push(rel.to.clone()); if remaining > 1 { queue.push_back((rel.to.clone(), remaining - 1)); } } } } results.sort(); results.dedup(); results } /// Find entities connected by a chain of relationships. pub fn find_connection(&self, from: &str, to: &str, max_depth: usize) -> Option> { let mut visited = HashSet::new(); let mut queue = VecDeque::new(); queue.push_back(vec![from.to_string()]); while let Some(path) = queue.pop_front() { let current = path.last().unwrap(); if current == to { return Some(path); } if path.len() >= max_depth { continue; } if visited.contains(current) { continue; } visited.insert(current.clone()); for rel in self.get_relationships_from(current) { let mut new_path = path.clone(); new_path.push(rel.to.clone()); queue.push_back(new_path); } } None } /// Add a concept (abstract idea with definition). pub fn add_concept(&mut self, name: &str, definition: &str) { self.concepts.insert( name.to_string(), Concept { name: name.to_string(), definition: definition.to_string(), examples: Vec::new(), related_concepts: Vec::new(), }, ); } /// Get a concept. pub fn get_concept(&self, name: &str) -> Option<&Concept> { self.concepts.get(name) } /// Number of entities. pub fn entity_count(&self) -> usize { self.entities.len() } /// Number of relationships. pub fn relationship_count(&self) -> usize { self.relationships.len() } /// Find analogies between two concepts by exploring shared relational structure. /// /// This is the core of dynamic analogy-making: instead of hardcoded categories, /// we inspect the *actual* relationships in the knowledge graph and look for /// structural parallels. "A:B :: C:D" when A→X and C→Y are the same *type* /// of relationship, even if X≠Y. /// /// Returns a list of discovered analogies sorted by confidence. pub fn find_analogies(&self, concept_a: &str, concept_b: &str) -> Vec { let mut analogies = Vec::new(); let a_lower = concept_a.to_lowercase(); let b_lower = concept_b.to_lowercase(); // Get all relationships from A and B let rels_from_a: Vec<_> = self .relationships .iter() .filter(|r| r.from.to_lowercase() == a_lower) .collect(); let rels_from_b: Vec<_> = self .relationships .iter() .filter(|r| r.from.to_lowercase() == b_lower) .collect(); for rel_a in &rels_from_a { for rel_b in &rels_from_b { // Same relation type → structural parallel if rel_a.relation == rel_b.relation { let target_same = rel_a.to.to_lowercase() == rel_b.to.to_lowercase(); let analogy = DynamicAnalogy { source: concept_a.to_string(), source_relation: format!("{} {}", rel_a.relation.as_str(), rel_a.to), source_rel_type: rel_a.relation, target: concept_b.to_string(), target_relation: format!("{} {}", rel_b.relation.as_str(), rel_b.to), target_rel_type: rel_b.relation, is_parallel: target_same, confidence: if target_same { 0.9 } else { 0.7 }, }; analogies.push(analogy); } // Inverse relation → potential contrast/opposite if let Some(inv) = rel_a.relation.inverse() { if inv == rel_b.relation && rel_a.from.to_lowercase() != rel_b.from.to_lowercase() { analogies.push(DynamicAnalogy { source: concept_a.to_string(), source_relation: format!("{} {}", rel_a.relation.as_str(), rel_a.to), source_rel_type: rel_a.relation, target: concept_b.to_string(), target_relation: format!("{} {}", rel_b.relation.as_str(), rel_b.to), target_rel_type: rel_b.relation, is_parallel: false, confidence: 0.5, }); } } } } // Sort by confidence and deduplicate analogies.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap()); analogies.dedup_by(|a, b| { a.source == b.source && a.target == b.target && a.source_rel_type == b.source_rel_type }); analogies } /// Find the best analogy connecting two arbitrary concepts by traversing the graph. /// /// Unlike `find_analogies` which takes two specific concepts, this one searches /// the graph for any two concepts that share structural similarity — useful when /// we want to understand "what is X like?" without knowing Y in advance. pub fn find_any_analogy_for(&self, concept: &str) -> Vec { let concept_lower = concept.to_lowercase(); let mut all_analogies = Vec::new(); // Find concepts that share a relationship with this one let rels_from = self .relationships .iter() .filter(|r| { r.from.to_lowercase() == concept_lower || r.to.to_lowercase() == concept_lower }) .collect::>(); for rel in &rels_from { // Find another relationship of the SAME type let others: Vec<_> = self .relationships .iter() .filter(|r| r.relation == rel.relation && r.from != rel.from && r.to != rel.to) .collect(); for other in others { let shared_targets = rel.to.to_lowercase() == other.to.to_lowercase(); let shared_sources = rel.from.to_lowercase() == other.from.to_lowercase(); if !shared_targets && !shared_sources { // Genuinely different — might be an analogy let (src, tgt, oth_src, oth_tgt) = if rel.from.to_lowercase() == concept_lower { (&rel.from, &rel.to, &other.from, &other.to) } else { (&rel.to, &rel.from, &other.from, &other.to) }; all_analogies.push(DynamicAnalogy { source: src.clone(), source_relation: format!("{} {}", rel.relation.as_str(), tgt), source_rel_type: rel.relation, target: oth_src.clone(), target_relation: format!("{} {}", other.relation.as_str(), oth_tgt), target_rel_type: other.relation, is_parallel: false, confidence: 0.5, }); } } } // Also try transitive analogies: if A→X→Y and B→Z→W, and X≈Z, then A:Y :: B:W let rels_from_concept: Vec<_> = self .relationships .iter() .filter(|r| r.from.to_lowercase() == concept_lower) .collect(); for rel in &rels_from_concept { let second_hop: Vec<_> = self .relationships .iter() .filter(|r| r.from.to_lowercase() == rel.to.to_lowercase()) .collect(); for sh in &second_hop { // Find another chain of same length with same relation type at both hops let matches: Vec<_> = self .relationships .iter() .filter(|r| { r.relation == rel.relation && r.from.to_lowercase() != concept_lower }) .collect(); for m in &matches { let m2: Vec<_> = self .relationships .iter() .filter(|r| { r.from.to_lowercase() == m.to.to_lowercase() && r.relation == sh.relation }) .collect(); for m2 in m2 { if rel.to.to_lowercase() != m.to.to_lowercase() { all_analogies.push(DynamicAnalogy { source: concept.to_string(), source_relation: format!( "{} → {} {}", rel.relation.as_str(), rel.to, sh.relation.as_str() ), source_rel_type: rel.relation, target: m.from.clone(), target_relation: format!( "{} → {} {}", m.relation.as_str(), m.to, m2.relation.as_str() ), target_rel_type: m.relation, is_parallel: false, confidence: 0.6, }); } } } } } all_analogies.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap()); all_analogies.truncate(5); all_analogies } /// Get concepts that are related to the given concept (1-hop neighbors). pub fn neighbors(&self, concept: &str) -> Vec<(&Relationship, String)> { let concept_lower = concept.to_lowercase(); let mut result = Vec::new(); for rel in &self.relationships { if rel.from.to_lowercase() == concept_lower { result.push((rel, rel.to.clone())); } if rel.to.to_lowercase() == concept_lower { if let Some(_inv) = rel.relation.inverse() { result.push((rel, rel.from.clone())); } } } result } } /// A dynamically discovered analogy from the knowledge graph. #[derive(Debug, Clone, PartialEq)] pub struct DynamicAnalogy { /// The source concept pub source: String, /// How the source relates (e.g., "causes heat") pub source_relation: String, /// The type of the source relationship pub source_rel_type: RelationType, /// The target concept pub target: String, /// How the target relates pub target_relation: String, /// The type of the target relationship pub target_rel_type: RelationType, /// Whether this is a parallel (same thing) or contrast (different) pub is_parallel: bool, /// Confidence in this analogy pub confidence: f64, } impl DynamicAnalogy { /// Human-readable explanation of this analogy. pub fn explanation(&self) -> String { if self.is_parallel { format!( "{} is to {} as {} is to {} — they share the same relational structure", self.source, self.source_relation, self.target, self.target_relation ) } else { format!( "{} is like {}: {} — parallel structure across different domains", self.source, self.target, self.source_relation ) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_entity_operations() { let mut kg = KnowledgeGraph::new(); kg.add_entity("Star"); kg.add_fact("Star", "A reasoning intelligence"); assert!(kg.get_entity("Star").is_some()); assert_eq!(kg.entity_count(), 1); } #[test] fn test_relationships() { let mut kg = KnowledgeGraph::new(); kg.add_relationship("Fire", RelationType::Causes, "Heat"); kg.add_relationship("Heat", RelationType::Causes, "Expansion"); let effects = kg.get_effects("Fire"); assert!(effects.contains(&"causes Heat".to_string())); } #[test] fn test_transitive_inference() { let mut kg = KnowledgeGraph::new(); kg.add_relationship("A", RelationType::IsA, "B"); kg.add_relationship("B", RelationType::IsA, "C"); kg.add_relationship("C", RelationType::IsA, "D"); let inferred = kg.infer_transitive("A", &RelationType::IsA, 3); assert!(inferred.contains(&"B".to_string())); assert!(inferred.contains(&"C".to_string())); assert!(inferred.contains(&"D".to_string())); } #[test] fn test_find_connection() { let mut kg = KnowledgeGraph::new(); kg.add_relationship("Fire", RelationType::Causes, "Heat"); kg.add_relationship("Heat", RelationType::Causes, "Expansion"); kg.add_relationship("Expansion", RelationType::Causes, "Pressure"); let path = kg.find_connection("Fire", "Pressure", 5); assert!(path.is_some()); let path = path.unwrap(); assert_eq!(path[0], "Fire"); assert_eq!(path.last().unwrap(), "Pressure"); } }