use crate::error::Result;
use crate::receptivity::{
ActivityState, ContextSnapshot, NotificationMode, ReceptivityEstimate,
ReceptivityModel, SuggestionOutcome,
};
use super::{now, YantrikDB};
const RECEPTIVITY_META_KEY: &str = "receptivity_model";
impl YantrikDB {
pub fn load_receptivity_model(&self) -> Result<ReceptivityModel> {
let meta = Self::get_meta(&self.conn(), RECEPTIVITY_META_KEY)?;
match meta {
Some(json) => serde_json::from_str(&json).map_err(|e| {
crate::error::YantrikDbError::Database(
rusqlite::Error::ToSqlConversionFailure(Box::new(e)),
)
}),
None => Ok(ReceptivityModel::new()),
}
}
pub fn save_receptivity_model(&self, model: &ReceptivityModel) -> Result<()> {
let json = serde_json::to_string(model).map_err(|e| {
crate::error::YantrikDbError::Database(
rusqlite::Error::ToSqlConversionFailure(Box::new(e)),
)
})?;
self.conn().execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)",
rusqlite::params![RECEPTIVITY_META_KEY, json],
)?;
Ok(())
}
pub fn estimate_receptivity(
&self,
context: &ContextSnapshot,
) -> Result<ReceptivityEstimate> {
let model = self.load_receptivity_model()?;
Ok(model.estimate(context))
}
pub fn is_user_receptive(
&self,
context: &ContextSnapshot,
) -> Result<bool> {
let estimate = self.estimate_receptivity(context)?;
Ok(estimate.is_receptive(0.5))
}
pub fn is_quiet_hours(&self) -> Result<bool> {
let model = self.load_receptivity_model()?;
Ok(model.quiet_hours.is_quiet(now()))
}
pub fn attention_budget_remaining(
&self,
suggestions_used: u32,
) -> Result<u32> {
let model = self.load_receptivity_model()?;
Ok(model.attention_budget.remaining(suggestions_used))
}
pub fn receptivity_observe(
&self,
context: &ContextSnapshot,
outcome: SuggestionOutcome,
) -> Result<()> {
let mut model = self.load_receptivity_model()?;
model.observe_outcome(context, outcome);
self.save_receptivity_model(&model)
}
pub fn receptivity_train(
&self,
history: &[(ContextSnapshot, SuggestionOutcome)],
) -> Result<u64> {
let mut model = self.load_receptivity_model()?;
for (ctx, outcome) in history {
model.observe_outcome(ctx, *outcome);
}
let count = model.training_count;
self.save_receptivity_model(&model)?;
Ok(count)
}
}
#[cfg(test)]
mod tests {
use crate::engine::YantrikDB;
use crate::receptivity::*;
fn test_db() -> YantrikDB {
YantrikDB::new(":memory:", 8).unwrap()
}
#[test]
fn test_receptivity_persistence_roundtrip() {
let db = test_db();
let mut model = ReceptivityModel::new();
model.bias = 1.5;
model.training_count = 42;
db.save_receptivity_model(&model).unwrap();
let loaded = db.load_receptivity_model().unwrap();
assert!((loaded.bias - 1.5).abs() < 1e-10);
assert_eq!(loaded.training_count, 42);
}
#[test]
fn test_estimate_receptivity() {
let db = test_db();
let context = ContextSnapshot {
now: 50000.0,
activity: ActivityState::Idle,
recent_interactions_15min: 5,
recent_outcomes: (3, 0, 0),
secs_since_last_interaction: 30.0,
session_duration_secs: 600.0,
emotional_valence: 0.3,
session_suggestions_accepted: 2,
session_suggestion_budget: 20,
notification_mode: NotificationMode::All,
};
let estimate = db.estimate_receptivity(&context).unwrap();
assert!(estimate.score >= 0.0 && estimate.score <= 1.0);
assert!(!estimate.factors.is_empty());
}
#[test]
fn test_receptivity_learning() {
let db = test_db();
let receptive_context = ContextSnapshot {
now: 50000.0,
activity: ActivityState::TaskSwitching,
recent_interactions_15min: 8,
recent_outcomes: (5, 0, 0),
secs_since_last_interaction: 10.0,
session_duration_secs: 1200.0,
emotional_valence: 0.5,
session_suggestions_accepted: 3,
session_suggestion_budget: 20,
notification_mode: NotificationMode::All,
};
let est_before = db.estimate_receptivity(&receptive_context).unwrap().score;
for _ in 0..10 {
db.receptivity_observe(&receptive_context, SuggestionOutcome::Accepted)
.unwrap();
}
let est_after = db.estimate_receptivity(&receptive_context).unwrap().score;
assert!(
est_after >= est_before - 0.1, "Score should not decrease much after accepts: {} -> {}",
est_before, est_after
);
}
#[test]
fn test_quiet_hours_check() {
let db = test_db();
let result = db.is_quiet_hours();
assert!(result.is_ok());
}
#[test]
fn test_attention_budget() {
let db = test_db();
assert_eq!(db.attention_budget_remaining(0).unwrap(), 20);
assert_eq!(db.attention_budget_remaining(15).unwrap(), 5);
assert_eq!(db.attention_budget_remaining(25).unwrap(), 0);
}
}