use crate::error::Result;
use crate::world_model::{
summarize_world_model, ActionKind, ActionOutcome, StateFeatures, TransitionModel,
WorldModelSummary,
};
use super::{now, YantrikDB};
const TRANSITION_MODEL_META_KEY: &str = "world_transition_model";
impl YantrikDB {
pub fn load_transition_model(&self) -> Result<TransitionModel> {
let meta = Self::get_meta(&self.conn(), TRANSITION_MODEL_META_KEY)?;
match meta {
Some(json) => {
let mut model: TransitionModel = serde_json::from_str(&json).map_err(|e| {
crate::error::YantrikDbError::Database(rusqlite::Error::ToSqlConversionFailure(
Box::new(e),
))
})?;
model.rebuild_index();
Ok(model)
}
None => Ok(TransitionModel::new()),
}
}
pub fn save_transition_model(&self, model: &TransitionModel) -> 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![TRANSITION_MODEL_META_KEY, json],
)?;
Ok(())
}
pub fn record_transition(
&self,
features: StateFeatures,
action: ActionKind,
outcome: ActionOutcome,
) -> Result<()> {
let mut model = self.load_transition_model()?;
model.record(features, action, outcome);
self.save_transition_model(&model)
}
pub fn predict_outcome(&self, features: &StateFeatures, action: ActionKind) -> Result<f64> {
let model = self.load_transition_model()?;
Ok(model.expected_success(features, action))
}
pub fn best_action_for_state(
&self,
features: &StateFeatures,
candidates: &[ActionKind],
) -> Result<Option<ActionKind>> {
let model = self.load_transition_model()?;
Ok(model.best_action(features, candidates))
}
pub fn world_model_summary(&self) -> Result<WorldModelSummary> {
let model = self.load_transition_model()?;
Ok(summarize_world_model(&model))
}
pub fn reset_transition_model(&self) -> Result<()> {
self.save_transition_model(&TransitionModel::new())
}
}
#[cfg(test)]
mod tests {
use crate::engine::YantrikDB;
use crate::world_model::{ActionKind, ActionOutcome, StateFeatures};
fn test_db() -> YantrikDB {
YantrikDB::new(":memory:", 8).unwrap()
}
fn noon() -> StateFeatures {
StateFeatures::discretize(43200.0, 0.7, 600.0, 0.0, 1)
}
#[test]
fn test_transition_model_persistence() {
let db = test_db();
db.record_transition(noon(), ActionKind::ExecuteTool, ActionOutcome::Succeeded)
.unwrap();
db.record_transition(noon(), ActionKind::ExecuteTool, ActionOutcome::Succeeded)
.unwrap();
db.record_transition(noon(), ActionKind::ExecuteTool, ActionOutcome::Failed)
.unwrap();
let success = db
.predict_outcome(&noon(), ActionKind::ExecuteTool)
.unwrap();
assert!(success > 0.4, "Expected success > 0.4, got {:.3}", success);
}
#[test]
fn test_best_action() {
let db = test_db();
for _ in 0..10 {
db.record_transition(
noon(),
ActionKind::SurfaceSuggestion,
ActionOutcome::Accepted,
)
.unwrap();
}
for _ in 0..10 {
db.record_transition(
noon(),
ActionKind::SendNotification,
ActionOutcome::Rejected,
)
.unwrap();
}
let best = db
.best_action_for_state(
&noon(),
&[ActionKind::SurfaceSuggestion, ActionKind::SendNotification],
)
.unwrap();
assert_eq!(best, Some(ActionKind::SurfaceSuggestion));
}
#[test]
fn test_world_model_summary() {
let db = test_db();
for _ in 0..5 {
db.record_transition(noon(), ActionKind::InvokeLlm, ActionOutcome::Succeeded)
.unwrap();
}
let summary = db.world_model_summary().unwrap();
assert_eq!(summary.total_transitions, 5);
assert_eq!(summary.unique_pairs, 1);
}
#[test]
fn test_reset() {
let db = test_db();
db.record_transition(noon(), ActionKind::ExecuteTool, ActionOutcome::Succeeded)
.unwrap();
db.reset_transition_model().unwrap();
let summary = db.world_model_summary().unwrap();
assert_eq!(summary.total_transitions, 0);
}
}