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/// ```rust
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        tracing::info!("Knowledge graph saved to {file_path}");
767
768        Ok(())
769    }
770
771    /// Find entities by name (case-insensitive partial match)
772    pub fn find_entities_by_name(&self, name: &str) -> impl Iterator<Item = &Entity> {
773        let name_lower = name.to_lowercase();
774        self.entities()
775            .filter(move |entity| entity.name.to_lowercase().contains(&name_lower))
776    }
777
778    /// Get entity by ID (string version for compatibility)
779    pub fn get_entity_by_id(&self, id: &str) -> Option<&Entity> {
780        let entity_id = EntityId::new(id.to_string());
781        self.get_entity(&entity_id)
782    }
783
784    /// Get entity relationships
785    pub fn get_entity_relationships(&self, entity_id: &str) -> impl Iterator<Item = &Relationship> {
786        let entity_id = EntityId::new(entity_id.to_string());
787        if let Some(&node_idx) = self.entity_index.get(&entity_id) {
788            self.graph
789                .edges(node_idx)
790                .map(|edge| edge.weight())
791                .collect::<Vec<_>>()
792                .into_iter()
793        } else {
794            Vec::new().into_iter()
795        }
796    }
797
798    /// Find relationship path between two entities (simplified BFS)
799    pub fn find_relationship_path(
800        &self,
801        entity1: &str,
802        entity2: &str,
803        _max_depth: usize,
804    ) -> Vec<String> {
805        let entity1_id = EntityId::new(entity1.to_string());
806        let entity2_id = EntityId::new(entity2.to_string());
807
808        let node1 = self.entity_index.get(&entity1_id);
809        let node2 = self.entity_index.get(&entity2_id);
810
811        if let (Some(&start), Some(&end)) = (node1, node2) {
812            // Simple path finding - just check direct connections for now
813            use petgraph::visit::EdgeRef;
814            for edge in self.graph.edges(start) {
815                if edge.target() == end {
816                    return vec![edge.weight().relation_type.clone()];
817                }
818            }
819        }
820
821        Vec::new() // No path found or entities don't exist
822    }
823
824    /// Build PageRank calculator from current graph structure
825    /// Only available when pagerank feature is enabled
826    #[cfg(feature = "pagerank")]
827    pub fn build_pagerank_calculator(
828        &self,
829    ) -> Result<crate::graph::pagerank::PersonalizedPageRank> {
830        let config = crate::graph::pagerank::PageRankConfig::default();
831        let (adjacency_matrix, node_mapping, reverse_mapping) = self.build_adjacency_matrix()?;
832
833        Ok(crate::graph::pagerank::PersonalizedPageRank::new(
834            config,
835            adjacency_matrix,
836            node_mapping,
837            reverse_mapping,
838        ))
839    }
840
841    /// Build adjacency matrix for PageRank calculations
842    /// Only available when pagerank feature is enabled
843    #[cfg(feature = "pagerank")]
844    fn build_adjacency_matrix(&self) -> Result<AdjacencyMatrixResult> {
845        let nodes: Vec<EntityId> = self.entities().map(|e| e.id.clone()).collect();
846        let node_mapping: HashMap<EntityId, usize> = nodes
847            .iter()
848            .enumerate()
849            .map(|(i, id)| (id.clone(), i))
850            .collect();
851        let reverse_mapping: HashMap<usize, EntityId> = nodes
852            .iter()
853            .enumerate()
854            .map(|(i, id)| (i, id.clone()))
855            .collect();
856
857        // Build sparse adjacency matrix from relationships
858        let mut row_indices = Vec::new();
859        let mut col_indices = Vec::new();
860        let mut values = Vec::new();
861
862        for relationship in self.get_all_relationships() {
863            if let (Some(&from_idx), Some(&to_idx)) = (
864                node_mapping.get(&relationship.source),
865                node_mapping.get(&relationship.target),
866            ) {
867                row_indices.push(from_idx);
868                col_indices.push(to_idx);
869                values.push(relationship.confidence as f64);
870            }
871        }
872
873        let matrix = if row_indices.is_empty() {
874            // Create an empty matrix if no relationships
875            sprs::CsMat::zero((nodes.len(), nodes.len()))
876        } else {
877            // Build using triplet matrix first, then convert to CSR
878            let mut triplet_mat = sprs::TriMat::new((nodes.len(), nodes.len()));
879            for ((row, col), val) in row_indices
880                .into_iter()
881                .zip(col_indices.into_iter())
882                .zip(values.into_iter())
883            {
884                triplet_mat.add_triplet(row, col, val);
885            }
886            triplet_mat.to_csr()
887        };
888
889        Ok((matrix, node_mapping, reverse_mapping))
890    }
891
892    /// Count entities in the graph
893    pub fn entity_count(&self) -> usize {
894        self.entities().count()
895    }
896
897    /// Count relationships in the graph
898    pub fn relationship_count(&self) -> usize {
899        self.get_all_relationships().len()
900    }
901
902    /// Count documents in the graph
903    pub fn document_count(&self) -> usize {
904        self.documents().count()
905    }
906
907    /// Get all relationships as an iterator
908    pub fn relationships(&self) -> impl Iterator<Item = &Relationship> {
909        self.graph.edge_weights()
910    }
911
912    /// Clear all entities and relationships while preserving documents and chunks
913    ///
914    /// This is useful for rebuilding the graph from scratch without reloading documents.
915    pub fn clear_entities_and_relationships(&mut self) {
916        self.graph.clear();
917        self.entity_index.clear();
918        // Note: documents and chunks are preserved
919    }
920
921    /// Calculate cosine similarity between two embedding vectors
922    ///
923    /// # Arguments
924    ///
925    /// * `a` - First embedding vector
926    /// * `b` - Second embedding vector
927    ///
928    /// # Returns
929    ///
930    /// Cosine similarity score (range: -1.0 to 1.0, typically 0.0 to 1.0 for embeddings)
931    fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
932        if a.len() != b.len() || a.is_empty() {
933            return 0.0;
934        }
935
936        let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
937        let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
938        let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
939
940        if norm_a == 0.0 || norm_b == 0.0 {
941            return 0.0;
942        }
943
944        dot_product / (norm_a * norm_b)
945    }
946
947    /// Calculate temporal relevance score for a temporal range
948    ///
949    /// Gives higher scores to:
950    /// - Recent events (closer to current time)
951    /// - Events with known temporal bounds
952    ///
953    /// # Arguments
954    ///
955    /// * `range` - The temporal range to score
956    ///
957    /// # Returns
958    ///
959    /// Temporal relevance boost (0.0-0.3 range)
960    fn calculate_temporal_relevance(range: &crate::graph::temporal::TemporalRange) -> f32 {
961        use std::time::{SystemTime, UNIX_EPOCH};
962
963        // Get current timestamp
964        let now = SystemTime::now()
965            .duration_since(UNIX_EPOCH)
966            .unwrap_or_default()
967            .as_secs() as i64;
968
969        // Calculate recency: how close is the event to now?
970        let mid_point = (range.start + range.end) / 2;
971        let years_ago = ((now - mid_point) / (365 * 24 * 3600)).abs();
972
973        // Decay function: more recent = higher score
974        // Events in the past 10 years get max boost (0.3)
975        // Older events get gradually less boost
976        let recency_boost = if years_ago <= 10 {
977            0.3
978        } else if years_ago <= 50 {
979            0.3 * (1.0 - (years_ago - 10) as f32 / 40.0)
980        } else if years_ago <= 200 {
981            0.1 * (1.0 - (years_ago - 50) as f32 / 150.0)
982        } else {
983            0.05 // Ancient events still get small boost for having temporal info
984        };
985
986        recency_boost.max(0.0)
987    }
988
989    /// Calculate dynamic weight for a relationship based on query context (Phase 2.2)
990    ///
991    /// Combines multiple factors:
992    /// - Base weight: relationship.confidence
993    /// - Semantic boost: similarity between relationship and query embeddings
994    /// - Temporal boost: relevance of temporal range
995    /// - Conceptual boost: matching query concepts in relationship type
996    ///
997    /// # Arguments
998    ///
999    /// * `relationship` - The relationship to weight
1000    /// * `query_embedding` - Optional embedding vector for the query
1001    /// * `query_concepts` - Concepts extracted from the query
1002    ///
1003    /// # Returns
1004    ///
1005    /// Dynamic weight value (typically 0.0-2.0, can be higher with strong matches)
1006    pub fn dynamic_weight(
1007        &self,
1008        relationship: &Relationship,
1009        query_embedding: Option<&[f32]>,
1010        query_concepts: &[String],
1011    ) -> f32 {
1012        let base_weight = relationship.confidence;
1013
1014        // Semantic boost: similarity between relationship and query
1015        let semantic_boost = if let (Some(rel_emb), Some(query_emb)) =
1016            (relationship.embedding.as_deref(), query_embedding)
1017        {
1018            Self::cosine_similarity(rel_emb, query_emb).max(0.0)
1019        } else {
1020            0.0
1021        };
1022
1023        // Temporal boost: relationships with recent or relevant temporal ranges
1024        let temporal_boost = if let Some(tr) = &relationship.temporal_range {
1025            Self::calculate_temporal_relevance(tr)
1026        } else {
1027            0.0
1028        };
1029
1030        // Conceptual boost: relationships whose type matches query concepts
1031        let concept_boost = query_concepts
1032            .iter()
1033            .filter(|c| {
1034                relationship
1035                    .relation_type
1036                    .to_lowercase()
1037                    .contains(&c.to_lowercase())
1038            })
1039            .count() as f32
1040            * 0.15; // 15% boost per matching concept
1041
1042        // Causal boost: causal relationships get extra weight
1043        let causal_boost = if let Some(strength) = relationship.causal_strength {
1044            strength * 0.2 // Up to 20% boost for strong causal links
1045        } else {
1046            0.0
1047        };
1048
1049        // Combine all factors
1050        base_weight * (1.0 + semantic_boost + temporal_boost + concept_boost + causal_boost)
1051    }
1052
1053    /// Convert KnowledgeGraph to petgraph format for Leiden clustering
1054    /// Returns a graph with entity names as nodes and relationship confidences as edge weights
1055    /// Only available when leiden feature is enabled
1056    #[cfg(feature = "leiden")]
1057    pub fn to_leiden_graph(&self) -> petgraph::Graph<String, f32, petgraph::Undirected> {
1058        let mut graph = Graph::new_undirected();
1059        let mut node_map = HashMap::new();
1060
1061        // Add nodes (entities) - use entity name as node label
1062        for entity in self.entities() {
1063            let idx = graph.add_node(entity.name.clone());
1064            node_map.insert(entity.id.clone(), idx);
1065        }
1066
1067        // Add edges (relationships) with confidence as weight
1068        for rel in self.get_all_relationships() {
1069            if let (Some(&src), Some(&tgt)) = (node_map.get(&rel.source), node_map.get(&rel.target))
1070            {
1071                graph.add_edge(src, tgt, rel.confidence);
1072            }
1073        }
1074
1075        graph
1076    }
1077
1078    /// Detect hierarchical communities in the entity graph using Leiden algorithm
1079    /// Only available when leiden feature is enabled
1080    ///
1081    /// # Arguments
1082    /// * `config` - Leiden algorithm configuration
1083    ///
1084    /// # Returns
1085    /// HierarchicalCommunities structure with community assignments at each level
1086    ///
1087    /// # Example
1088    /// ```no_run
1089    /// use graphrag_core::{KnowledgeGraph, graph::LeidenConfig};
1090    ///
1091    /// let graph = KnowledgeGraph::new();
1092    /// // ... build graph ...
1093    ///
1094    /// let config = LeidenConfig {
1095    ///     max_cluster_size: 10,
1096    ///     resolution: 1.0,
1097    ///     ..Default::default()
1098    /// };
1099    ///
1100    /// let communities = graph.detect_hierarchical_communities(config).unwrap();
1101    /// ```
1102    #[cfg(feature = "leiden")]
1103    pub fn detect_hierarchical_communities(
1104        &self,
1105        config: crate::graph::leiden::LeidenConfig,
1106    ) -> Result<crate::graph::leiden::HierarchicalCommunities> {
1107        use crate::graph::leiden::LeidenCommunityDetector;
1108
1109        // Convert to Leiden-compatible graph format
1110        let leiden_graph = self.to_leiden_graph();
1111
1112        // Create detector and run clustering
1113        let detector = LeidenCommunityDetector::new(config);
1114        let mut communities = detector.detect_communities(&leiden_graph)?;
1115
1116        // Enrich with entity metadata
1117        communities.entity_mapping = Some(self.build_entity_mapping());
1118
1119        Ok(communities)
1120    }
1121
1122    /// Build mapping from entity names to entity metadata
1123    /// Used to enrich hierarchical communities with entity information
1124    #[cfg(feature = "leiden")]
1125    fn build_entity_mapping(&self) -> HashMap<String, crate::graph::leiden::EntityMetadata> {
1126        use crate::graph::leiden::EntityMetadata;
1127
1128        self.entities()
1129            .map(|entity| {
1130                let metadata = EntityMetadata {
1131                    id: entity.id.to_string(),
1132                    name: entity.name.clone(),
1133                    entity_type: entity.entity_type.clone(),
1134                    confidence: entity.confidence,
1135                    mention_count: entity.mentions.len(),
1136                };
1137                (entity.name.clone(), metadata)
1138            })
1139            .collect()
1140    }
1141
1142    /// Build hierarchical organization of relationships (Phase 3.1)
1143    ///
1144    /// Creates multi-level clusters of relationships using recursive community detection
1145    /// and LLM-generated summaries for efficient retrieval.
1146    ///
1147    /// # Arguments
1148    /// * `num_levels` - Number of hierarchy levels to create (default: 3)
1149    /// * `ollama_client` - Optional Ollama client for generating cluster summaries
1150    ///
1151    /// # Returns
1152    /// Result containing the built hierarchy or an error
1153    ///
1154    /// # Example
1155    /// ```no_run
1156    /// use graphrag_core::{KnowledgeGraph, ollama::OllamaClient};
1157    ///
1158    /// let mut graph = KnowledgeGraph::new();
1159    /// // ... build graph ...
1160    ///
1161    /// let ollama = OllamaClient::new("http://localhost:11434", "llama3.2");
1162    /// graph.build_relationship_hierarchy(3, Some(ollama)).await.unwrap();
1163    /// ```
1164    #[cfg(feature = "async")]
1165    pub async fn build_relationship_hierarchy(
1166        &mut self,
1167        num_levels: usize,
1168        ollama_client: Option<crate::ollama::OllamaClient>,
1169    ) -> Result<()> {
1170        use crate::graph::hierarchical_relationships::HierarchyBuilder;
1171
1172        let builder = HierarchyBuilder::from_graph(self).with_num_levels(num_levels);
1173
1174        let builder = if let Some(client) = ollama_client {
1175            builder.with_ollama_client(client)
1176        } else {
1177            builder
1178        };
1179
1180        let hierarchy = builder.build().await?;
1181        self.relationship_hierarchy = Some(hierarchy);
1182
1183        Ok(())
1184    }
1185}
1186
1187impl Default for KnowledgeGraph {
1188    fn default() -> Self {
1189        Self::new()
1190    }
1191}
1192
1193impl Document {
1194    /// Create a new document
1195    pub fn new(id: DocumentId, title: String, content: String) -> Self {
1196        Self {
1197            id,
1198            title,
1199            content,
1200            metadata: IndexMap::new(),
1201            chunks: Vec::new(),
1202        }
1203    }
1204
1205    /// Add metadata to the document
1206    pub fn with_metadata(mut self, key: String, value: String) -> Self {
1207        self.metadata.insert(key, value);
1208        self
1209    }
1210
1211    /// Add chunks to the document
1212    pub fn with_chunks(mut self, chunks: Vec<TextChunk>) -> Self {
1213        self.chunks = chunks;
1214        self
1215    }
1216}
1217
1218impl TextChunk {
1219    /// Create a new text chunk
1220    pub fn new(
1221        id: ChunkId,
1222        document_id: DocumentId,
1223        content: String,
1224        start_offset: usize,
1225        end_offset: usize,
1226    ) -> Self {
1227        Self {
1228            id,
1229            document_id,
1230            content,
1231            start_offset,
1232            end_offset,
1233            embedding: None,
1234            entities: Vec::new(),
1235            metadata: ChunkMetadata::default(),
1236        }
1237    }
1238
1239    /// Add an embedding to the chunk
1240    pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
1241        self.embedding = Some(embedding);
1242        self
1243    }
1244
1245    /// Add entities to the chunk
1246    pub fn with_entities(mut self, entities: Vec<EntityId>) -> Self {
1247        self.entities = entities;
1248        self
1249    }
1250
1251    /// Add metadata to the chunk
1252    pub fn with_metadata(mut self, metadata: ChunkMetadata) -> Self {
1253        self.metadata = metadata;
1254        self
1255    }
1256}
1257
1258impl Entity {
1259    /// Create a new entity
1260    pub fn new(id: EntityId, name: String, entity_type: String, confidence: f32) -> Self {
1261        Self {
1262            id,
1263            name,
1264            entity_type,
1265            confidence,
1266            mentions: Vec::new(),
1267            embedding: None,
1268            // Temporal fields default to None (backward compatible)
1269            first_mentioned: None,
1270            last_mentioned: None,
1271            temporal_validity: None,
1272        }
1273    }
1274
1275    /// Add mentions to the entity
1276    pub fn with_mentions(mut self, mentions: Vec<EntityMention>) -> Self {
1277        self.mentions = mentions;
1278        self
1279    }
1280
1281    /// Add an embedding to the entity
1282    pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
1283        self.embedding = Some(embedding);
1284        self
1285    }
1286
1287    /// Set temporal validity range for this entity
1288    pub fn with_temporal_validity(mut self, start: i64, end: i64) -> Self {
1289        self.temporal_validity = Some(crate::graph::temporal::TemporalRange::new(start, end));
1290        self
1291    }
1292
1293    /// Set mention timestamps for this entity
1294    pub fn with_mention_times(mut self, first: i64, last: i64) -> Self {
1295        self.first_mentioned = Some(first);
1296        self.last_mentioned = Some(last);
1297        self
1298    }
1299}
1300
1301#[cfg(test)]
1302mod temporal_tests {
1303    use super::*;
1304
1305    #[test]
1306    fn test_entity_with_temporal_fields() {
1307        let entity = Entity::new(
1308            EntityId::new("socrates".to_string()),
1309            "Socrates".to_string(),
1310            "PERSON".to_string(),
1311            0.9,
1312        )
1313        .with_temporal_validity(-470 * 365 * 24 * 3600, -399 * 365 * 24 * 3600) // 470-399 BC
1314        .with_mention_times(1000, 2000);
1315
1316        assert_eq!(entity.name, "Socrates");
1317        assert!(entity.temporal_validity.is_some());
1318        assert!(entity.first_mentioned.is_some());
1319        assert!(entity.last_mentioned.is_some());
1320
1321        let validity = entity.temporal_validity.unwrap();
1322        assert_eq!(validity.start, -470 * 365 * 24 * 3600);
1323        assert_eq!(validity.end, -399 * 365 * 24 * 3600);
1324    }
1325
1326    #[test]
1327    fn test_entity_temporal_serialization() {
1328        let entity = Entity::new(
1329            EntityId::new("test".to_string()),
1330            "Test Entity".to_string(),
1331            "TEST".to_string(),
1332            0.8,
1333        )
1334        .with_temporal_validity(100, 200);
1335
1336        let json = serde_json::to_string(&entity).unwrap();
1337        let deserialized: Entity = serde_json::from_str(&json).unwrap();
1338
1339        assert_eq!(deserialized.name, "Test Entity");
1340        assert!(deserialized.temporal_validity.is_some());
1341
1342        let validity = deserialized.temporal_validity.unwrap();
1343        assert_eq!(validity.start, 100);
1344        assert_eq!(validity.end, 200);
1345    }
1346
1347    #[test]
1348    fn test_relationship_with_temporal_type() {
1349        let rel = Relationship::new(
1350            EntityId::new("socrates".to_string()),
1351            EntityId::new("plato".to_string()),
1352            "TAUGHT".to_string(),
1353            0.9,
1354        )
1355        .with_temporal_type(crate::graph::temporal::TemporalRelationType::Caused)
1356        .with_temporal_range(100, 200);
1357
1358        assert!(rel.temporal_type.is_some());
1359        assert!(rel.temporal_range.is_some());
1360        assert!(rel.causal_strength.is_some());
1361
1362        let temporal_type = rel.temporal_type.unwrap();
1363        assert_eq!(
1364            temporal_type,
1365            crate::graph::temporal::TemporalRelationType::Caused
1366        );
1367
1368        // Auto-set causal strength for causal types
1369        let strength = rel.causal_strength.unwrap();
1370        assert_eq!(strength, 0.9); // Default strength for Caused
1371    }
1372
1373    #[test]
1374    fn test_relationship_with_causal_strength() {
1375        let rel = Relationship::new(
1376            EntityId::new("a".to_string()),
1377            EntityId::new("b".to_string()),
1378            "INFLUENCED".to_string(),
1379            0.8,
1380        )
1381        .with_temporal_type(crate::graph::temporal::TemporalRelationType::Enabled)
1382        .with_causal_strength(0.75);
1383
1384        assert!(rel.causal_strength.is_some());
1385        assert_eq!(rel.causal_strength.unwrap(), 0.75);
1386    }
1387
1388    #[test]
1389    fn test_relationship_temporal_serialization() {
1390        let rel = Relationship::new(
1391            EntityId::new("source".to_string()),
1392            EntityId::new("target".to_string()),
1393            "CAUSED".to_string(),
1394            0.9,
1395        )
1396        .with_temporal_type(crate::graph::temporal::TemporalRelationType::Caused)
1397        .with_temporal_range(100, 200)
1398        .with_causal_strength(0.95);
1399
1400        let json = serde_json::to_string(&rel).unwrap();
1401        let deserialized: Relationship = serde_json::from_str(&json).unwrap();
1402
1403        assert_eq!(deserialized.relation_type, "CAUSED");
1404        assert!(deserialized.temporal_type.is_some());
1405        assert!(deserialized.temporal_range.is_some());
1406        assert!(deserialized.causal_strength.is_some());
1407
1408        let temporal_type = deserialized.temporal_type.unwrap();
1409        assert_eq!(
1410            temporal_type,
1411            crate::graph::temporal::TemporalRelationType::Caused
1412        );
1413
1414        let range = deserialized.temporal_range.unwrap();
1415        assert_eq!(range.start, 100);
1416        assert_eq!(range.end, 200);
1417
1418        assert_eq!(deserialized.causal_strength.unwrap(), 0.95);
1419    }
1420
1421    #[test]
1422    fn test_entity_backward_compatibility() {
1423        // Test that entities without temporal fields still work
1424        let entity = Entity::new(
1425            EntityId::new("test".to_string()),
1426            "Test".to_string(),
1427            "TEST".to_string(),
1428            0.9,
1429        );
1430
1431        assert!(entity.first_mentioned.is_none());
1432        assert!(entity.last_mentioned.is_none());
1433        assert!(entity.temporal_validity.is_none());
1434
1435        // Serialization should skip None fields
1436        let json = serde_json::to_string(&entity).unwrap();
1437        assert!(!json.contains("first_mentioned"));
1438        assert!(!json.contains("last_mentioned"));
1439        assert!(!json.contains("temporal_validity"));
1440    }
1441
1442    #[test]
1443    fn test_relationship_backward_compatibility() {
1444        // Test that relationships without temporal fields still work
1445        let rel = Relationship::new(
1446            EntityId::new("a".to_string()),
1447            EntityId::new("b".to_string()),
1448            "RELATED_TO".to_string(),
1449            0.8,
1450        );
1451
1452        assert!(rel.temporal_type.is_none());
1453        assert!(rel.temporal_range.is_none());
1454        assert!(rel.causal_strength.is_none());
1455
1456        // Serialization should skip None fields
1457        let json = serde_json::to_string(&rel).unwrap();
1458        assert!(!json.contains("temporal_type"));
1459        assert!(!json.contains("temporal_range"));
1460        assert!(!json.contains("causal_strength"));
1461    }
1462}