use crate::traits::{BehaviorPattern, Episode, Goal, Procedure, UserProfile};
use crate::utils::truncate_str;
#[derive(Debug, Clone)]
pub struct Suggestion {
#[allow(dead_code)]
pub text: String,
#[allow(dead_code)] pub source: SuggestionSource,
#[allow(dead_code)] pub confidence: f32,
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub enum SuggestionSource {
Pattern(i64), Goal(String), Procedure(i64), Episode(i64), }
pub struct SuggestionContext {
#[allow(dead_code)] pub last_action: Option<String>,
#[allow(dead_code)] pub current_topic: Option<String>,
pub relevant_pattern_ids: Vec<i64>,
pub relevant_goal_ids: Vec<String>,
pub relevant_procedure_ids: Vec<i64>,
pub relevant_episode_ids: Vec<i64>,
#[allow(dead_code)] pub session_duration_mins: i32,
#[allow(dead_code)] pub tool_call_count: i32,
#[allow(dead_code)] pub has_errors: bool,
#[allow(dead_code)] pub user_message: String,
}
pub struct ProactiveEngine {
patterns: Vec<BehaviorPattern>,
goals: Vec<Goal>,
procedures: Vec<Procedure>,
recent_episodes: Vec<Episode>,
profile: UserProfile,
}
impl ProactiveEngine {
pub fn new(
patterns: Vec<BehaviorPattern>,
goals: Vec<Goal>,
procedures: Vec<Procedure>,
recent_episodes: Vec<Episode>,
profile: UserProfile,
) -> Self {
Self {
patterns,
goals,
procedures,
recent_episodes,
profile,
}
}
pub fn get_suggestions(&self, context: &SuggestionContext) -> Vec<Suggestion> {
if !self.profile.likes_suggestions {
return vec![];
}
let mut suggestions = vec![];
suggestions.extend(self.pattern_suggestions(context));
suggestions.extend(self.goal_suggestions(context));
suggestions.extend(self.procedure_suggestions(context));
suggestions.extend(self.episode_suggestions(context));
suggestions.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
suggestions.truncate(3);
suggestions
}
#[allow(dead_code)] pub fn get_top_suggestion(
&self,
context: &SuggestionContext,
min_confidence: f32,
) -> Option<Suggestion> {
self.get_suggestions(context)
.into_iter()
.find(|s| s.confidence >= min_confidence)
}
fn pattern_suggestions(&self, context: &SuggestionContext) -> Vec<Suggestion> {
let mut suggestions = vec![];
for pattern in &self.patterns {
if pattern.confidence < 0.6 {
continue;
}
let matches = match pattern.pattern_type.as_str() {
"sequence" | "trigger" => context.relevant_pattern_ids.contains(&pattern.id),
"habit" => {
pattern.occurrence_count >= 3
}
_ => false,
};
if matches {
if let Some(action) = &pattern.action {
suggestions.push(Suggestion {
text: format!("Based on your usual pattern: {}", action),
source: SuggestionSource::Pattern(pattern.id),
confidence: pattern.confidence,
});
}
}
}
suggestions
}
fn goal_suggestions(&self, context: &SuggestionContext) -> Vec<Suggestion> {
let mut suggestions = vec![];
for goal in &self.goals {
if goal.status != "active" {
continue;
}
let relevant = context.relevant_goal_ids.contains(&goal.id);
if relevant {
let confidence = match goal.priority.as_str() {
"high" => 0.8,
"medium" => 0.6,
_ => 0.4,
};
suggestions.push(Suggestion {
text: format!("This relates to your goal: {}", goal.description),
source: SuggestionSource::Goal(goal.id.clone()),
confidence,
});
}
}
suggestions
}
fn procedure_suggestions(&self, context: &SuggestionContext) -> Vec<Suggestion> {
let mut suggestions = vec![];
for procedure in &self.procedures {
if procedure.success_count <= procedure.failure_count {
continue;
}
let matches = context.relevant_procedure_ids.contains(&procedure.id);
if matches {
let success_rate = procedure.success_count as f32
/ (procedure.success_count + procedure.failure_count) as f32;
suggestions.push(Suggestion {
text: format!("I know how to handle this (procedure: {})", procedure.name),
source: SuggestionSource::Procedure(procedure.id),
confidence: success_rate * 0.8, });
}
}
suggestions
}
fn episode_suggestions(&self, context: &SuggestionContext) -> Vec<Suggestion> {
let mut suggestions = vec![];
for episode in &self.recent_episodes {
let topic_match = context.relevant_episode_ids.contains(&episode.id);
if topic_match {
let confidence = episode.importance * 0.6;
suggestions.push(Suggestion {
text: format!(
"We've worked on something similar before: {}",
truncate_str(&episode.summary, 50)
),
source: SuggestionSource::Episode(episode.id),
confidence,
});
}
}
suggestions
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn make_profile(likes_suggestions: bool) -> UserProfile {
UserProfile {
id: 1,
verbosity_preference: "medium".to_string(),
explanation_depth: "moderate".to_string(),
tone_preference: "neutral".to_string(),
emoji_preference: "none".to_string(),
typical_session_length: None,
active_hours: None,
common_workflows: None,
asks_before_acting: true,
prefers_explanations: true,
likes_suggestions,
updated_at: Utc::now(),
}
}
#[test]
fn test_no_suggestions_when_disabled() {
let engine = ProactiveEngine::new(vec![], vec![], vec![], vec![], make_profile(false));
let context = SuggestionContext {
last_action: None,
current_topic: None,
relevant_pattern_ids: vec![],
relevant_goal_ids: vec![],
relevant_procedure_ids: vec![],
relevant_episode_ids: vec![],
session_duration_mins: 5,
tool_call_count: 0,
has_errors: false,
user_message: "test".to_string(),
};
let suggestions = engine.get_suggestions(&context);
assert!(suggestions.is_empty());
}
#[test]
fn test_goal_suggestion() {
let mut goal = Goal::new_finite("Complete the Rust migration", "test-session");
goal.priority = "high".to_string();
let goal_id = goal.id.clone();
let engine = ProactiveEngine::new(vec![], vec![goal], vec![], vec![], make_profile(true));
let context = SuggestionContext {
last_action: None,
current_topic: None,
relevant_pattern_ids: vec![],
relevant_goal_ids: vec![goal_id],
relevant_procedure_ids: vec![],
relevant_episode_ids: vec![],
session_duration_mins: 5,
tool_call_count: 0,
has_errors: false,
user_message: "Anything".to_string(),
};
let suggestions = engine.get_suggestions(&context);
assert!(!suggestions.is_empty());
}
#[test]
fn test_goal_suggestion_does_not_use_keyword_guessing() {
let mut goal = Goal::new_finite("Complete the Rust migration", "test-session");
goal.priority = "high".to_string();
let engine = ProactiveEngine::new(vec![], vec![goal], vec![], vec![], make_profile(true));
let context = SuggestionContext {
last_action: None,
current_topic: Some("rust".to_string()),
relevant_pattern_ids: vec![],
relevant_goal_ids: vec![],
relevant_procedure_ids: vec![],
relevant_episode_ids: vec![],
session_duration_mins: 5,
tool_call_count: 0,
has_errors: false,
user_message: "help me with rust".to_string(),
};
let suggestions = engine.get_suggestions(&context);
assert!(suggestions.is_empty());
}
}