mirror of
https://gitea.ingwaz.work/Ingwaz/openbrain-mcp.git
synced 2026-06-15 22:07:08 +00:00
feat(truth): add ECAN attention economy module (#33)
Implement Economic Attention Network (ECAN) for memory importance management: - EcanParams: decay_rate, beta, spread_factor, threshold - decay_sti(): STI decay toward zero over time - update_lti(): LTI accumulation weighted by STI, penalized below threshold - spread(): boost STI for confirmed memories - cycle(): full decay + LTI update weighted by truth value - initialize(): compute initial STI/LTI from truth value and confidence 14 unit tests covering decay convergence, LTI growth/penalty, spread clamping, cycle behavior, initialization, and bounds. Part of #29
This commit is contained in:
@@ -7,6 +7,7 @@ pub mod embedding;
|
||||
pub mod migrations;
|
||||
pub mod tools;
|
||||
pub mod transport;
|
||||
pub mod truth;
|
||||
pub mod ttl;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
274
src/truth/ecan.rs
Normal file
274
src/truth/ecan.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
//! Economic Attention Network (ECAN) for memory importance management.
|
||||
//!
|
||||
//! ECAN assigns Short-Term Importance (STI) and Long-Term Importance (LTI)
|
||||
//! to memories, enabling natural prioritization of verified, frequently-confirmed
|
||||
//! knowledge while deprioritizing stale or unreliable memories.
|
||||
//!
|
||||
//! Based on the OpenCog ECAN model adapted for the Bushidai Truth Engine.
|
||||
|
||||
/// Clamp a value to the range [0.0, 1.0].
|
||||
#[inline]
|
||||
fn clamp01(v: f32) -> f32 {
|
||||
v.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// ECAN parameters controlling attention dynamics.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EcanParams {
|
||||
/// STI decay rate per cycle (0.0–1.0). Higher = slower decay.
|
||||
pub decay_rate: f32,
|
||||
/// LTI update rate — how fast LTI accumulates from STI.
|
||||
pub beta: f32,
|
||||
/// Fraction of STI that spreads to LTI each cycle.
|
||||
pub spread_factor: f32,
|
||||
/// STI threshold below which LTI update is penalized.
|
||||
pub threshold: f32,
|
||||
}
|
||||
|
||||
impl EcanParams {
|
||||
/// Create EcanParams with the given decay rate and spread factor.
|
||||
/// Other parameters use sensible defaults.
|
||||
pub fn new(decay_rate: f32, spread_factor: f32) -> Self {
|
||||
Self {
|
||||
decay_rate: clamp01(decay_rate),
|
||||
beta: 0.01,
|
||||
spread_factor: clamp01(spread_factor),
|
||||
threshold: 0.1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply STI decay. Each cycle, STI moves toward zero.
|
||||
///
|
||||
/// ```text
|
||||
/// new_sti = sti * decay_rate
|
||||
/// ```
|
||||
pub fn decay_sti(&self, sti: f32) -> f32 {
|
||||
clamp01(sti * self.decay_rate)
|
||||
}
|
||||
|
||||
/// Update LTI based on current STI.
|
||||
///
|
||||
/// LTI accumulates slowly when STI is high (memory is actively relevant).
|
||||
/// When STI falls below threshold, LTI update is penalized.
|
||||
///
|
||||
/// ```text
|
||||
/// new_lti = lti * (1 - beta) + spread_factor * sti
|
||||
/// if sti < threshold: new_lti *= 0.5
|
||||
/// ```
|
||||
pub fn update_lti(&self, lti: f32, sti: f32) -> f32 {
|
||||
let mut new_lti = lti * (1.0 - self.beta) + self.spread_factor * sti;
|
||||
if sti < self.threshold {
|
||||
new_lti *= 0.5;
|
||||
}
|
||||
clamp01(new_lti)
|
||||
}
|
||||
|
||||
/// Boost STI by a spread amount (e.g., when a memory is confirmed).
|
||||
pub fn spread(&self, sti: f32, amount: f32) -> f32 {
|
||||
clamp01(sti + amount)
|
||||
}
|
||||
|
||||
/// Run a full ECAN cycle: decay STI, then update LTI.
|
||||
///
|
||||
/// The truth_value is used to weight the STI contribution:
|
||||
/// higher truth values contribute more to LTI accumulation.
|
||||
pub fn cycle(&self, sti: f32, lti: f32, truth_value: f32) -> (f32, f32) {
|
||||
let new_sti = self.decay_sti(sti);
|
||||
// Weight STI contribution by truth value for LTI update
|
||||
let weighted_sti = new_sti * clamp01(truth_value);
|
||||
let new_lti = self.update_lti(lti, weighted_sti);
|
||||
(new_sti, new_lti)
|
||||
}
|
||||
|
||||
/// Compute initial STI and LTI for a newly scored memory.
|
||||
///
|
||||
/// STI starts proportional to truth_value (high truth = high initial attention).
|
||||
/// LTI starts proportional to confidence (high confidence = more reliable).
|
||||
pub fn initialize(&self, truth_value: f32, confidence: f32) -> (f32, f32) {
|
||||
let sti = clamp01(truth_value);
|
||||
let lti = clamp01(confidence * 0.5); // Start LTI conservatively
|
||||
(sti, lti)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EcanParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
decay_rate: 0.95,
|
||||
beta: 0.01,
|
||||
spread_factor: 0.05,
|
||||
threshold: 0.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn default_params() -> EcanParams {
|
||||
EcanParams::default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_decay_reduces_sti() {
|
||||
let params = default_params();
|
||||
let sti = 0.8;
|
||||
let decayed = params.decay_sti(sti);
|
||||
assert!(decayed < sti, "STI should decrease after decay");
|
||||
assert!(decayed > 0.0, "STI should remain positive");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_decay_converges_to_zero() {
|
||||
let params = default_params();
|
||||
let mut sti = 1.0;
|
||||
for _ in 0..200 {
|
||||
sti = params.decay_sti(sti);
|
||||
}
|
||||
assert!(
|
||||
sti < 0.001,
|
||||
"STI should converge near zero after many cycles, got {}",
|
||||
sti
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_lti_grows_with_high_sti() {
|
||||
let params = default_params();
|
||||
let lti = 0.1;
|
||||
let high_sti = 0.9;
|
||||
let new_lti = params.update_lti(lti, high_sti);
|
||||
assert!(
|
||||
new_lti > lti,
|
||||
"LTI should grow when STI is high: {} > {}",
|
||||
new_lti,
|
||||
lti
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_lti_penalized_below_threshold() {
|
||||
let params = default_params();
|
||||
let lti = 0.5;
|
||||
let low_sti = 0.01; // Below threshold of 0.1
|
||||
let new_lti = params.update_lti(lti, low_sti);
|
||||
// With penalty, LTI should be roughly halved
|
||||
assert!(
|
||||
new_lti < lti * 0.6,
|
||||
"LTI should be penalized below threshold: {} < {}",
|
||||
new_lti,
|
||||
lti * 0.6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_lti_not_penalized_above_threshold() {
|
||||
let params = default_params();
|
||||
let lti = 0.5;
|
||||
let above_threshold_sti = 0.5;
|
||||
let new_lti = params.update_lti(lti, above_threshold_sti);
|
||||
// LTI should stay close to original or grow slightly
|
||||
assert!(
|
||||
new_lti >= lti * 0.9,
|
||||
"LTI should not be heavily penalized above threshold: {} >= {}",
|
||||
new_lti,
|
||||
lti * 0.9
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_spread_increases_sti() {
|
||||
let params = default_params();
|
||||
let sti = 0.5;
|
||||
let boosted = params.spread(sti, 0.2);
|
||||
assert_eq!(boosted, 0.7, "Spread should add to STI");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_spread_clamps_to_one() {
|
||||
let params = default_params();
|
||||
let sti = 0.9;
|
||||
let boosted = params.spread(sti, 0.5);
|
||||
assert_eq!(boosted, 1.0, "Spread should clamp at 1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_full_cycle() {
|
||||
let params = default_params();
|
||||
let (new_sti, new_lti) = params.cycle(0.8, 0.3, 0.9);
|
||||
// STI should decay
|
||||
assert!(new_sti < 0.8, "STI should decay in cycle");
|
||||
assert!(new_sti > 0.0, "STI should remain positive");
|
||||
// LTI should adjust based on weighted STI
|
||||
assert!(new_lti > 0.0, "LTI should remain positive");
|
||||
// Both bounded
|
||||
assert!(new_sti <= 1.0 && new_lti <= 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_cycle_low_truth_reduces_lti_contribution() {
|
||||
let params = default_params();
|
||||
let (_, lti_high_truth) = params.cycle(0.8, 0.3, 0.9);
|
||||
let (_, lti_low_truth) = params.cycle(0.8, 0.3, 0.1);
|
||||
// Higher truth value should contribute more to LTI
|
||||
assert!(
|
||||
lti_high_truth >= lti_low_truth,
|
||||
"High truth should contribute more to LTI: {} >= {}",
|
||||
lti_high_truth,
|
||||
lti_low_truth
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_initialize_from_truth_value() {
|
||||
let params = default_params();
|
||||
let (sti, lti) = params.initialize(0.9, 0.8);
|
||||
assert_eq!(sti, 0.9, "Initial STI should equal truth_value");
|
||||
assert!(
|
||||
(lti - 0.4).abs() < 0.001,
|
||||
"Initial LTI should be confidence * 0.5 = 0.4, got {}",
|
||||
lti
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_initialize_zero() {
|
||||
let params = default_params();
|
||||
let (sti, lti) = params.initialize(0.0, 0.0);
|
||||
assert_eq!(sti, 0.0);
|
||||
assert_eq!(lti, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_values_bounded() {
|
||||
let params = default_params();
|
||||
// Test with extreme values
|
||||
let decayed = params.decay_sti(1.5);
|
||||
assert!(decayed <= 1.0, "Decay should clamp to 1.0");
|
||||
|
||||
let lti = params.update_lti(2.0, 2.0);
|
||||
assert!(lti <= 1.0, "LTI update should clamp to 1.0");
|
||||
|
||||
let spread = params.spread(0.8, 0.5);
|
||||
assert!(spread <= 1.0, "Spread should clamp to 1.0");
|
||||
|
||||
let (sti, lti) = params.cycle(1.5, 1.5, 1.5);
|
||||
assert!(sti <= 1.0 && lti <= 1.0, "Cycle should clamp outputs");
|
||||
|
||||
let (sti, lti) = params.initialize(2.0, 2.0);
|
||||
assert!(
|
||||
sti <= 1.0 && lti <= 1.0,
|
||||
"Initialize should clamp outputs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecan_from_new() {
|
||||
let params = EcanParams::new(0.90, 0.10);
|
||||
assert_eq!(params.decay_rate, 0.90);
|
||||
assert_eq!(params.spread_factor, 0.10);
|
||||
assert_eq!(params.beta, 0.01);
|
||||
assert_eq!(params.threshold, 0.1);
|
||||
}
|
||||
}
|
||||
14
src/truth/mod.rs
Normal file
14
src/truth/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
//! Truth scoring engine — PLN deduction, ECAN attention, and memory scoring.
|
||||
//!
|
||||
//! This module implements neuro-symbolic reasoning for evaluating the
|
||||
//! truthfulness of stored memories. Based on the Bushidai Truth Simulator
|
||||
//! by Thijs Smits (TS87).
|
||||
//!
|
||||
//! ## Components
|
||||
//!
|
||||
//! - **PLN** (Probabilistic Logic Networks): Deduction rules for computing
|
||||
//! truth values from evidence chains.
|
||||
//! - **ECAN** (Economic Attention Network): Manages short-term and long-term
|
||||
//! importance of memories, enabling natural prioritization of verified knowledge.
|
||||
|
||||
pub mod ecan;
|
||||
Reference in New Issue
Block a user