#![allow(clippy::collapsible_if)]
use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
#[derive(Debug, Clone)]
pub struct Inspiration {
pub centroid: Vec<f32>,
pub nearby_concepts: Vec<(NodeId, f32)>,
pub novelty_score: f32,
pub coherence_score: f32,
}
pub struct Muse<'a> {
#[allow(dead_code)]
db: &'a AletheiaDB,
}
#[cfg(feature = "semantic-reasoning")]
impl<'a> Muse<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn inspire(
&self,
nodes: &[NodeId],
property: Option<&str>,
limit: usize,
) -> Result<Option<Inspiration>> {
if nodes.is_empty() {
return Ok(None);
}
let prop_name = if let Some(p) = property {
p.to_string()
} else {
let indexes = self.db.list_vector_indexes();
if let Some(info) = indexes.first() {
info.property_name.clone()
} else {
return Ok(None); }
};
let mut vectors = Vec::with_capacity(nodes.len());
for &node_id in nodes {
let node = self.db.get_node(node_id)?;
#[allow(clippy::collapsible_if)]
if let Some(val) = node.properties.get(&prop_name) {
if let Some(vec) = val.as_vector() {
vectors.push(vec.to_vec());
}
}
}
if vectors.is_empty() {
return Ok(None);
}
let dim = vectors[0].len();
let mut centroid = vec![0.0; dim];
for vec in &vectors {
if vec.len() != dim {
continue; }
for (i, val) in vec.iter().enumerate() {
centroid[i] += val;
}
}
let centroid = crate::core::vector::ops::normalize(¢roid);
let neighbors = self.db.search_vectors_in(&prop_name, ¢roid, limit)?;
let max_sim = neighbors.first().map(|(_, score)| *score).unwrap_or(-1.0);
let novelty_score = 1.0 - max_sim.max(0.0);
let mut total_sim = 0.0;
let mut count = 0;
for vec in &vectors {
if let Ok(sim) = crate::core::vector::ops::cosine_similarity(¢roid, vec) {
total_sim += sim;
count += 1;
}
}
let coherence_score = if count > 0 {
total_sim / count as f32
} else {
0.0
};
Ok(Some(Inspiration {
centroid,
nearby_concepts: neighbors,
novelty_score,
coherence_score,
}))
}
}
#[cfg(not(feature = "semantic-reasoning"))]
impl<'a> Muse<'a> {
pub fn new(_db: &'a AletheiaDB) -> Self {
panic!(
"Experimental feature 'Muse' requires the 'nova' feature. Please enable it in Cargo.toml."
);
}
#[cfg(test)]
fn new_internal(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn inspire(
&self,
_nodes: &[NodeId],
_property: Option<&str>,
_limit: usize,
) -> Result<Option<Inspiration>> {
panic!(
"Experimental feature 'Muse' requires the 'nova' feature. Please enable it in Cargo.toml."
);
}
}
#[cfg(all(test, feature = "semantic-reasoning"))]
mod tests {
use super::*;
use crate::core::property::PropertyMapBuilder;
use crate::index::vector::{DistanceMetric, HnswConfig};
#[test]
fn test_muse_finds_void() {
let db = AletheiaDB::new().unwrap();
let config = HnswConfig::new(2, DistanceMetric::Cosine);
db.enable_vector_index("vec", config).unwrap();
let props_a = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let a = db.create_node("Node", props_a).unwrap();
let props_b = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let b = db.create_node("Node", props_b).unwrap();
let muse = Muse::new(&db);
let inspiration = muse.inspire(&[a, b], None, 5).unwrap().unwrap();
let c = &inspiration.centroid;
assert!((c[0] - 0.707).abs() < 0.01);
assert!((c[1] - 0.707).abs() < 0.01);
assert!(inspiration.novelty_score > 0.2);
assert!(inspiration.novelty_score < 0.4);
assert!((inspiration.coherence_score - 0.707).abs() < 0.01);
}
#[test]
fn test_muse_finds_crowded_space() {
let db = AletheiaDB::new().unwrap();
let config = HnswConfig::new(2, DistanceMetric::Cosine);
db.enable_vector_index("vec", config).unwrap();
let props_a = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let a = db.create_node("Node", props_a).unwrap();
let props_b = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let b = db.create_node("Node", props_b).unwrap();
let props_c = PropertyMapBuilder::new()
.insert_vector("vec", &[0.707, 0.707])
.build();
let _c = db.create_node("Node", props_c).unwrap();
let muse = Muse::new(&db);
let inspiration = muse.inspire(&[a, b], None, 5).unwrap().unwrap();
assert!(inspiration.novelty_score < 0.01);
}
}
#[cfg(all(test, not(feature = "semantic-reasoning")))]
mod stub_tests {
use super::*;
use crate::AletheiaDB;
#[test]
#[should_panic(expected = "Experimental feature 'Muse' requires the 'nova' feature")]
fn test_stub_new_panics() {
let db = AletheiaDB::new().unwrap();
let _ = Muse::new(&db);
}
#[test]
#[should_panic(expected = "Experimental feature 'Muse' requires the 'nova' feature")]
fn test_stub_inspire_panics() {
let db = AletheiaDB::new().unwrap();
let muse = Muse::new_internal(&db);
let _ = muse.inspire(&[], None, 0);
}
}