use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
use crate::core::interning::{GLOBAL_INTERNER, InternedString};
use crate::experimental::temporal_narrative::NarrativeGenerator;
use std::fmt::Write;
pub struct GraphContextBuilder<'a> {
db: &'a AletheiaDB,
center_node: NodeId,
history_limit: usize,
neighbor_limit: usize,
}
impl<'a> GraphContextBuilder<'a> {
pub fn new(db: &'a AletheiaDB, center_node: NodeId) -> Self {
Self {
db,
center_node,
history_limit: 5,
neighbor_limit: 10,
}
}
pub fn with_history_limit(mut self, limit: usize) -> Self {
self.history_limit = limit;
self
}
pub fn with_neighbor_limit(mut self, limit: usize) -> Self {
self.neighbor_limit = limit;
self
}
fn resolve(s: InternedString) -> String {
GLOBAL_INTERNER
.resolve_with(s, |s| s.to_string())
.unwrap_or_else(|| format!("<interned:{}>", s.as_u32()))
}
pub fn build(&self) -> Result<String> {
let mut output = String::new();
let node = self.db.get_node(self.center_node)?;
let label = Self::resolve(node.label);
writeln!(
&mut output,
"# Node Context: {} ({})",
self.center_node.as_u64(),
label
)
.unwrap();
writeln!(&mut output, "\n## Properties").unwrap();
if node.properties.is_empty() {
writeln!(&mut output, "- (No properties)").unwrap();
} else {
let mut props: Vec<_> = node.properties.iter().collect();
props.sort_by_key(|(k, _)| *k);
for (key_id, val) in props {
let key = Self::resolve(*key_id);
writeln!(&mut output, "- {}: {}", key, val).unwrap();
}
}
writeln!(&mut output, "\n## Evolution").unwrap();
let generator = NarrativeGenerator::new(self.db);
match generator.generate_node_narrative(self.center_node) {
Ok(events) => {
if events.is_empty() {
writeln!(&mut output, "- No history available.").unwrap();
} else {
for event in events.iter().rev().take(self.history_limit) {
writeln!(
&mut output,
"- {} (v{}): {}",
event.timestamp, event.version_number, event.description
)
.unwrap();
for change in &event.changes {
writeln!(&mut output, " - {}", change).unwrap();
}
}
if events.len() > self.history_limit {
writeln!(
&mut output,
"- ... ({} more versions)",
events.len() - self.history_limit
)
.unwrap();
}
}
}
Err(e) => {
writeln!(&mut output, "- Error retrieving history: {}", e).unwrap();
}
}
writeln!(&mut output, "\n## Neighborhood").unwrap();
let edges = self.db.get_outgoing_edges(self.center_node);
if edges.is_empty() {
writeln!(&mut output, "- (No outgoing edges)").unwrap();
} else {
writeln!(
&mut output,
"{} outgoing edges (showing max {}):",
edges.len(),
self.neighbor_limit
)
.unwrap();
for edge_id in edges.iter().take(self.neighbor_limit) {
if let Ok(edge) = self.db.get_edge(*edge_id) {
let edge_label = Self::resolve(edge.label);
let target_desc = if let Ok(target_node) = self.db.get_node(edge.target) {
format!(
"{} ({})",
Self::resolve(target_node.label),
edge.target.as_u64()
)
} else {
format!("Node {}", edge.target.as_u64())
};
writeln!(&mut output, "- {} -> {}", edge_label, target_desc).unwrap();
if !edge.properties.is_empty() {
let mut props_str: Vec<String> = edge
.properties
.iter()
.map(|(k, v)| format!("{}: {}", Self::resolve(*k), v))
.collect();
props_str.sort();
writeln!(
&mut output,
" - Properties: {{ {} }}",
props_str.join(", ")
)
.unwrap();
}
}
}
}
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::transaction::WriteOps;
use crate::core::property::PropertyMapBuilder;
#[test]
fn test_graph_context_generation() {
let db = AletheiaDB::new().unwrap();
let props1 = PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("role", "Engineer")
.build();
let node_a = db.create_node("Person", props1).unwrap();
db.write(|tx| {
let props2 = PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("role", "Senior Engineer")
.build();
tx.update_node(node_a, props2)
})
.unwrap();
let props_b = PropertyMapBuilder::new()
.insert("name", "Gallifrey Inc")
.build();
let node_b = db.create_node("Company", props_b).unwrap();
let props_edge = PropertyMapBuilder::new().insert("since", 2020i64).build();
db.create_edge(node_a, node_b, "WORKS_AT", props_edge)
.unwrap();
let context = GraphContextBuilder::new(&db, node_a)
.with_history_limit(5)
.build()
.unwrap();
println!("{}", context);
assert!(context.contains("# Node Context:"));
assert!(context.contains("Person"));
assert!(context.contains("name: \"Alice\""));
assert!(context.contains("role: \"Senior Engineer\""));
assert!(context.contains("## Evolution"));
assert!(context.contains("updated properties")); assert!(
context
.contains("Modified property 'role' from '\"Engineer\"' to '\"Senior Engineer\"'")
);
assert!(context.contains("## Neighborhood"));
assert!(context.contains("WORKS_AT -> Company"));
assert!(context.contains("since: 2020"));
}
}