Skip to main content

mnemo_graph/
lib.rs

1//! Bitemporal graph layer for Mnemo.
2//!
3//! Inspired by Graphiti ([repo](https://github.com/getzep/graphiti),
4//! [paper](https://arxiv.org/abs/2501.13956)). The model is the same:
5//! every edge carries `valid_from` / `valid_to` (when the *fact* is
6//! true in the world) plus `recorded_at` (when the system saw it),
7//! so historical queries can ask "what did we believe at time T?"
8//! without losing later corrections.
9//!
10//! ```text
11//! valid_from              valid_to (None = still true)
12//!     ^                       ^
13//!     |   fact validity       |
14//!     +-----------------------+
15//!     |
16//!     +-- recorded_at (when we wrote the row)
17//! ```
18//!
19//! Today this crate ships:
20//!
21//! 1. The [`TemporalEdge`] type and a [`GraphStore`] async trait.
22//! 2. A DuckDB-backed [`DuckGraphStore`] that creates `graph_nodes`
23//!    and `graph_edges` tables on first use and supports the round-trip
24//!    + bitemporal `as_of` walk needed by retrieval.
25//! 3. [`graph_expand`] — bounded BFS that respects `as_of` filtering
26//!    and a maximum depth.
27//!
28//! The LLM-driven [`TemporalEdge::extract`] path is feature-gated under
29//! `graph-extract` and currently returns an empty `Vec`. A real
30//! extractor lands in v0.4.0 final once the prompt + ICL examples are
31//! tuned.
32
33pub mod extract;
34pub mod model;
35pub mod store;
36
37pub use crate::model::TemporalEdge;
38pub use crate::store::{GraphStore, duckdb::DuckGraphStore};
39
40use chrono::{DateTime, Utc};
41use std::collections::{HashSet, VecDeque};
42use uuid::Uuid;
43
44use crate::store::Result;
45
46/// Bounded BFS from `seed` that respects bitemporal validity at
47/// `as_of` and a max walk depth.
48///
49/// Returns every UUID reachable through edges whose
50/// `valid_from <= as_of < valid_to.unwrap_or(MAX)`. Self-loops are
51/// dropped. The seed is included in the returned set unless the
52/// caller filters it out themselves.
53pub async fn graph_expand(
54    store: &dyn GraphStore,
55    seed: Uuid,
56    depth: u8,
57    as_of: DateTime<Utc>,
58) -> Result<Vec<Uuid>> {
59    let mut visited: HashSet<Uuid> = HashSet::new();
60    let mut frontier: VecDeque<(Uuid, u8)> = VecDeque::new();
61    frontier.push_back((seed, 0));
62    visited.insert(seed);
63
64    while let Some((node, d)) = frontier.pop_front() {
65        if d == depth {
66            continue;
67        }
68        for edge in store.outgoing_at(node, as_of).await? {
69            if edge.dst == node {
70                continue;
71            }
72            if visited.insert(edge.dst) {
73                frontier.push_back((edge.dst, d + 1));
74            }
75        }
76    }
77    Ok(visited.into_iter().collect())
78}