llm_toolkit_expertise/
context.rs1use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum Priority {
17 Critical,
19 High,
21 #[default]
23 Normal,
24 Low,
26}
27
28impl PartialOrd for Priority {
30 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
31 Some(self.cmp(other))
32 }
33}
34
35impl Ord for Priority {
36 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
37 self.weight().cmp(&other.weight())
38 }
39}
40
41impl Priority {
42 pub fn weight(&self) -> u8 {
44 match self {
45 Priority::Critical => 4,
46 Priority::High => 3,
47 Priority::Normal => 2,
48 Priority::Low => 1,
49 }
50 }
51
52 pub fn label(&self) -> &'static str {
54 match self {
55 Priority::Critical => "CRITICAL",
56 Priority::High => "HIGH",
57 Priority::Normal => "NORMAL",
58 Priority::Low => "LOW",
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
68#[serde(tag = "type", rename_all = "snake_case")]
69pub enum ContextProfile {
70 #[default]
72 Always,
73
74 Conditional {
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 task_types: Vec<String>,
79
80 #[serde(default, skip_serializing_if = "Vec::is_empty")]
82 user_states: Vec<String>,
83
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 task_health: Option<TaskHealth>,
87 },
88}
89
90impl ContextProfile {
91 pub fn matches(&self, context: &ContextMatcher) -> bool {
93 match self {
94 ContextProfile::Always => true,
95 ContextProfile::Conditional {
96 task_types,
97 user_states,
98 task_health,
99 } => {
100 let task_match = task_types.is_empty()
101 || context
102 .task_type
103 .as_ref()
104 .map(|t| task_types.contains(t))
105 .unwrap_or(false);
106
107 let user_match = user_states.is_empty()
108 || context
109 .user_state
110 .as_ref()
111 .map(|s| user_states.contains(s))
112 .unwrap_or(false);
113
114 let health_match = task_health
115 .as_ref()
116 .map(|h| context.task_health.as_ref() == Some(h))
117 .unwrap_or(true);
118
119 task_match && user_match && health_match
120 }
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
130#[serde(rename_all = "snake_case")]
131pub enum TaskHealth {
132 OnTrack,
134
135 AtRisk,
137
138 OffTrack,
140}
141
142impl TaskHealth {
143 pub fn label(&self) -> &'static str {
145 match self {
146 TaskHealth::OnTrack => "On Track",
147 TaskHealth::AtRisk => "At Risk",
148 TaskHealth::OffTrack => "Off Track",
149 }
150 }
151
152 pub fn emoji(&self) -> &'static str {
154 match self {
155 TaskHealth::OnTrack => "✅",
156 TaskHealth::AtRisk => "⚠️",
157 TaskHealth::OffTrack => "🚫",
158 }
159 }
160}
161
162#[derive(Debug, Clone, Default)]
166pub struct ContextMatcher {
167 pub task_type: Option<String>,
168 pub user_state: Option<String>,
169 pub task_health: Option<TaskHealth>,
170}
171
172impl ContextMatcher {
173 pub fn new() -> Self {
175 Self::default()
176 }
177
178 pub fn with_task_type(mut self, task_type: impl Into<String>) -> Self {
180 self.task_type = Some(task_type.into());
181 self
182 }
183
184 pub fn with_user_state(mut self, user_state: impl Into<String>) -> Self {
186 self.user_state = Some(user_state.into());
187 self
188 }
189
190 pub fn with_task_health(mut self, task_health: TaskHealth) -> Self {
192 self.task_health = Some(task_health);
193 self
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_priority_ordering() {
203 assert!(Priority::Critical > Priority::High);
204 assert!(Priority::High > Priority::Normal);
205 assert!(Priority::Normal > Priority::Low);
206 }
207
208 #[test]
209 fn test_priority_weight() {
210 assert_eq!(Priority::Critical.weight(), 4);
211 assert_eq!(Priority::High.weight(), 3);
212 assert_eq!(Priority::Normal.weight(), 2);
213 assert_eq!(Priority::Low.weight(), 1);
214 }
215
216 #[test]
217 fn test_context_always_matches() {
218 let profile = ContextProfile::Always;
219 let context = ContextMatcher::new();
220 assert!(profile.matches(&context));
221 }
222
223 #[test]
224 fn test_context_conditional_matching() {
225 let profile = ContextProfile::Conditional {
226 task_types: vec!["Debug".to_string()],
227 user_states: vec![],
228 task_health: Some(TaskHealth::AtRisk),
229 };
230
231 let matching_context = ContextMatcher::new()
232 .with_task_type("Debug")
233 .with_task_health(TaskHealth::AtRisk);
234
235 let non_matching_context = ContextMatcher::new()
236 .with_task_type("Review")
237 .with_task_health(TaskHealth::OnTrack);
238
239 assert!(profile.matches(&matching_context));
240 assert!(!profile.matches(&non_matching_context));
241 }
242}