use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TemporalTriple {
pub subject: String,
pub predicate: String,
pub object: String,
pub valid_from: i64,
pub valid_to: Option<i64>,
pub confidence: f64,
}
impl TemporalTriple {
pub fn is_valid_at(&self, timestamp: i64) -> bool {
if timestamp < self.valid_from {
return false;
}
match self.valid_to {
Some(to) => timestamp < to,
None => true,
}
}
pub fn overlaps_range(&self, from: i64, to: i64) -> bool {
if let Some(t) = self.valid_to {
if t <= from {
return false;
}
}
self.valid_from < to
}
}
pub struct TemporalKnowledgeGraph {
triples: Vec<TemporalTriple>,
time_index: BTreeMap<i64, Vec<usize>>,
}
impl Default for TemporalKnowledgeGraph {
fn default() -> Self {
Self::new()
}
}
impl TemporalKnowledgeGraph {
pub fn new() -> Self {
Self {
triples: Vec::new(),
time_index: BTreeMap::new(),
}
}
pub fn insert(&mut self, triple: TemporalTriple) {
let idx = self.triples.len();
let ts = triple.valid_from;
self.triples.push(triple);
self.time_index.entry(ts).or_default().push(idx);
}
pub fn query_at(&self, timestamp: i64) -> Vec<&TemporalTriple> {
self.triples
.iter()
.filter(|t| t.is_valid_at(timestamp))
.collect()
}
pub fn query_range(&self, from: i64, to: i64) -> Vec<&TemporalTriple> {
self.triples
.iter()
.filter(|t| t.overlaps_range(from, to))
.collect()
}
pub fn entities_at(&self, timestamp: i64) -> HashSet<String> {
let mut set = HashSet::new();
for t in self.query_at(timestamp) {
set.insert(t.subject.clone());
set.insert(t.object.clone());
}
set
}
pub fn temporal_path(&self, from: &str, to: &str, at: i64) -> Option<Vec<String>> {
if from == to {
return Some(vec![from.to_string()]);
}
let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
for t in self.query_at(at) {
adj.entry(t.subject.as_str())
.or_default()
.push(t.object.as_str());
}
let mut visited: HashSet<&str> = HashSet::new();
let mut queue: VecDeque<(&str, Vec<&str>)> = VecDeque::new();
queue.push_back((from, vec![from]));
visited.insert(from);
while let Some((node, path)) = queue.pop_front() {
if let Some(neighbors) = adj.get(node) {
for &neighbor in neighbors {
if neighbor == to {
let mut result: Vec<String> = path.iter().map(|s| s.to_string()).collect();
result.push(to.to_string());
return Some(result);
}
if !visited.contains(neighbor) {
visited.insert(neighbor);
let mut new_path = path.clone();
new_path.push(neighbor);
queue.push_back((neighbor, new_path));
}
}
}
}
None
}
pub fn len(&self) -> usize {
self.triples.len()
}
pub fn is_empty(&self) -> bool {
self.triples.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityHistory {
pub entity: String,
pub events: Vec<TemporalTriple>,
pub first_seen: Option<i64>,
pub last_seen: Option<i64>,
pub relationship_count: usize,
}
pub struct TemporalGraphRag {
kg: TemporalKnowledgeGraph,
embedding_cache: HashMap<String, Vec<f32>>,
}
impl Default for TemporalGraphRag {
fn default() -> Self {
Self::new()
}
}
impl TemporalGraphRag {
pub fn new() -> Self {
Self {
kg: TemporalKnowledgeGraph::new(),
embedding_cache: HashMap::new(),
}
}
pub fn ingest_event(
&mut self,
subject: &str,
predicate: &str,
object: &str,
timestamp: i64,
confidence: f64,
) {
self.kg.insert(TemporalTriple {
subject: subject.to_string(),
predicate: predicate.to_string(),
object: object.to_string(),
valid_from: timestamp,
valid_to: None,
confidence: confidence.clamp(0.0, 1.0),
});
}
pub fn query(&self, question: &str, timestamp: i64, top_k: usize) -> Vec<TemporalTriple> {
let keywords: Vec<String> = question
.split_whitespace()
.map(|w| w.to_lowercase())
.collect();
let candidates = self.kg.query_at(timestamp);
let mut scored: Vec<(f64, &TemporalTriple)> = candidates
.into_iter()
.map(|t| {
let score = self.keyword_score(t, &keywords);
(score, t)
})
.collect();
scored.sort_by(|(sa, ta), (sb, tb)| {
sa.partial_cmp(sb)
.unwrap_or(std::cmp::Ordering::Equal)
.reverse()
.then_with(|| {
ta.confidence
.partial_cmp(&tb.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
.reverse()
})
});
scored
.into_iter()
.take(top_k)
.map(|(_, t)| t.clone())
.collect()
}
fn keyword_score(&self, triple: &TemporalTriple, keywords: &[String]) -> f64 {
if keywords.is_empty() {
return 0.0;
}
let text = format!(
"{} {} {}",
triple.subject.to_lowercase(),
triple.predicate.to_lowercase(),
triple.object.to_lowercase()
);
let matched = keywords
.iter()
.filter(|kw| text.contains(kw.as_str()))
.count();
let raw_score = matched as f64 / keywords.len() as f64;
raw_score * 0.7 + triple.confidence * 0.3
}
pub fn summarize_entity_history(&self, entity: &str) -> EntityHistory {
let events: Vec<TemporalTriple> = self
.kg
.triples
.iter()
.filter(|t| t.subject == entity || t.object == entity)
.cloned()
.collect();
let first_seen = events.iter().map(|t| t.valid_from).min();
let last_seen = events.iter().map(|t| t.valid_from).max();
let relationship_count: HashSet<String> =
events.iter().map(|t| t.predicate.clone()).collect();
let relationship_count = relationship_count.len();
EntityHistory {
entity: entity.to_string(),
events,
first_seen,
last_seen,
relationship_count,
}
}
pub fn cache_embedding(&mut self, entity: &str, embedding: Vec<f32>) {
self.embedding_cache.insert(entity.to_string(), embedding);
}
pub fn get_embedding(&self, entity: &str) -> Option<&Vec<f32>> {
self.embedding_cache.get(entity)
}
pub fn event_count(&self) -> usize {
self.kg.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_triple(s: &str, p: &str, o: &str, from: i64, to: Option<i64>) -> TemporalTriple {
TemporalTriple {
subject: s.to_string(),
predicate: p.to_string(),
object: o.to_string(),
valid_from: from,
valid_to: to,
confidence: 0.9,
}
}
#[test]
fn test_is_valid_at_within_interval() {
let t = make_triple("s", "p", "o", 100, Some(200));
assert!(t.is_valid_at(100));
assert!(t.is_valid_at(150));
assert!(!t.is_valid_at(99));
assert!(!t.is_valid_at(200)); }
#[test]
fn test_is_valid_at_no_end() {
let t = make_triple("s", "p", "o", 100, None);
assert!(t.is_valid_at(100));
assert!(t.is_valid_at(i64::MAX));
assert!(!t.is_valid_at(99));
}
#[test]
fn test_overlaps_range_full_overlap() {
let t = make_triple("s", "p", "o", 50, Some(150));
assert!(t.overlaps_range(100, 200)); }
#[test]
fn test_overlaps_range_no_overlap_before() {
let t = make_triple("s", "p", "o", 0, Some(50));
assert!(!t.overlaps_range(100, 200));
}
#[test]
fn test_overlaps_range_no_overlap_after() {
let t = make_triple("s", "p", "o", 300, None);
assert!(!t.overlaps_range(100, 200));
}
#[test]
fn test_overlaps_range_open_end() {
let t = make_triple("s", "p", "o", 50, None);
assert!(t.overlaps_range(100, 200)); }
#[test]
fn test_insert_increases_len() {
let mut kg = TemporalKnowledgeGraph::new();
assert_eq!(kg.len(), 0);
assert!(kg.is_empty());
kg.insert(make_triple("s", "p", "o", 0, None));
assert_eq!(kg.len(), 1);
assert!(!kg.is_empty());
}
#[test]
fn test_insert_updates_time_index() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("s", "p", "o", 1000, None));
assert!(kg.time_index.contains_key(&1000));
}
#[test]
fn test_query_at_returns_valid_triples() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("a", "p", "b", 0, Some(100)));
kg.insert(make_triple("c", "p", "d", 50, None));
kg.insert(make_triple("e", "p", "f", 200, None));
let valid = kg.query_at(75);
assert_eq!(valid.len(), 2);
}
#[test]
fn test_query_at_empty_graph() {
let kg = TemporalKnowledgeGraph::new();
assert!(kg.query_at(0).is_empty());
}
#[test]
fn test_query_at_excludes_expired() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("a", "p", "b", 0, Some(50)));
let valid = kg.query_at(100);
assert!(valid.is_empty());
}
#[test]
fn test_query_range_returns_overlapping() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("a", "p", "b", 0, Some(100))); kg.insert(make_triple("c", "p", "d", 80, Some(200))); kg.insert(make_triple("e", "p", "f", 200, None));
let result = kg.query_range(50, 150);
assert_eq!(result.len(), 2);
}
#[test]
fn test_query_range_no_overlap() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("a", "p", "b", 0, Some(10)));
let result = kg.query_range(100, 200);
assert!(result.is_empty());
}
#[test]
fn test_entities_at_collects_subjects_and_objects() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("Alice", "knows", "Bob", 0, None));
kg.insert(make_triple("Bob", "likes", "Carol", 0, None));
let entities = kg.entities_at(0);
assert!(entities.contains("Alice"));
assert!(entities.contains("Bob"));
assert!(entities.contains("Carol"));
}
#[test]
fn test_entities_at_respects_timestamp() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("Alice", "knows", "Bob", 0, Some(50)));
kg.insert(make_triple("Carol", "knows", "Dave", 100, None));
let entities = kg.entities_at(75);
assert!(entities.is_empty());
}
#[test]
fn test_temporal_path_direct_edge() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("A", "p", "B", 0, None));
let path = kg.temporal_path("A", "B", 0).expect("should succeed");
assert_eq!(path, vec!["A", "B"]);
}
#[test]
fn test_temporal_path_multi_hop() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("A", "p", "B", 0, None));
kg.insert(make_triple("B", "p", "C", 0, None));
kg.insert(make_triple("C", "p", "D", 0, None));
let path = kg.temporal_path("A", "D", 0).expect("should succeed");
assert_eq!(path.first().map(|s| s.as_str()), Some("A"));
assert_eq!(path.last().map(|s| s.as_str()), Some("D"));
assert!(path.len() >= 2);
}
#[test]
fn test_temporal_path_no_path() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("A", "p", "B", 0, None));
assert!(kg.temporal_path("A", "C", 0).is_none());
}
#[test]
fn test_temporal_path_same_node() {
let kg = TemporalKnowledgeGraph::new();
let path = kg.temporal_path("A", "A", 0).expect("should succeed");
assert_eq!(path, vec!["A"]);
}
#[test]
fn test_temporal_path_ignores_future_triples() {
let mut kg = TemporalKnowledgeGraph::new();
kg.insert(make_triple("A", "p", "B", 1000, None)); assert!(kg.temporal_path("A", "B", 0).is_none());
}
#[test]
fn test_ingest_event_stores_triple() {
let mut rag = TemporalGraphRag::new();
rag.ingest_event("Alice", "knows", "Bob", 1000, 0.9);
assert_eq!(rag.event_count(), 1);
}
#[test]
fn test_ingest_event_clamps_confidence() {
let mut rag = TemporalGraphRag::new();
rag.ingest_event("A", "p", "B", 0, 1.5); rag.ingest_event("C", "p", "D", 0, -0.5); let triples = rag.kg.query_at(0);
for t in triples {
assert!(t.confidence >= 0.0 && t.confidence <= 1.0);
}
}
#[test]
fn test_query_returns_relevant_triple() {
let mut rag = TemporalGraphRag::new();
rag.ingest_event("Apple", "releases", "iPhone", 1000, 0.9);
rag.ingest_event("Google", "releases", "Pixel", 1000, 0.8);
let results = rag.query("Apple iPhone", 1000, 5);
assert!(!results.is_empty());
assert_eq!(results[0].subject, "Apple");
}
#[test]
fn test_query_respects_top_k() {
let mut rag = TemporalGraphRag::new();
for i in 0..10 {
rag.ingest_event(&format!("S{i}"), "p", &format!("O{i}"), 0, 0.9);
}
let results = rag.query("any", 0, 3);
assert!(results.len() <= 3);
}
#[test]
fn test_query_respects_timestamp() {
let mut rag = TemporalGraphRag::new();
rag.ingest_event("Past", "event", "X", 0, 0.9);
let results = rag.query("Past event X", -1, 5);
assert!(results.is_empty());
}
#[test]
fn test_query_empty_graph_returns_empty() {
let rag = TemporalGraphRag::new();
let results = rag.query("anything", 0, 5);
assert!(results.is_empty());
}
#[test]
fn test_summarize_entity_history_basic() {
let mut rag = TemporalGraphRag::new();
rag.ingest_event("Alice", "knows", "Bob", 100, 0.9);
rag.ingest_event("Alice", "likes", "Carol", 200, 0.8);
rag.ingest_event("Dave", "knows", "Alice", 300, 0.7);
let history = rag.summarize_entity_history("Alice");
assert_eq!(history.entity, "Alice");
assert_eq!(history.events.len(), 3);
assert_eq!(history.first_seen, Some(100));
assert_eq!(history.last_seen, Some(300));
assert_eq!(history.relationship_count, 2); }
#[test]
fn test_summarize_entity_history_unknown_entity() {
let rag = TemporalGraphRag::new();
let history = rag.summarize_entity_history("Unknown");
assert!(history.events.is_empty());
assert!(history.first_seen.is_none());
assert!(history.last_seen.is_none());
assert_eq!(history.relationship_count, 0);
}
#[test]
fn test_embedding_cache_roundtrip() {
let mut rag = TemporalGraphRag::new();
let embedding = vec![0.1_f32, 0.2, 0.3];
rag.cache_embedding("Alice", embedding.clone());
assert_eq!(rag.get_embedding("Alice"), Some(&embedding));
assert!(rag.get_embedding("Bob").is_none());
}
}