use crate::context::{ContextProfile, TaskHealth};
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct RenderContext {
pub task_type: Option<String>,
pub user_states: Vec<String>,
pub task_health: Option<TaskHealth>,
}
impl RenderContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_task_type(mut self, task_type: impl Into<String>) -> Self {
self.task_type = Some(task_type.into());
self
}
pub fn with_user_state(mut self, state: impl Into<String>) -> Self {
self.user_states.push(state.into());
self
}
pub fn with_task_health(mut self, health: TaskHealth) -> Self {
self.task_health = Some(health);
self
}
pub fn matches(&self, profile: &ContextProfile) -> bool {
match profile {
ContextProfile::Always => true,
ContextProfile::Conditional {
task_types,
user_states,
task_health,
} => {
let task_type_match = if task_types.is_empty() {
true } else {
self.task_type
.as_ref()
.map(|tt| task_types.contains(tt))
.unwrap_or(false)
};
let user_state_match = if user_states.is_empty() {
true } else {
self.user_states
.iter()
.any(|state| user_states.contains(state))
};
let task_health_match = if let Some(required_health) = task_health {
self.task_health.as_ref() == Some(required_health)
} else {
true };
task_type_match && user_state_match && task_health_match
}
}
}
}
#[derive(Debug, Clone)]
pub struct ContextualPrompt<'a> {
expertise: &'a super::Expertise,
context: RenderContext,
}
impl<'a> ContextualPrompt<'a> {
pub fn from_expertise(expertise: &'a super::Expertise, context: RenderContext) -> Self {
Self { expertise, context }
}
pub fn with_task_type(mut self, task_type: impl Into<String>) -> Self {
self.context = self.context.with_task_type(task_type);
self
}
pub fn with_user_state(mut self, state: impl Into<String>) -> Self {
self.context = self.context.with_user_state(state);
self
}
pub fn with_task_health(mut self, health: TaskHealth) -> Self {
self.context = self.context.with_task_health(health);
self
}
pub fn to_prompt(&self) -> String {
self.expertise.to_prompt_with_context(&self.context)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_context_builder() {
let context = RenderContext::new()
.with_task_type("security-review")
.with_user_state("beginner")
.with_task_health(TaskHealth::AtRisk);
assert_eq!(context.task_type, Some("security-review".to_string()));
assert_eq!(context.user_states, vec!["beginner"]);
assert_eq!(context.task_health, Some(TaskHealth::AtRisk));
}
#[test]
fn test_matches_always() {
let context = RenderContext::new();
assert!(context.matches(&ContextProfile::Always));
}
#[test]
fn test_matches_task_type() {
let context = RenderContext::new().with_task_type("security-review");
let profile = ContextProfile::Conditional {
task_types: vec!["security-review".to_string()],
user_states: vec![],
task_health: None,
};
assert!(context.matches(&profile));
let wrong_profile = ContextProfile::Conditional {
task_types: vec!["code-review".to_string()],
user_states: vec![],
task_health: None,
};
assert!(!context.matches(&wrong_profile));
}
#[test]
fn test_matches_user_state() {
let context = RenderContext::new()
.with_user_state("beginner")
.with_user_state("confused");
let profile = ContextProfile::Conditional {
task_types: vec![],
user_states: vec!["beginner".to_string()],
task_health: None,
};
assert!(context.matches(&profile));
let profile2 = ContextProfile::Conditional {
task_types: vec![],
user_states: vec!["expert".to_string()],
task_health: None,
};
assert!(!context.matches(&profile2));
}
#[test]
fn test_matches_task_health() {
let context = RenderContext::new().with_task_health(TaskHealth::AtRisk);
let profile = ContextProfile::Conditional {
task_types: vec![],
user_states: vec![],
task_health: Some(TaskHealth::AtRisk),
};
assert!(context.matches(&profile));
let wrong_profile = ContextProfile::Conditional {
task_types: vec![],
user_states: vec![],
task_health: Some(TaskHealth::OnTrack),
};
assert!(!context.matches(&wrong_profile));
}
#[test]
fn test_matches_combined() {
let context = RenderContext::new()
.with_task_type("security-review")
.with_user_state("beginner")
.with_task_health(TaskHealth::AtRisk);
let profile = ContextProfile::Conditional {
task_types: vec!["security-review".to_string()],
user_states: vec!["beginner".to_string()],
task_health: Some(TaskHealth::AtRisk),
};
assert!(context.matches(&profile));
let partial_profile = ContextProfile::Conditional {
task_types: vec!["security-review".to_string()],
user_states: vec!["expert".to_string()], task_health: Some(TaskHealth::AtRisk),
};
assert!(!context.matches(&partial_profile));
}
#[test]
fn test_matches_no_constraints() {
let context = RenderContext::new().with_task_type("anything");
let profile = ContextProfile::Conditional {
task_types: vec![],
user_states: vec![],
task_health: None,
};
assert!(context.matches(&profile));
}
}