pub mod decay;
pub use decay::{DecayLane, DecayParams, decay_weight};
use std::time::SystemTime;
use crate::model::memory::MemoryRecord;
pub trait ScoreLane: Send + Sync {
fn score(&self, mem: &MemoryRecord, ctx: &ScoreContext) -> f32;
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone)]
pub struct ScoreContext {
pub now: SystemTime,
pub query_text: String,
pub letta_mode: bool,
}
impl ScoreContext {
pub fn new(now: SystemTime, query_text: impl Into<String>) -> Self {
Self {
now,
query_text: query_text.into(),
letta_mode: false,
}
}
pub fn with_letta_mode(mut self, on: bool) -> Self {
self.letta_mode = on;
self
}
}
pub const DEFAULT_VECTOR_WEIGHT: f32 = 0.55;
pub const DEFAULT_BM25_WEIGHT: f32 = 0.20;
pub const DEFAULT_RECENCY_WEIGHT: f32 = 0.15;
pub const DEFAULT_DECAY_WEIGHT: f32 = 0.10;
pub fn fuse_default(vector: f32, bm25: f32, recency: f32, decay: f32) -> f32 {
fuse_weighted(
vector,
bm25,
recency,
decay,
DEFAULT_VECTOR_WEIGHT,
DEFAULT_BM25_WEIGHT,
DEFAULT_RECENCY_WEIGHT,
DEFAULT_DECAY_WEIGHT,
)
}
#[allow(clippy::too_many_arguments)]
pub fn fuse_weighted(
vector: f32,
bm25: f32,
recency: f32,
decay: f32,
w_vector: f32,
w_bm25: f32,
w_recency: f32,
w_decay: f32,
) -> f32 {
(vector * w_vector + bm25 * w_bm25 + recency * w_recency + decay * w_decay).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_weights_sum_to_one() {
let s = DEFAULT_VECTOR_WEIGHT
+ DEFAULT_BM25_WEIGHT
+ DEFAULT_RECENCY_WEIGHT
+ DEFAULT_DECAY_WEIGHT;
assert!((s - 1.0).abs() < 1e-6, "weights sum {s} should be 1.0");
}
#[test]
fn fuse_clamps_to_unit_interval() {
let s = fuse_default(-1.0, 0.0, 0.0, 0.0);
assert_eq!(s, 0.0);
let s = fuse_default(2.0, 2.0, 2.0, 2.0);
assert_eq!(s, 1.0);
}
#[test]
fn fuse_default_is_monotonic_in_each_lane() {
let base = fuse_default(0.5, 0.5, 0.5, 0.5);
assert!(fuse_default(0.6, 0.5, 0.5, 0.5) > base);
assert!(fuse_default(0.5, 0.6, 0.5, 0.5) > base);
assert!(fuse_default(0.5, 0.5, 0.6, 0.5) > base);
assert!(fuse_default(0.5, 0.5, 0.5, 0.6) > base);
}
}