Skip to main content

graphrag_core/core/
mod.rs

1//! Core data structures and abstractions for GraphRAG
2//!
3//! This module contains the fundamental types, traits, and error handling
4//! that power the GraphRAG system.
5
6pub mod error;
7pub mod metadata;
8
9// Registry requires async feature (uses storage)
10#[cfg(feature = "async")]
11pub mod registry;
12
13// Traits module requires async feature
14#[cfg(feature = "async")]
15pub mod traits;
16
17// Ollama adapters require both async and ollama features
18#[cfg(all(feature = "async", feature = "ollama"))]
19pub mod ollama_adapters;
20
21// Entity extraction adapters require async feature
22#[cfg(feature = "async")]
23pub mod entity_adapters;
24
25// Retrieval system adapters require async feature
26#[cfg(feature = "async")]
27pub mod retrieval_adapters;
28
29// Test utilities for mock implementations
30#[cfg(feature = "async")]
31pub mod test_utils;
32
33#[cfg(test)]
34pub mod test_traits;
35
36// Re-export key items for convenience
37pub use error::{ErrorContext, ErrorSeverity, ErrorSuggestion, GraphRAGError, Result};
38pub use metadata::ChunkMetadata;
39
40#[cfg(feature = "async")]
41pub use registry::{RegistryBuilder, ServiceConfig, ServiceContext, ServiceRegistry};
42
43// Traits require async feature
44#[cfg(feature = "async")]
45pub use traits::*;
46
47/// Core trait for text chunking strategies
48///
49/// This trait provides a simple interface for different chunking approaches.
50/// Implementations can range from simple text splitters to sophisticated
51/// AST-based code chunking strategies.
52///
53/// # Examples
54///
55/// ```ignore
56/// use graphrag_core::{ChunkingStrategy, TextChunk};
57///
58/// struct SimpleChunker;
59///
60/// impl ChunkingStrategy for SimpleChunker {
61///     fn chunk(&self, text: &str) -> Vec<TextChunk> {
62///         // Simple implementation
63///         vec![]
64///     }
65/// }
66/// ```
67pub trait ChunkingStrategy: Send + Sync {
68    /// Chunk text into pieces following the strategy's logic
69    ///
70    /// # Arguments
71    /// * `text` - The input text to chunk
72    ///
73    /// # Returns
74    /// A vector of TextChunk objects representing the chunks
75    fn chunk(&self, text: &str) -> Vec<TextChunk>;
76}
77
78use indexmap::IndexMap;
79use petgraph::{graph::NodeIndex, Graph};
80use std::collections::HashMap;
81
82// PageRank-related imports are only available when the feature is enabled
83#[cfg(feature = "pagerank")]
84use sprs::CsMat;
85
86/// Type alias for adjacency matrix build result to reduce type complexity
87/// Only available when pagerank feature is enabled
88#[cfg(feature = "pagerank")]
89type AdjacencyMatrixResult = (
90    CsMat<f64>,
91    HashMap<EntityId, usize>,
92    HashMap<usize, EntityId>,
93);
94
95/// Unique identifier for documents
96#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
97pub struct DocumentId(pub String);
98
99impl DocumentId {
100    /// Creates a new DocumentId from a string
101    pub fn new(id: String) -> Self {
102        Self(id)
103    }
104}
105
106impl std::fmt::Display for DocumentId {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "{}", self.0)
109    }
110}
111
112impl From<String> for DocumentId {
113    fn from(s: String) -> Self {
114        Self(s)
115    }
116}
117
118impl From<DocumentId> for String {
119    fn from(id: DocumentId) -> Self {
120        id.0
121    }
122}
123
124/// Unique identifier for entities
125#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
126pub struct EntityId(pub String);
127
128impl EntityId {
129    /// Creates a new EntityId from a string
130    pub fn new(id: String) -> Self {
131        Self(id)
132    }
133}
134
135impl std::fmt::Display for EntityId {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(f, "{}", self.0)
138    }
139}
140
141impl From<String> for EntityId {
142    fn from(s: String) -> Self {
143        Self(s)
144    }
145}
146
147impl From<EntityId> for String {
148    fn from(id: EntityId) -> Self {
149        id.0
150    }
151}
152
153/// Unique identifier for text chunks
154#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
155pub struct ChunkId(pub String);
156
157impl ChunkId {
158    /// Creates a new ChunkId from a string
159    pub fn new(id: String) -> Self {
160        Self(id)
161    }
162}
163
164impl std::fmt::Display for ChunkId {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        write!(f, "{}", self.0)
167    }
168}
169
170impl From<String> for ChunkId {
171    fn from(s: String) -> Self {
172        Self(s)
173    }
174}
175
176impl From<ChunkId> for String {
177    fn from(id: ChunkId) -> Self {
178        id.0
179    }
180}
181
182/// A document in the system
183#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
184pub struct Document {
185    /// Unique identifier for the document
186    pub id: DocumentId,
187    /// Title of the document
188    pub title: String,
189    /// Full text content of the document
190    pub content: String,
191    /// Additional metadata key-value pairs
192    pub metadata: IndexMap<String, String>,
193    /// Text chunks extracted from the document
194    pub chunks: Vec<TextChunk>,
195}
196
197/// A chunk of text from a document
198#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
199pub struct TextChunk {
200    /// Unique identifier for the chunk
201    pub id: ChunkId,
202    /// ID of the parent document
203    pub document_id: DocumentId,
204    /// Text content of the chunk
205    pub content: String,
206    /// Starting character offset in the original document
207    pub start_offset: usize,
208    /// Ending character offset in the original document
209    pub end_offset: usize,
210    /// Optional vector embedding for the chunk
211    pub embedding: Option<Vec<f32>>,
212    /// List of entity IDs mentioned in this chunk
213    pub entities: Vec<EntityId>,
214    /// Semantic metadata for the chunk (chapter, keywords, summary, etc.)
215    pub metadata: ChunkMetadata,
216}
217
218/// An entity extracted from text
219#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
220pub struct Entity {
221    /// Unique identifier for the entity
222    pub id: EntityId,
223    /// Name or label of the entity
224    pub name: String,
225    /// Type or category of the entity (e.g., "person", "organization")
226    pub entity_type: String,
227    /// Confidence score for the entity extraction (0.0-1.0)
228    pub confidence: f32,
229    /// List of locations where this entity is mentioned
230    pub mentions: Vec<EntityMention>,
231    /// Optional vector embedding for the entity
232    pub embedding: Option<Vec<f32>>,
233
234    // Temporal fields (Phase 1.2 - Advanced GraphRAG)
235    /// First time this entity was mentioned (Unix timestamp)
236    #[serde(skip_serializing_if = "Option::is_none", default)]
237    pub first_mentioned: Option<i64>,
238    /// Last time this entity was mentioned (Unix timestamp)
239    #[serde(skip_serializing_if = "Option::is_none", default)]
240    pub last_mentioned: Option<i64>,
241    /// Temporal validity range (when the entity exists in the real world)
242    #[serde(skip_serializing_if = "Option::is_none", default)]
243    pub temporal_validity: Option<crate::graph::temporal::TemporalRange>,
244}
245
246/// A mention of an entity in text
247#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
248pub struct EntityMention {
249    /// ID of the chunk containing this mention
250    pub chunk_id: ChunkId,
251    /// Starting character offset of the mention in the chunk
252    pub start_offset: usize,
253    /// Ending character offset of the mention in the chunk
254    pub end_offset: usize,
255    /// Confidence score for this specific mention (0.0-1.0)
256    pub confidence: f32,
257}
258
259/// Relationship between entities
260#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
261pub struct Relationship {
262    /// Source entity ID for the relationship
263    pub source: EntityId,
264    /// Target entity ID for the relationship
265    pub target: EntityId,
266    /// Type of relationship (e.g., "works_for", "located_in")
267    pub relation_type: String,
268    /// Confidence score for the relationship (0.0-1.0)
269    pub confidence: f32,
270    /// Chunk IDs providing context for this relationship
271    pub context: Vec<ChunkId>,
272
273    /// Optional embedding vector for semantic similarity matching
274    #[serde(skip_serializing_if = "Option::is_none", default)]
275    pub embedding: Option<Vec<f32>>,
276
277    // Temporal and causal fields (Phase 1.2 - Advanced GraphRAG)
278    /// Type of temporal relationship (Before, During, Caused, etc.)
279    #[serde(skip_serializing_if = "Option::is_none", default)]
280    pub temporal_type: Option<crate::graph::temporal::TemporalRelationType>,
281    /// Temporal range when this relationship was valid
282    #[serde(skip_serializing_if = "Option::is_none", default)]
283    pub temporal_range: Option<crate::graph::temporal::TemporalRange>,
284    /// Strength of causal relationship (0.0-1.0), only meaningful for causal temporal types
285    #[serde(skip_serializing_if = "Option::is_none", default)]
286    pub causal_strength: Option<f32>,
287}
288
289impl Relationship {
290    /// Create a new relationship
291    pub fn new(source: EntityId, target: EntityId, relation_type: String, confidence: f32) -> Self {
292        Self {
293            source,
294            target,
295            relation_type,
296            confidence,
297            context: Vec::new(),
298            embedding: None,
299            // Temporal fields default to None (backward compatible)
300            temporal_type: None,
301            temporal_range: None,
302            causal_strength: None,
303        }
304    }
305
306    /// Add context chunks to the relationship
307    pub fn with_context(mut self, context: Vec<ChunkId>) -> Self {
308        self.context = context;
309        self
310    }
311
312    /// Set temporal type for this relationship
313    pub fn with_temporal_type(
314        mut self,
315        temporal_type: crate::graph::temporal::TemporalRelationType,
316    ) -> Self {
317        self.temporal_type = Some(temporal_type);
318        // Auto-set causal strength based on type if not already set
319        if self.causal_strength.is_none() && temporal_type.is_causal() {
320            self.causal_strength = Some(temporal_type.default_strength());
321        }
322        self
323    }
324
325    /// Set temporal range for this relationship
326    pub fn with_temporal_range(mut self, start: i64, end: i64) -> Self {
327        self.temporal_range = Some(crate::graph::temporal::TemporalRange::new(start, end));
328        self
329    }
330
331    /// Set causal strength for this relationship
332    pub fn with_causal_strength(mut self, strength: f32) -> Self {
333        self.causal_strength = Some(strength.clamp(0.0, 1.0));
334        self
335    }
336
337    /// Set embedding vector for this relationship (Phase 2.2)
338    pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
339        self.embedding = Some(embedding);
340        self
341    }
342}
343
344/// Knowledge graph containing entities and their relationships
345#[derive(Debug, Clone)]
346pub struct KnowledgeGraph {
347    graph: Graph<Entity, Relationship>,
348    entity_index: HashMap<EntityId, NodeIndex>,
349    documents: IndexMap<DocumentId, Document>,
350    chunks: IndexMap<ChunkId, TextChunk>,
351
352    /// Hierarchical organization of relationships (Phase 3.1)
353    #[cfg(feature = "async")]
354    pub relationship_hierarchy:
355        Option<crate::graph::hierarchical_relationships::RelationshipHierarchy>,
356}
357
358impl KnowledgeGraph {
359    /// Create a new empty knowledge graph
360    pub fn new() -> Self {
361        Self {
362            graph: Graph::new(),
363            entity_index: HashMap::new(),
364            documents: IndexMap::new(),
365            chunks: IndexMap::new(),
366            #[cfg(feature = "async")]
367            relationship_hierarchy: None,
368        }
369    }
370
371    /// Add a document to the knowledge graph
372    pub fn add_document(&mut self, document: Document) -> Result<()> {
373        let document_id = document.id.clone();
374
375        // Add chunks to the index
376        for chunk in &document.chunks {
377            self.chunks.insert(chunk.id.clone(), chunk.clone());
378        }
379
380        // Store the document
381        self.documents.insert(document_id, document);
382
383        Ok(())
384    }
385
386    /// Add an entity to the knowledge graph
387    pub fn add_entity(&mut self, entity: Entity) -> Result<NodeIndex> {
388        let entity_id = entity.id.clone();
389        let node_index = self.graph.add_node(entity);
390        self.entity_index.insert(entity_id, node_index);
391        Ok(node_index)
392    }
393
394    /// Add a relationship between entities
395    pub fn add_relationship(&mut self, relationship: Relationship) -> Result<()> {
396        let source_idx = self.entity_index.get(&relationship.source).ok_or_else(|| {
397            crate::GraphRAGError::GraphConstruction {
398                message: format!("Source entity {} not found", relationship.source),
399            }
400        })?;
401
402        let target_idx = self.entity_index.get(&relationship.target).ok_or_else(|| {
403            crate::GraphRAGError::GraphConstruction {
404                message: format!("Target entity {} not found", relationship.target),
405            }
406        })?;
407
408        self.graph.add_edge(*source_idx, *target_idx, relationship);
409        Ok(())
410    }
411
412    /// Add a chunk to the knowledge graph
413    pub fn add_chunk(&mut self, chunk: TextChunk) -> Result<()> {
414        self.chunks.insert(chunk.id.clone(), chunk);
415        Ok(())
416    }
417
418    /// Get an entity by ID
419    pub fn get_entity(&self, id: &EntityId) -> Option<&Entity> {
420        let node_idx = self.entity_index.get(id)?;
421        self.graph.node_weight(*node_idx)
422    }
423
424    /// Get a document by ID
425    pub fn get_document(&self, id: &DocumentId) -> Option<&Document> {
426        self.documents.get(id)
427    }
428
429    /// Get a chunk by ID
430    pub fn get_chunk(&self, id: &ChunkId) -> Option<&TextChunk> {
431        self.chunks.get(id)
432    }
433
434    /// Get a mutable reference to an entity by ID
435    pub fn get_entity_mut(&mut self, id: &EntityId) -> Option<&mut Entity> {
436        let node_idx = self.entity_index.get(id)?;
437        self.graph.node_weight_mut(*node_idx)
438    }
439
440    /// Get a mutable reference to a chunk by ID
441    pub fn get_chunk_mut(&mut self, id: &ChunkId) -> Option<&mut TextChunk> {
442        self.chunks.get_mut(id)
443    }
444
445    /// Get all entities
446    pub fn entities(&self) -> impl Iterator<Item = &Entity> {
447        self.graph.node_weights()
448    }
449
450    /// Get all entities (mutable)
451    pub fn entities_mut(&mut self) -> impl Iterator<Item = &mut Entity> {
452        self.graph.node_weights_mut()
453    }
454
455    /// Get all documents
456    pub fn documents(&self) -> impl Iterator<Item = &Document> {
457        self.documents.values()
458    }
459
460    /// Get all documents (mutable)
461    pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
462        self.documents.values_mut()
463    }
464
465    /// Get all chunks
466    pub fn chunks(&self) -> impl Iterator<Item = &TextChunk> {
467        self.chunks.values()
468    }
469
470    /// Get all chunks (mutable)
471    pub fn chunks_mut(&mut self) -> impl Iterator<Item = &mut TextChunk> {
472        self.chunks.values_mut()
473    }
474
475    /// Get neighbors of an entity
476    pub fn get_neighbors(&self, entity_id: &EntityId) -> Vec<(&Entity, &Relationship)> {
477        use petgraph::visit::EdgeRef;
478
479        if let Some(&node_idx) = self.entity_index.get(entity_id) {
480            self.graph
481                .edges(node_idx)
482                .filter_map(|edge| {
483                    let target_entity = self.graph.node_weight(edge.target())?;
484                    Some((target_entity, edge.weight()))
485                })
486                .collect()
487        } else {
488            Vec::new()
489        }
490    }
491
492    /// Get all relationships in the graph
493    pub fn get_all_relationships(&self) -> Vec<&Relationship> {
494        self.graph.edge_weights().collect()
495    }
496
497    /// Load knowledge graph from JSON file
498    pub fn load_from_json(file_path: &str) -> Result<Self> {
499        use std::fs;
500
501        // Read and parse JSON
502        let json_str = fs::read_to_string(file_path)?;
503        let json_data = json::parse(&json_str).map_err(|e| GraphRAGError::Config {
504            message: format!("Failed to parse JSON: {}", e),
505        })?;
506
507        let mut kg = KnowledgeGraph::new();
508
509        // Load entities
510        if json_data["entities"].is_array() {
511            for entity_obj in json_data["entities"].members() {
512                let id = EntityId::new(entity_obj["id"].as_str().unwrap_or("").to_string());
513                let name = entity_obj["name"].as_str().unwrap_or("").to_string();
514                let entity_type = entity_obj["type"].as_str().unwrap_or("").to_string();
515                let confidence = entity_obj["confidence"].as_f32().unwrap_or(0.0);
516
517                // Parse mentions
518                let mut mentions = Vec::new();
519                if entity_obj["mentions"].is_array() {
520                    for mention_obj in entity_obj["mentions"].members() {
521                        let mention = EntityMention {
522                            chunk_id: ChunkId::new(
523                                mention_obj["chunk_id"].as_str().unwrap_or("").to_string(),
524                            ),
525                            start_offset: mention_obj["start_offset"].as_usize().unwrap_or(0),
526                            end_offset: mention_obj["end_offset"].as_usize().unwrap_or(0),
527                            confidence: mention_obj["confidence"].as_f32().unwrap_or(0.0),
528                        };
529                        mentions.push(mention);
530                    }
531                }
532
533                let entity = Entity {
534                    id,
535                    name,
536                    entity_type,
537                    confidence,
538                    mentions,
539                    embedding: None, // Embeddings not stored in JSON
540                    first_mentioned: None,
541                    last_mentioned: None,
542                    temporal_validity: None,
543                };
544
545                kg.add_entity(entity)?;
546            }
547        }
548
549        // Load relationships
550        if json_data["relationships"].is_array() {
551            for rel_obj in json_data["relationships"].members() {
552                let source = EntityId::new(rel_obj["source_id"].as_str().unwrap_or("").to_string());
553                let target = EntityId::new(rel_obj["target_id"].as_str().unwrap_or("").to_string());
554                let relation_type = rel_obj["relation_type"].as_str().unwrap_or("").to_string();
555                let confidence = rel_obj["confidence"].as_f32().unwrap_or(0.0);
556
557                let mut context = Vec::new();
558                if rel_obj["context_chunks"].is_array() {
559                    for chunk_id in rel_obj["context_chunks"].members() {
560                        if let Some(chunk_id_str) = chunk_id.as_str() {
561                            context.push(ChunkId::new(chunk_id_str.to_string()));
562                        }
563                    }
564                }
565
566                let relationship = Relationship {
567                    source,
568                    target,
569                    relation_type,
570                    confidence,
571                    context,
572                    embedding: None,
573                    temporal_type: None,
574                    temporal_range: None,
575                    causal_strength: None,
576                };
577
578                // Ignore errors if entities don't exist
579                let _ = kg.add_relationship(relationship);
580            }
581        }
582
583        // Load chunks with full content
584        if json_data["chunks"].is_array() {
585            for chunk_obj in json_data["chunks"].members() {
586                let id = ChunkId::new(chunk_obj["id"].as_str().unwrap_or("").to_string());
587                let document_id =
588                    DocumentId::new(chunk_obj["document_id"].as_str().unwrap_or("").to_string());
589                let start_offset = chunk_obj["start_offset"].as_usize().unwrap_or(0);
590                let end_offset = chunk_obj["end_offset"].as_usize().unwrap_or(0);
591
592                // Get full content
593                let content = chunk_obj["content"].as_str().unwrap_or("").to_string();
594
595                // Load entities list
596                let mut entities = Vec::new();
597                if chunk_obj["entities"].is_array() {
598                    for entity_id in chunk_obj["entities"].members() {
599                        if let Some(entity_id_str) = entity_id.as_str() {
600                            entities.push(EntityId::new(entity_id_str.to_string()));
601                        }
602                    }
603                }
604
605                let chunk = TextChunk {
606                    id,
607                    document_id,
608                    content,
609                    start_offset,
610                    end_offset,
611                    embedding: None, // Embeddings not stored in JSON
612                    entities,
613                    metadata: ChunkMetadata::default(),
614                };
615                kg.add_chunk(chunk)?;
616            }
617        }
618
619        // Load documents with full content
620        if json_data["documents"].is_array() {
621            for doc_obj in json_data["documents"].members() {
622                let id = DocumentId::new(doc_obj["id"].as_str().unwrap_or("").to_string());
623                let title = doc_obj["title"].as_str().unwrap_or("").to_string();
624                let content = doc_obj["content"].as_str().unwrap_or("").to_string();
625
626                // Parse metadata
627                let mut metadata = IndexMap::new();
628                if doc_obj["metadata"].is_object() {
629                    for (key, value) in doc_obj["metadata"].entries() {
630                        metadata.insert(key.to_string(), value.as_str().unwrap_or("").to_string());
631                    }
632                }
633
634                let document = Document {
635                    id,
636                    title,
637                    content,
638                    metadata,
639                    chunks: vec![], // Chunks are stored separately in the graph
640                };
641                kg.add_document(document)?;
642            }
643        }
644
645        Ok(kg)
646    }
647
648    /// Save knowledge graph to JSON file with optimized format for entities and relationships
649    pub fn save_to_json(&self, file_path: &str) -> Result<()> {
650        use std::fs;
651
652        // Create optimized JSON structure based on 2024 best practices
653        let mut json_data = json::JsonValue::new_object();
654
655        // Add metadata
656        json_data["metadata"] = json::object! {
657            "format_version" => "2.0",
658            "created_at" => chrono::Utc::now().to_rfc3339(),
659            "total_entities" => self.entities().count(),
660            "total_relationships" => self.get_all_relationships().len(),
661            "total_chunks" => self.chunks().count(),
662            "total_documents" => self.documents().count()
663        };
664
665        // Add entities with enhanced information
666        let mut entities_array = json::JsonValue::new_array();
667        for entity in self.entities() {
668            let mut entity_obj = json::object! {
669                "id" => entity.id.to_string(),
670                "name" => entity.name.clone(),
671                "type" => entity.entity_type.clone(),
672                "confidence" => entity.confidence,
673                "mentions_count" => entity.mentions.len()
674            };
675
676            // Add mentions with chunk references
677            let mut mentions_array = json::JsonValue::new_array();
678            for mention in &entity.mentions {
679                mentions_array
680                    .push(json::object! {
681                        "chunk_id" => mention.chunk_id.to_string(),
682                        "start_offset" => mention.start_offset,
683                        "end_offset" => mention.end_offset,
684                        "confidence" => mention.confidence
685                    })
686                    .unwrap();
687            }
688            entity_obj["mentions"] = mentions_array;
689
690            // Add embedding if present
691            if let Some(embedding) = &entity.embedding {
692                entity_obj["has_embedding"] = true.into();
693                entity_obj["embedding_dimension"] = embedding.len().into();
694                // Store only first few dimensions for debugging (full embedding too large for JSON)
695                let sample_embedding: Vec<f32> = embedding.iter().take(5).cloned().collect();
696                entity_obj["embedding_sample"] = sample_embedding.into();
697            } else {
698                entity_obj["has_embedding"] = false.into();
699            }
700
701            entities_array.push(entity_obj).unwrap();
702        }
703        json_data["entities"] = entities_array;
704
705        // Add relationships with detailed information
706        let mut relationships_array = json::JsonValue::new_array();
707        for relationship in self.get_all_relationships() {
708            let rel_obj = json::object! {
709                "source_id" => relationship.source.to_string(),
710                "target_id" => relationship.target.to_string(),
711                "relation_type" => relationship.relation_type.clone(),
712                "confidence" => relationship.confidence,
713                "context_chunks" => relationship.context.iter()
714                    .map(|c| c.to_string())
715                    .collect::<Vec<String>>()
716            };
717            relationships_array.push(rel_obj).unwrap();
718        }
719        json_data["relationships"] = relationships_array;
720
721        // Add chunks information with FULL content for persistence
722        let mut chunks_array = json::JsonValue::new_array();
723        for chunk in self.chunks() {
724            let mut chunk_obj = json::object! {
725                "id" => chunk.id.to_string(),
726                "document_id" => chunk.document_id.to_string(),
727                "content" => chunk.content.clone(),  // Full content for persistence
728                "start_offset" => chunk.start_offset,
729                "end_offset" => chunk.end_offset
730            };
731
732            // Add entities list
733            let entities_list: Vec<String> = chunk.entities.iter().map(|e| e.to_string()).collect();
734            chunk_obj["entities"] = entities_list.into();
735
736            // Add embedding info
737            chunk_obj["has_embedding"] = chunk.embedding.is_some().into();
738            if let Some(embedding) = &chunk.embedding {
739                chunk_obj["embedding_dimension"] = embedding.len().into();
740            }
741
742            chunks_array.push(chunk_obj).unwrap();
743        }
744        json_data["chunks"] = chunks_array;
745
746        // Add documents information with FULL content for persistence
747        let mut documents_array = json::JsonValue::new_array();
748        for document in self.documents() {
749            let mut meta_obj = json::JsonValue::new_object();
750            for (key, value) in &document.metadata {
751                meta_obj[key] = value.clone().into();
752            }
753
754            let doc_obj = json::object! {
755                "id" => document.id.to_string(),
756                "title" => document.title.clone(),
757                "content" => document.content.clone(),  // Full content for persistence
758                "metadata" => meta_obj
759            };
760            documents_array.push(doc_obj).unwrap();
761        }
762        json_data["documents"] = documents_array;
763
764        // Save to file
765        fs::write(file_path, json_data.dump())?;
766        #[cfg(feature = "tracing")]
767        tracing::info!("Knowledge graph saved to {file_path}");
768
769        Ok(())
770    }
771
772    /// Find entities by name (case-insensitive partial match)
773    pub fn find_entities_by_name(&self, name: &str) -> impl Iterator<Item = &Entity> {
774        let name_lower = name.to_lowercase();
775        self.entities()
776            .filter(move |entity| entity.name.to_lowercase().contains(&name_lower))
777    }
778
779    /// Get entity by ID (string version for compatibility)
780    pub fn get_entity_by_id(&self, id: &str) -> Option<&Entity> {
781        let entity_id = EntityId::new(id.to_string());
782        self.get_entity(&entity_id)
783    }
784
785    /// Get entity relationships
786    pub fn get_entity_relationships(&self, entity_id: &str) -> impl Iterator<Item = &Relationship> {
787        let entity_id = EntityId::new(entity_id.to_string());
788        if let Some(&node_idx) = self.entity_index.get(&entity_id) {
789            self.graph
790                .edges(node_idx)
791                .map(|edge| edge.weight())
792                .collect::<Vec<_>>()
793                .into_iter()
794        } else {
795            Vec::new().into_iter()
796        }
797    }
798
799    /// Find relationship path between two entities (simplified BFS)
800    pub fn find_relationship_path(
801        &self,
802        entity1: &str,
803        entity2: &str,
804        _max_depth: usize,
805    ) -> Vec<String> {
806        let entity1_id = EntityId::new(entity1.to_string());
807        let entity2_id = EntityId::new(entity2.to_string());
808
809        let node1 = self.entity_index.get(&entity1_id);
810        let node2 = self.entity_index.get(&entity2_id);
811
812        if let (Some(&start), Some(&end)) = (node1, node2) {
813            // Simple path finding - just check direct connections for now
814            use petgraph::visit::EdgeRef;
815            for edge in self.graph.edges(start) {
816                if edge.target() == end {
817                    return vec![edge.weight().relation_type.clone()];
818                }
819            }
820        }
821
822        Vec::new() // No path found or entities don't exist
823    }
824
825    /// Build PageRank calculator from current graph structure
826    /// Only available when pagerank feature is enabled
827    #[cfg(feature = "pagerank")]
828    pub fn build_pagerank_calculator(
829        &self,
830    ) -> Result<crate::graph::pagerank::PersonalizedPageRank> {
831        let config = crate::graph::pagerank::PageRankConfig::default();
832        let (adjacency_matrix, node_mapping, reverse_mapping) = self.build_adjacency_matrix()?;
833
834        Ok(crate::graph::pagerank::PersonalizedPageRank::new(
835            config,
836            adjacency_matrix,
837            node_mapping,
838            reverse_mapping,
839        ))
840    }
841
842    /// Build adjacency matrix for PageRank calculations
843    /// Only available when pagerank feature is enabled
844    #[cfg(feature = "pagerank")]
845    fn build_adjacency_matrix(&self) -> Result<AdjacencyMatrixResult> {
846        let nodes: Vec<EntityId> = self.entities().map(|e| e.id.clone()).collect();
847        let node_mapping: HashMap<EntityId, usize> = nodes
848            .iter()
849            .enumerate()
850            .map(|(i, id)| (id.clone(), i))
851            .collect();
852        let reverse_mapping: HashMap<usize, EntityId> = nodes
853            .iter()
854            .enumerate()
855            .map(|(i, id)| (i, id.clone()))
856            .collect();
857
858        // Build sparse adjacency matrix from relationships
859        let mut row_indices = Vec::new();
860        let mut col_indices = Vec::new();
861        let mut values = Vec::new();
862
863        for relationship in self.get_all_relationships() {
864            if let (Some(&from_idx), Some(&to_idx)) = (
865                node_mapping.get(&relationship.source),
866                node_mapping.get(&relationship.target),
867            ) {
868                row_indices.push(from_idx);
869                col_indices.push(to_idx);
870                values.push(relationship.confidence as f64);
871            }
872        }
873
874        let matrix = if row_indices.is_empty() {
875            // Create an empty matrix if no relationships
876            sprs::CsMat::zero((nodes.len(), nodes.len()))
877        } else {
878            // Build using triplet matrix first, then convert to CSR
879            let mut triplet_mat = sprs::TriMat::new((nodes.len(), nodes.len()));
880            for ((row, col), val) in row_indices.into_iter().zip(col_indices).zip(values) {
881                triplet_mat.add_triplet(row, col, val);
882            }
883            triplet_mat.to_csr()
884        };
885
886        Ok((matrix, node_mapping, reverse_mapping))
887    }
888
889    /// Count entities in the graph
890    pub fn entity_count(&self) -> usize {
891        self.entities().count()
892    }
893
894    /// Count relationships in the graph
895    pub fn relationship_count(&self) -> usize {
896        self.get_all_relationships().len()
897    }
898
899    /// Count documents in the graph
900    pub fn document_count(&self) -> usize {
901        self.documents().count()
902    }
903
904    /// Get all relationships as an iterator
905    pub fn relationships(&self) -> impl Iterator<Item = &Relationship> {
906        self.graph.edge_weights()
907    }
908
909    /// Clear all entities and relationships while preserving documents and chunks
910    ///
911    /// This is useful for rebuilding the graph from scratch without reloading documents.
912    pub fn clear_entities_and_relationships(&mut self) {
913        self.graph.clear();
914        self.entity_index.clear();
915        // Note: documents and chunks are preserved
916    }
917
918    /// Calculate cosine similarity between two embedding vectors
919    ///
920    /// # Arguments
921    ///
922    /// * `a` - First embedding vector
923    /// * `b` - Second embedding vector
924    ///
925    /// # Returns
926    ///
927    /// Cosine similarity score (range: -1.0 to 1.0, typically 0.0 to 1.0 for embeddings)
928    fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
929        if a.len() != b.len() || a.is_empty() {
930            return 0.0;
931        }
932
933        let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
934        let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
935        let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
936
937        if norm_a == 0.0 || norm_b == 0.0 {
938            return 0.0;
939        }
940
941        dot_product / (norm_a * norm_b)
942    }
943
944    /// Calculate temporal relevance score for a temporal range
945    ///
946    /// Gives higher scores to:
947    /// - Recent events (closer to current time)
948    /// - Events with known temporal bounds
949    ///
950    /// # Arguments
951    ///
952    /// * `range` - The temporal range to score
953    ///
954    /// # Returns
955    ///
956    /// Temporal relevance boost (0.0-0.3 range)
957    fn calculate_temporal_relevance(range: &crate::graph::temporal::TemporalRange) -> f32 {
958        use std::time::{SystemTime, UNIX_EPOCH};
959
960        // Get current timestamp
961        let now = SystemTime::now()
962            .duration_since(UNIX_EPOCH)
963            .unwrap_or_default()
964            .as_secs() as i64;
965
966        // Calculate recency: how close is the event to now?
967        let mid_point = (range.start + range.end) / 2;
968        let years_ago = ((now - mid_point) / (365 * 24 * 3600)).abs();
969
970        // Decay function: more recent = higher score
971        // Events in the past 10 years get max boost (0.3)
972        // Older events get gradually less boost
973        let recency_boost = if years_ago <= 10 {
974            0.3
975        } else if years_ago <= 50 {
976            0.3 * (1.0 - (years_ago - 10) as f32 / 40.0)
977        } else if years_ago <= 200 {
978            0.1 * (1.0 - (years_ago - 50) as f32 / 150.0)
979        } else {
980            0.05 // Ancient events still get small boost for having temporal info
981        };
982
983        recency_boost.max(0.0)
984    }
985
986    /// Calculate dynamic weight for a relationship based on query context (Phase 2.2)
987    ///
988    /// Combines multiple factors:
989    /// - Base weight: relationship.confidence
990    /// - Semantic boost: similarity between relationship and query embeddings
991    /// - Temporal boost: relevance of temporal range
992    /// - Conceptual boost: matching query concepts in relationship type
993    ///
994    /// # Arguments
995    ///
996    /// * `relationship` - The relationship to weight
997    /// * `query_embedding` - Optional embedding vector for the query
998    /// * `query_concepts` - Concepts extracted from the query
999    ///
1000    /// # Returns
1001    ///
1002    /// Dynamic weight value (typically 0.0-2.0, can be higher with strong matches)
1003    pub fn dynamic_weight(
1004        &self,
1005        relationship: &Relationship,
1006        query_embedding: Option<&[f32]>,
1007        query_concepts: &[String],
1008    ) -> f32 {
1009        let base_weight = relationship.confidence;
1010
1011        // Semantic boost: similarity between relationship and query
1012        let semantic_boost = if let (Some(rel_emb), Some(query_emb)) =
1013            (relationship.embedding.as_deref(), query_embedding)
1014        {
1015            Self::cosine_similarity(rel_emb, query_emb).max(0.0)
1016        } else {
1017            0.0
1018        };
1019
1020        // Temporal boost: relationships with recent or relevant temporal ranges
1021        let temporal_boost = if let Some(tr) = &relationship.temporal_range {
1022            Self::calculate_temporal_relevance(tr)
1023        } else {
1024            0.0
1025        };
1026
1027        // Conceptual boost: relationships whose type matches query concepts
1028        let concept_boost = query_concepts
1029            .iter()
1030            .filter(|c| {
1031                relationship
1032                    .relation_type
1033                    .to_lowercase()
1034                    .contains(&c.to_lowercase())
1035            })
1036            .count() as f32
1037            * 0.15; // 15% boost per matching concept
1038
1039        // Causal boost: causal relationships get extra weight
1040        let causal_boost = if let Some(strength) = relationship.causal_strength {
1041            strength * 0.2 // Up to 20% boost for strong causal links
1042        } else {
1043            0.0
1044        };
1045
1046        // Combine all factors
1047        base_weight * (1.0 + semantic_boost + temporal_boost + concept_boost + causal_boost)
1048    }
1049
1050    /// Convert KnowledgeGraph to petgraph format for Leiden clustering
1051    /// Returns a graph with entity names as nodes and relationship confidences as edge weights
1052    /// Only available when leiden feature is enabled
1053    #[cfg(feature = "leiden")]
1054    pub fn to_leiden_graph(&self) -> petgraph::Graph<String, f32, petgraph::Undirected> {
1055        let mut graph = Graph::new_undirected();
1056        let mut node_map = HashMap::new();
1057
1058        // Add nodes (entities) - use entity name as node label
1059        for entity in self.entities() {
1060            let idx = graph.add_node(entity.name.clone());
1061            node_map.insert(entity.id.clone(), idx);
1062        }
1063
1064        // Add edges (relationships) with confidence as weight
1065        for rel in self.get_all_relationships() {
1066            if let (Some(&src), Some(&tgt)) = (node_map.get(&rel.source), node_map.get(&rel.target))
1067            {
1068                graph.add_edge(src, tgt, rel.confidence);
1069            }
1070        }
1071
1072        graph
1073    }
1074
1075    /// Detect hierarchical communities in the entity graph using Leiden algorithm
1076    /// Only available when leiden feature is enabled
1077    ///
1078    /// # Arguments
1079    /// * `config` - Leiden algorithm configuration
1080    ///
1081    /// # Returns
1082    /// HierarchicalCommunities structure with community assignments at each level
1083    ///
1084    /// # Example
1085    /// ```no_run
1086    /// use graphrag_core::{KnowledgeGraph, graph::LeidenConfig};
1087    ///
1088    /// let graph = KnowledgeGraph::new();
1089    /// // ... build graph ...
1090    ///
1091    /// let config = LeidenConfig {
1092    ///     max_cluster_size: 10,
1093    ///     resolution: 1.0,
1094    ///     ..Default::default()
1095    /// };
1096    ///
1097    /// let communities = graph.detect_hierarchical_communities(config).unwrap();
1098    /// ```
1099    #[cfg(feature = "leiden")]
1100    pub fn detect_hierarchical_communities(
1101        &self,
1102        config: crate::graph::leiden::LeidenConfig,
1103    ) -> Result<crate::graph::leiden::HierarchicalCommunities> {
1104        use crate::graph::leiden::LeidenCommunityDetector;
1105
1106        // Convert to Leiden-compatible graph format
1107        let leiden_graph = self.to_leiden_graph();
1108
1109        // Create detector and run clustering
1110        let detector = LeidenCommunityDetector::new(config);
1111        let mut communities = detector.detect_communities(&leiden_graph)?;
1112
1113        // Enrich with entity metadata
1114        communities.entity_mapping = Some(self.build_entity_mapping());
1115
1116        Ok(communities)
1117    }
1118
1119    /// Build mapping from entity names to entity metadata
1120    /// Used to enrich hierarchical communities with entity information
1121    #[cfg(feature = "leiden")]
1122    fn build_entity_mapping(&self) -> HashMap<String, crate::graph::leiden::EntityMetadata> {
1123        use crate::graph::leiden::EntityMetadata;
1124
1125        self.entities()
1126            .map(|entity| {
1127                let metadata = EntityMetadata {
1128                    id: entity.id.to_string(),
1129                    name: entity.name.clone(),
1130                    entity_type: entity.entity_type.clone(),
1131                    confidence: entity.confidence,
1132                    mention_count: entity.mentions.len(),
1133                };
1134                (entity.name.clone(), metadata)
1135            })
1136            .collect()
1137    }
1138
1139    /// Build hierarchical organization of relationships (Phase 3.1)
1140    ///
1141    /// Creates multi-level clusters of relationships using recursive community detection
1142    /// and LLM-generated summaries for efficient retrieval.
1143    ///
1144    /// # Arguments
1145    /// * `num_levels` - Number of hierarchy levels to create (default: 3)
1146    /// * `ollama_client` - Optional Ollama client for generating cluster summaries
1147    ///
1148    /// # Returns
1149    /// Result containing the built hierarchy or an error
1150    ///
1151    /// # Example
1152    /// ```ignore
1153    /// use graphrag_core::{KnowledgeGraph, ollama::OllamaClient};
1154    ///
1155    /// let mut graph = KnowledgeGraph::new();
1156    /// // ... build graph ...
1157    ///
1158    /// let ollama = OllamaClient::new("http://localhost:11434", "llama3.2");
1159    /// graph.build_relationship_hierarchy(3, Some(ollama)).await.unwrap();
1160    /// ```
1161    #[cfg(feature = "async")]
1162    pub async fn build_relationship_hierarchy(
1163        &mut self,
1164        num_levels: usize,
1165        ollama_client: Option<crate::ollama::OllamaClient>,
1166    ) -> Result<()> {
1167        use crate::graph::hierarchical_relationships::HierarchyBuilder;
1168
1169        let builder = HierarchyBuilder::from_graph(self).with_num_levels(num_levels);
1170
1171        let builder = if let Some(client) = ollama_client {
1172            builder.with_ollama_client(client)
1173        } else {
1174            builder
1175        };
1176
1177        let hierarchy = builder.build().await?;
1178        self.relationship_hierarchy = Some(hierarchy);
1179
1180        Ok(())
1181    }
1182}
1183
1184impl Default for KnowledgeGraph {
1185    fn default() -> Self {
1186        Self::new()
1187    }
1188}
1189
1190impl Document {
1191    /// Create a new document
1192    pub fn new(id: DocumentId, title: String, content: String) -> Self {
1193        Self {
1194            id,
1195            title,
1196            content,
1197            metadata: IndexMap::new(),
1198            chunks: Vec::new(),
1199        }
1200    }
1201
1202    /// Add metadata to the document
1203    pub fn with_metadata(mut self, key: String, value: String) -> Self {
1204        self.metadata.insert(key, value);
1205        self
1206    }
1207
1208    /// Add chunks to the document
1209    pub fn with_chunks(mut self, chunks: Vec<TextChunk>) -> Self {
1210        self.chunks = chunks;
1211        self
1212    }
1213}
1214
1215impl TextChunk {
1216    /// Create a new text chunk
1217    pub fn new(
1218        id: ChunkId,
1219        document_id: DocumentId,
1220        content: String,
1221        start_offset: usize,
1222        end_offset: usize,
1223    ) -> Self {
1224        Self {
1225            id,
1226            document_id,
1227            content,
1228            start_offset,
1229            end_offset,
1230            embedding: None,
1231            entities: Vec::new(),
1232            metadata: ChunkMetadata::default(),
1233        }
1234    }
1235
1236    /// Add an embedding to the chunk
1237    pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
1238        self.embedding = Some(embedding);
1239        self
1240    }
1241
1242    /// Add entities to the chunk
1243    pub fn with_entities(mut self, entities: Vec<EntityId>) -> Self {
1244        self.entities = entities;
1245        self
1246    }
1247
1248    /// Add metadata to the chunk
1249    pub fn with_metadata(mut self, metadata: ChunkMetadata) -> Self {
1250        self.metadata = metadata;
1251        self
1252    }
1253}
1254
1255impl Entity {
1256    /// Create a new entity
1257    pub fn new(id: EntityId, name: String, entity_type: String, confidence: f32) -> Self {
1258        Self {
1259            id,
1260            name,
1261            entity_type,
1262            confidence,
1263            mentions: Vec::new(),
1264            embedding: None,
1265            // Temporal fields default to None (backward compatible)
1266            first_mentioned: None,
1267            last_mentioned: None,
1268            temporal_validity: None,
1269        }
1270    }
1271
1272    /// Add mentions to the entity
1273    pub fn with_mentions(mut self, mentions: Vec<EntityMention>) -> Self {
1274        self.mentions = mentions;
1275        self
1276    }
1277
1278    /// Add an embedding to the entity
1279    pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
1280        self.embedding = Some(embedding);
1281        self
1282    }
1283
1284    /// Set temporal validity range for this entity
1285    pub fn with_temporal_validity(mut self, start: i64, end: i64) -> Self {
1286        self.temporal_validity = Some(crate::graph::temporal::TemporalRange::new(start, end));
1287        self
1288    }
1289
1290    /// Set mention timestamps for this entity
1291    pub fn with_mention_times(mut self, first: i64, last: i64) -> Self {
1292        self.first_mentioned = Some(first);
1293        self.last_mentioned = Some(last);
1294        self
1295    }
1296}
1297
1298#[cfg(test)]
1299mod temporal_tests {
1300    use super::*;
1301
1302    #[test]
1303    fn test_entity_with_temporal_fields() {
1304        let entity = Entity::new(
1305            EntityId::new("socrates".to_string()),
1306            "Socrates".to_string(),
1307            "PERSON".to_string(),
1308            0.9,
1309        )
1310        .with_temporal_validity(-470 * 365 * 24 * 3600, -399 * 365 * 24 * 3600) // 470-399 BC
1311        .with_mention_times(1000, 2000);
1312
1313        assert_eq!(entity.name, "Socrates");
1314        assert!(entity.temporal_validity.is_some());
1315        assert!(entity.first_mentioned.is_some());
1316        assert!(entity.last_mentioned.is_some());
1317
1318        let validity = entity.temporal_validity.unwrap();
1319        assert_eq!(validity.start, -470 * 365 * 24 * 3600);
1320        assert_eq!(validity.end, -399 * 365 * 24 * 3600);
1321    }
1322
1323    #[test]
1324    fn test_entity_temporal_serialization() {
1325        let entity = Entity::new(
1326            EntityId::new("test".to_string()),
1327            "Test Entity".to_string(),
1328            "TEST".to_string(),
1329            0.8,
1330        )
1331        .with_temporal_validity(100, 200);
1332
1333        let json = serde_json::to_string(&entity).unwrap();
1334        let deserialized: Entity = serde_json::from_str(&json).unwrap();
1335
1336        assert_eq!(deserialized.name, "Test Entity");
1337        assert!(deserialized.temporal_validity.is_some());
1338
1339        let validity = deserialized.temporal_validity.unwrap();
1340        assert_eq!(validity.start, 100);
1341        assert_eq!(validity.end, 200);
1342    }
1343
1344    #[test]
1345    fn test_relationship_with_temporal_type() {
1346        let rel = Relationship::new(
1347            EntityId::new("socrates".to_string()),
1348            EntityId::new("plato".to_string()),
1349            "TAUGHT".to_string(),
1350            0.9,
1351        )
1352        .with_temporal_type(crate::graph::temporal::TemporalRelationType::Caused)
1353        .with_temporal_range(100, 200);
1354
1355        assert!(rel.temporal_type.is_some());
1356        assert!(rel.temporal_range.is_some());
1357        assert!(rel.causal_strength.is_some());
1358
1359        let temporal_type = rel.temporal_type.unwrap();
1360        assert_eq!(
1361            temporal_type,
1362            crate::graph::temporal::TemporalRelationType::Caused
1363        );
1364
1365        // Auto-set causal strength for causal types
1366        let strength = rel.causal_strength.unwrap();
1367        assert_eq!(strength, 0.9); // Default strength for Caused
1368    }
1369
1370    #[test]
1371    fn test_relationship_with_causal_strength() {
1372        let rel = Relationship::new(
1373            EntityId::new("a".to_string()),
1374            EntityId::new("b".to_string()),
1375            "INFLUENCED".to_string(),
1376            0.8,
1377        )
1378        .with_temporal_type(crate::graph::temporal::TemporalRelationType::Enabled)
1379        .with_causal_strength(0.75);
1380
1381        assert!(rel.causal_strength.is_some());
1382        assert_eq!(rel.causal_strength.unwrap(), 0.75);
1383    }
1384
1385    #[test]
1386    fn test_relationship_temporal_serialization() {
1387        let rel = Relationship::new(
1388            EntityId::new("source".to_string()),
1389            EntityId::new("target".to_string()),
1390            "CAUSED".to_string(),
1391            0.9,
1392        )
1393        .with_temporal_type(crate::graph::temporal::TemporalRelationType::Caused)
1394        .with_temporal_range(100, 200)
1395        .with_causal_strength(0.95);
1396
1397        let json = serde_json::to_string(&rel).unwrap();
1398        let deserialized: Relationship = serde_json::from_str(&json).unwrap();
1399
1400        assert_eq!(deserialized.relation_type, "CAUSED");
1401        assert!(deserialized.temporal_type.is_some());
1402        assert!(deserialized.temporal_range.is_some());
1403        assert!(deserialized.causal_strength.is_some());
1404
1405        let temporal_type = deserialized.temporal_type.unwrap();
1406        assert_eq!(
1407            temporal_type,
1408            crate::graph::temporal::TemporalRelationType::Caused
1409        );
1410
1411        let range = deserialized.temporal_range.unwrap();
1412        assert_eq!(range.start, 100);
1413        assert_eq!(range.end, 200);
1414
1415        assert_eq!(deserialized.causal_strength.unwrap(), 0.95);
1416    }
1417
1418    #[test]
1419    fn test_entity_backward_compatibility() {
1420        // Test that entities without temporal fields still work
1421        let entity = Entity::new(
1422            EntityId::new("test".to_string()),
1423            "Test".to_string(),
1424            "TEST".to_string(),
1425            0.9,
1426        );
1427
1428        assert!(entity.first_mentioned.is_none());
1429        assert!(entity.last_mentioned.is_none());
1430        assert!(entity.temporal_validity.is_none());
1431
1432        // Serialization should skip None fields
1433        let json = serde_json::to_string(&entity).unwrap();
1434        assert!(!json.contains("first_mentioned"));
1435        assert!(!json.contains("last_mentioned"));
1436        assert!(!json.contains("temporal_validity"));
1437    }
1438
1439    #[test]
1440    fn test_relationship_backward_compatibility() {
1441        // Test that relationships without temporal fields still work
1442        let rel = Relationship::new(
1443            EntityId::new("a".to_string()),
1444            EntityId::new("b".to_string()),
1445            "RELATED_TO".to_string(),
1446            0.8,
1447        );
1448
1449        assert!(rel.temporal_type.is_none());
1450        assert!(rel.temporal_range.is_none());
1451        assert!(rel.causal_strength.is_none());
1452
1453        // Serialization should skip None fields
1454        let json = serde_json::to_string(&rel).unwrap();
1455        assert!(!json.contains("temporal_type"));
1456        assert!(!json.contains("temporal_range"));
1457        assert!(!json.contains("causal_strength"));
1458    }
1459}